--- id: game-asset-pipeline title: Game Asset Pipeline — Texture / Audio / Mesh 압축 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [game, asset, pipeline, vibe-coding] tech_stack: { language: "Various", applicable_to: ["Game"] } applied_in: [] aliases: [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) ```bash # Convert basisu input.png -ktx2 -comp_level 4 -uastc # Output: input.ktx2 # Three.js / Bevy 등 직접 load ``` ```ts 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 ```bash # TexturePacker / free-tex-packer # 입력: 100 작은 png # 출력: 1 큰 png + JSON ``` ```ts // 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) ```ts // 큰 button 도 작은 image 로 { left: 10, top: 10, right: 10, bottom: 10 } // corner 안 stretch, edge / center 만. ``` ### Mesh compression — Draco ```bash # GLTF + Draco gltf-pipeline -i model.glb -o compressed.glb -d ``` ```ts 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) ```bash gltfpack -i input.glb -o output.glb -cc # compression ``` ```ts import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'; loader.setMeshoptDecoder(MeshoptDecoder); ``` ### LOD (Level of Detail) ```ts 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 ``` ```bash ffmpeg -i music.wav -c:a libopus -b:a 96k music.opus ``` ### Streaming audio ```ts // Howler — preload + lazy const music = new Howl({ src: ['music.opus'], preload: true, html5: true }); music.play(); ``` → 큰 file 도 stream. ### Asset bundling (Webpack/Vite) ```ts // 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 ```ts // 큰 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 자동. ``` ```ts // Hash filename (Vite default) // player.abc123.png — content-addressed ``` ### Texture atlas + GPU instancing ```ts // 같은 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) ```ts if (import.meta.hot) { import.meta.hot.accept('./player.png', (newUrl) => { playerSprite.texture = await loader.load(newUrl); }); } ``` → Asset 변경 즉시 반영. ### Preload screen ```tsx ``` ```ts 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) ```bash # 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. ## 🔗 관련 문서 - [[Frontend_Image_Optimization]] - [[Mobile_App_Size_Optimization]] - [[Frontend_Three_R3F]]