Files
2nd/10_Wiki/Topics/Coding/Game_Asset_Pipeline.md
T
2026-05-09 21:08:02 +09:00

6.5 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
game-asset-pipeline Game Asset Pipeline — Texture / Audio / Mesh 압축 Coding draft B conceptual 2026-05-09 2026-05-09
game
asset
pipeline
vibe-coding
language applicable_to
Various
Game
asset pipeline
KTX2
basis universal
draco
audio compression
atlas

Game Asset Pipeline

Asset = bundle 의 90%. Texture (KTX2 / Basis), Mesh (Draco / Meshopt), Audio (Opus), Sprite atlas, model LOD. 빌드 시 자동 처리 + CDN.

📖 핵심 개념

  • Texture: 가장 큰 부분.
  • Atlas: 작은 sprite 합치기 — draw call ↓.
  • LOD: 거리별 mesh 다른 detail.
  • Streaming: 큰 game = 점진 download.

💻 코드 패턴

Texture compression

원본 PNG: 1MB
JPEG: 200KB (lossy, no transparency)
WebP: 100KB (lossy, transparent OK)
AVIF: 80KB (latest)

GPU 압축 (game-specific):
KTX2 + Basis Universal:  ~10% size, GPU 직접 사용
DDS / S3TC / BC:         legacy, vendor-specific
ETC2:                     mobile
ASTC:                      modern mobile/desktop

Basis Universal (cross-GPU)

# Convert
basisu input.png -ktx2 -comp_level 4 -uastc

# Output: input.ktx2
# Three.js / Bevy 등 직접 load
import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';

const ktx2Loader = new KTX2Loader()
  .setTranscoderPath('/basis/')
  .detectSupport(renderer);

ktx2Loader.load('texture.ktx2', (texture) => {
  // 자동 GPU format 으로 transcoding
});

→ Web 에서 작은 download + GPU memory 작게.

Sprite atlas

# TexturePacker / free-tex-packer
# 입력: 100 작은 png
# 출력: 1 큰 png + JSON
// PixiJS spritesheet
const sheet = await Assets.load('atlas.json');
const sprite = new Sprite(sheet.textures['player_idle.png']);

→ 100 draw call → 1.

9-slice / NinePatch (UI)

// 큰 button 도 작은 image 로
{ left: 10, top: 10, right: 10, bottom: 10 }
// corner 안 stretch, edge / center 만.

Mesh compression — Draco

# GLTF + Draco
gltf-pipeline -i model.glb -o compressed.glb -d
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';

const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath('/draco/');
loader.setDRACOLoader(dracoLoader);
loader.load('model.glb', (gltf) => { ... });

→ Mesh 90% 감소 가능.

Meshopt (alternative, 빠른 decode)

gltfpack -i input.glb -o output.glb -cc  # compression
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
loader.setMeshoptDecoder(MeshoptDecoder);

LOD (Level of Detail)

import { LOD } from 'three';

const lod = new LOD();
lod.addLevel(highMesh, 0);     // 0-10 m
lod.addLevel(midMesh, 10);     // 10-50 m
lod.addLevel(lowMesh, 50);     // 50+ m
scene.add(lod);

→ 거리별 detail.

Audio compression

WAV: 10MB
MP3: 1MB (legacy)
Ogg Vorbis: 1MB
Opus: 0.7MB (modern best)
AAC: iOS 친화

음악 = streaming + lossy
효과음 (짧음, 즉시) = WAV / 작은 OGG
ffmpeg -i music.wav -c:a libopus -b:a 96k music.opus

Streaming audio

// Howler — preload + lazy
const music = new Howl({ src: ['music.opus'], preload: true, html5: true });
music.play();

→ 큰 file 도 stream.

Asset bundling (Webpack/Vite)

// Static import
import playerImg from './assets/player.png';

// Dynamic
const url = new URL('./assets/player.png', import.meta.url).href;

// Vite — assets folder
// Vite 가 hash + optimize.

Bundle / chunks

// 큰 model = lazy
const heavyModel = lazy(() => import('./HeavyModel'));

// 또는 preload
const link = document.createElement('link');
link.rel = 'prefetch';
link.href = '/textures/level2.ktx2';
document.head.appendChild(link);

CDN serving

모든 asset = CDN.
Cache-Control: max-age=31536000, immutable (hash 가 url 안)
ETag / Last-Modified 자동.
// Hash filename (Vite default)
// player.abc123.png — content-addressed

Texture atlas + GPU instancing

// 같은 mesh 천 개 = 한 draw call
const inst = new InstancedMesh(geometry, material, 1000);
for (let i = 0; i < 1000; i++) {
  matrix.setPosition(x, y, z);
  inst.setMatrixAt(i, matrix);
}
inst.instanceMatrix.needsUpdate = true;

Asset budget

Web game (low-end mobile):
- Total: ~20MB initial download
- JS:    ~3MB
- Textures: ~5MB
- Models: ~3MB
- Audio:  ~5MB

Game level:
- 1-3 MB compressed level

Hot-reload (dev)

if (import.meta.hot) {
  import.meta.hot.accept('./player.png', (newUrl) => {
    playerSprite.texture = await loader.load(newUrl);
  });
}

→ Asset 변경 즉시 반영.

Preload screen

<LoadScreen progress={progress}>
  <Suspense fallback={null}>
    <Game />
  </Suspense>
</LoadScreen>
const assets = [
  '/player.ktx2', '/level.glb', '/music.opus', /* ... */
];
let loaded = 0;
for (const a of assets) {
  await preload(a);
  loaded++;
  setProgress(loaded / assets.length);
}

Tilemap (2D level)

# Tiled editor → tmx file
# Phaser / Pixi 가 직접 load

Animation export

- 2D sprite: Aseprite / TexturePacker → spritesheet
- 3D: Blender → GLB (animations clip 포함)
- Spine / DragonBones: skeletal 2D animation

Texture mipmaps

GPU 가 거리별 다른 size 자동.
KTX2 안 자동 포함.

Manual:
texture.generateMipmaps = true;
texture.minFilter = THREE.LinearMipmapLinearFilter;

Asset versioning + cache invalidation

manifest.json:
{
  "player.png": "abc123",
  "level1.glb": "def456"
}

→ 변경 시 hash 변경 → CDN 자동.

🤔 의사결정 기준

Asset 추천
Web game texture KTX2 + Basis
Mobile native ASTC / ETC2
Sprite Atlas + WebP
3D model GLB + Draco / Meshopt
Music Opus streaming
SFX Ogg / WAV
UI icon SVG / WebP
Big level Streamed chunks

안티패턴

  • PNG / JPG GPU 사용: GPU 가 RGBA 압축 X. KTX2.
  • 개별 sprite: draw call 폭발. atlas.
  • Mesh 압축 X: GLB 100MB.
  • 모든 quality 제공: bundle 폭발. LOD + dynamic.
  • Audio WAV: 큰 file. Opus.
  • Asset hot path 매번 load: cache.
  • Preload 모든 거: 첫 load 매우 길음. lazy.

🤖 LLM 활용 힌트

  • KTX2 + Draco + Opus + atlas 4종 = 90% 감소.
  • LOD + mipmaps + instancing = 60fps.
  • CDN + immutable cache.

🔗 관련 문서