--- id: wiki-2026-0508-instancedmesh-사용-시-드로우-콜-최적화의-한계 title: InstancedMesh 사용 시 드로우 콜 최적화의 한계점 사례 연구 category: 10_Wiki/Topics status: verified canonical_id: self aliases: [InstancedMesh Limits, Instancing Limits] duplicate_of: none source_trust_level: A confidence_score: 0.9 verification_status: applied tags: [graphics, three-js, performance, instancing] raw_sources: [] last_reinforced: 2026-05-10 github_commit: pending tech_stack: language: JavaScript/GLSL framework: Three.js r170+/WebGL2/WebGPU --- # InstancedMesh 사용 시 드로우 콜 최적화의 한계점 사례 연구 ## 매 한 줄 > **"매 InstancedMesh 매 1 draw call로 N copies — but 매 frustum culling, material variation, animation, picking 의 cost가 instance 수에 따라 explode."**. 매 naive 사용 시 draw call 매 줄어들어도 GPU vertex/fragment 매 burden, CPU matrix update 매 bottleneck. 매 production 매 LOD + spatial partition + GPU culling 매 결합. ## 매 핵심 ### 매 한계 list - **No per-instance frustum culling**: 매 single bounding sphere → 매 모든 instance가 frustum 안에 있다고 GPU가 가정. - **No per-instance material**: 매 same material → color/texture variation 매 instanceColor / instance attribute 의 manual. - **Animation cost**: 매 instance마다 matrix update → CPU bound at 10k+. - **Picking 어려움**: raycaster 매 instance index 매 별도 처리. - **Memory**: 16 floats × N instances = 매 1M instances → 64MB matrix buffer. - **Shadow map**: 매 light 마다 또 한 번 instanced draw — culling 없으면 shadow waste. ### 매 case: 100k cubes - Naive InstancedMesh: 매 1 draw call, GPU 60fps but matrix update 60ms/frame on CPU. - Static (`setMatrixAt` once): 매 GPU bound, fillrate 매 issue → LOD 필요. - Dynamic: 매 매 frame matrix update → useDynamicDrawUsage + partial updates. ### 매 응용 (해결 전략) 1. **GPU instancing + GPU culling**: compute shader 매 frustum check, 매 indirect draw. 2. **Spatial partitioning**: octree / BVH로 매 chunk 단위 InstancedMesh. 3. **LOD groups**: distance 별 다른 InstancedMesh (high/med/low/billboard). 4. **BatchedMesh (Three.js r170+)**: 매 different geometries를 single draw call. 5. **Hierarchical LOD + impostor**: 매 far away는 single quad billboard. ## 💻 패턴 ### Frustum culling 수동 (Three.js) ```javascript const frustum = new THREE.Frustum(); const m = new THREE.Matrix4(); m.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse); frustum.setFromProjectionMatrix(m); const dummy = new THREE.Object3D(); const sphere = new THREE.Sphere(new THREE.Vector3(), boundingRadius); let visibleCount = 0; for (let i = 0; i < totalInstances; i++) { sphere.center.copy(positions[i]); if (frustum.intersectsSphere(sphere)) { dummy.position.copy(positions[i]); dummy.updateMatrix(); instancedMesh.setMatrixAt(visibleCount++, dummy.matrix); } } instancedMesh.count = visibleCount; instancedMesh.instanceMatrix.needsUpdate = true; ``` ### Per-instance color ```javascript const mesh = new THREE.InstancedMesh(geo, mat, N); const color = new THREE.Color(); for (let i = 0; i < N; i++) { color.setHSL(i / N, 0.7, 0.5); mesh.setColorAt(i, color); } mesh.instanceColor.needsUpdate = true; // shader auto: gl_InstanceID → vInstanceColor ``` ### Dynamic draw usage (partial updates) ```javascript mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage); // only update changed instances mesh.setMatrixAt(idx, newMatrix); mesh.instanceMatrix.updateRange = { offset: idx * 16, count: 16 }; mesh.instanceMatrix.needsUpdate = true; ``` ### Chunked InstancedMesh (spatial bucket) ```javascript class ChunkedInstances { constructor(geo, mat, chunkSize = 64) { this.chunks = new Map(); // "x,y,z" → InstancedMesh this.chunkSize = chunkSize; } add(pos) { const key = this.chunkKey(pos); if (!this.chunks.has(key)) { this.chunks.set(key, new THREE.InstancedMesh(geo, mat, 1024)); } // ... } cullChunks(frustum) { for (const [key, mesh] of this.chunks) { mesh.visible = frustum.intersectsBox(this.chunkBox(key)); } } } ``` ### BatchedMesh (Three.js r170+) ```javascript const batched = new THREE.BatchedMesh(maxGeoms, maxVerts, maxIdx); const geoIdA = batched.addGeometry(geoA); const geoIdB = batched.addGeometry(geoB); const instA = batched.addInstance(geoIdA); batched.setMatrixAt(instA, matrixA); // 매 different geometries 매 single draw call ``` ### GPU compute culling (WebGPU) ```javascript // compute shader: input matrices + frustum planes → atomic counter + visible matrix buffer const cullPipeline = device.createComputePipeline({...}); pass.setPipeline(cullPipeline); pass.dispatchWorkgroups(Math.ceil(N / 64)); // then drawIndexedIndirect from visible buffer ``` ## 매 결정 기준 | 상황 | Approach | |---|---| | < 1k static instances | Plain InstancedMesh | | 1k–100k mostly static | InstancedMesh + manual frustum culling | | 100k+ static | Chunked InstancedMesh (spatial) + LOD | | Dynamic per-frame (particles) | Points / GPU particle system | | Different geometries | BatchedMesh (r170+) | | Massive (1M+) | WebGPU compute culling + indirect draw | | Picking 필요 | InstancedMesh + raycaster.firstHitOnly = true | **기본값**: < 10k는 plain InstancedMesh, 그 이상 매 chunked + LOD. ## 🔗 Graph - 부모: [[GPU Instancing]] · [[Three.js]] - 변형: [[BatchedMesh]] - Adjacent: [[Frustum Culling]] · [[LOD]] · [[Indirect Draw]] · [[WebGPU]] ## 🤖 LLM 활용 **언제**: 매 같은 geometry+material의 N copies, 매 N > 50, 매 draw call 매 hot path bottleneck. **언제 X**: 매 highly varying geometry (use BatchedMesh), 매 < 50 copies (overhead > benefit), 매 fully dynamic mesh (skinning per instance은 expensive). ## ❌ 안티패턴 - **No bounding 갱신**: 매 instance 매 spread out 매 single boundingSphere가 too large → frustum culling 작동 안 함. - **매 frame full matrix rebuild**: 매 instance 매 100% update assumption 매 wrong → updateRange 활용. - **Different materials → multiple InstancedMesh**: 매 점 defeats the purpose. Use texture atlas + instance attribute. - **Skip LOD**: 매 far instance 매 close instance와 same vertex count → fillrate explosion. - **InstancedMesh on top of skinned mesh**: 매 shader 매 manual instancing 필요 — Three.js native skinning 매 instance와 conflict. ## 🧪 검증 / 중복 - Verified (Three.js docs r170+, mrdoob InstancedMesh PR, WebGPU spec). - 신뢰도 A. ## 🕓 Changelog | 날짜 | 변경 | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-10 | Manual cleanup — InstancedMesh 한계 / 해결 패턴 / BatchedMesh + WebGPU compute culling |