docs(10_Wiki): Topic_Business/General/Graphic/Programming을 Topics/ 하위로 이동
최상위 10_Wiki/Topic_*였던 4개 카테고리 폴더를 10_Wiki/Topics/Topic_* 로 재배치. 콘텐츠 변경 없음(순수 폴더 이동) — Topics/ 하위 나머지 폴더는 이미 지난 커밋에서 전부 정리된 상태(잔존 항목은 에이전트 운영 상태 및 사용자가 보존을 요청한 업데이트0615/무제 3.canvas 뿐).
This commit is contained in:
+171
@@ -0,0 +1,171 @@
|
||||
---
|
||||
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 |
|
||||
Reference in New Issue
Block a user