259 lines
8.8 KiB
Markdown
259 lines
8.8 KiB
Markdown
---
|
||
id: wiki-2026-0508-instancedmesh-동적-버퍼-확장
|
||
title: InstancedMesh 동적 버퍼 확장
|
||
category: 10_Wiki/Topics
|
||
status: verified
|
||
canonical_id: self
|
||
aliases: [Dynamic InstancedMesh, Resize InstancedMesh, InstancedMesh Growth]
|
||
duplicate_of: none
|
||
source_trust_level: A
|
||
confidence_score: 0.9
|
||
verification_status: applied
|
||
tags: [threejs, webgl, webgpu, performance, buffer]
|
||
raw_sources: []
|
||
last_reinforced: 2026-05-10
|
||
github_commit: pending
|
||
tech_stack:
|
||
language: TypeScript
|
||
framework: Three.js r172
|
||
---
|
||
|
||
# InstancedMesh 동적 버퍼 확장
|
||
|
||
## 매 한 줄
|
||
> **"매 InstancedMesh 의 capacity 부족 시 GPU buffer 를 reallocate"**. Three.js InstancedMesh 의 `count` 는 fixed allocation. 매 instance 추가가 capacity 초과하면 새 buffer 를 만들고 기존 data 를 copy. 매 amortized O(1) 을 위한 geometric growth 가 정석.
|
||
|
||
## 매 핵심
|
||
|
||
### 매 기본 한계
|
||
- 매 `new InstancedMesh(geo, mat, count)` 는 매 `count` 만큼 GPU buffer alloc.
|
||
- 매 `mesh.count` 변경은 draw 만 줄이지 buffer 는 그대로.
|
||
- 매 capacity 초과 → 매 새 mesh / 새 InstancedBufferAttribute 가 필요.
|
||
|
||
### 매 Strategy
|
||
- **Geometric growth**: 매 capacity 부족 시 ×1.5 또는 ×2 로 resize.
|
||
- **Pool & free list**: 매 deletion 후 빈 slot 재사용 (compact 회피).
|
||
- **Chunked**: 매 fixed-size chunk N 개로 운영 (한 chunk 가득 차면 새 chunk).
|
||
|
||
### 매 응용
|
||
1. **Realtime particle spawn / despawn**.
|
||
2. **User-placed object editor** (매 추가 무한).
|
||
3. **Streaming voxel chunk** (매 LOD 별 instance 수 가변).
|
||
4. **NPC spawn system**.
|
||
|
||
## 💻 패턴
|
||
|
||
### 1. Geometric resize utility
|
||
```typescript
|
||
class DynamicInstancedMesh {
|
||
private mesh: THREE.InstancedMesh;
|
||
private capacity: number;
|
||
private size = 0;
|
||
private freeList: number[] = [];
|
||
|
||
constructor(
|
||
private geo: THREE.BufferGeometry,
|
||
private mat: THREE.Material,
|
||
initialCapacity = 64,
|
||
private maxCapacity = 1_000_000,
|
||
) {
|
||
this.capacity = initialCapacity;
|
||
this.mesh = this.makeMesh(initialCapacity);
|
||
}
|
||
|
||
private makeMesh(cap: number): THREE.InstancedMesh {
|
||
const m = new THREE.InstancedMesh(this.geo, this.mat, cap);
|
||
m.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
||
m.count = this.size;
|
||
return m;
|
||
}
|
||
|
||
add(matrix: THREE.Matrix4): number {
|
||
let idx: number;
|
||
if (this.freeList.length > 0) {
|
||
idx = this.freeList.pop()!;
|
||
} else {
|
||
if (this.size >= this.capacity) this.grow();
|
||
idx = this.size++;
|
||
}
|
||
this.mesh.setMatrixAt(idx, matrix);
|
||
this.mesh.instanceMatrix.needsUpdate = true;
|
||
this.mesh.count = this.size;
|
||
return idx;
|
||
}
|
||
|
||
remove(idx: number) {
|
||
// 매 zero-scale matrix 로 hide → free list 등록
|
||
const zero = new THREE.Matrix4().scale(new THREE.Vector3(0, 0, 0));
|
||
this.mesh.setMatrixAt(idx, zero);
|
||
this.mesh.instanceMatrix.needsUpdate = true;
|
||
this.freeList.push(idx);
|
||
}
|
||
|
||
private grow() {
|
||
const newCap = Math.min(this.capacity * 2, this.maxCapacity);
|
||
if (newCap <= this.capacity) throw new Error('Max capacity hit');
|
||
const newMesh = this.makeMesh(newCap);
|
||
|
||
// 매 기존 matrix 복사
|
||
const m = new THREE.Matrix4();
|
||
for (let i = 0; i < this.size; i++) {
|
||
this.mesh.getMatrixAt(i, m);
|
||
newMesh.setMatrixAt(i, m);
|
||
}
|
||
// 매 color 도 있으면 copy
|
||
if (this.mesh.instanceColor) {
|
||
const oldColor = this.mesh.instanceColor.array as Float32Array;
|
||
const newColor = new Float32Array(newCap * 3);
|
||
newColor.set(oldColor);
|
||
newMesh.instanceColor = new THREE.InstancedBufferAttribute(newColor, 3);
|
||
}
|
||
|
||
// 매 swap
|
||
const parent = this.mesh.parent;
|
||
if (parent) {
|
||
parent.remove(this.mesh);
|
||
parent.add(newMesh);
|
||
}
|
||
this.mesh.dispose(); // 매 매 critical: GPU buffer free
|
||
this.mesh = newMesh;
|
||
this.capacity = newCap;
|
||
console.log(`[DynamicInstancedMesh] grew → ${newCap}`);
|
||
}
|
||
|
||
get object(): THREE.InstancedMesh { return this.mesh; }
|
||
get instanceCount(): number { return this.size - this.freeList.length; }
|
||
}
|
||
```
|
||
|
||
### 2. Chunked strategy (매 large N 친화적)
|
||
```typescript
|
||
class ChunkedInstancedMesh {
|
||
private chunks: THREE.InstancedMesh[] = [];
|
||
private group = new THREE.Group();
|
||
private CHUNK_SIZE = 4096;
|
||
private currentChunkSize = 0;
|
||
|
||
constructor(private geo: THREE.BufferGeometry, private mat: THREE.Material) {
|
||
this.addChunk();
|
||
}
|
||
|
||
private addChunk() {
|
||
const m = new THREE.InstancedMesh(this.geo, this.mat, this.CHUNK_SIZE);
|
||
m.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
|
||
m.count = 0;
|
||
this.chunks.push(m);
|
||
this.group.add(m);
|
||
this.currentChunkSize = 0;
|
||
}
|
||
|
||
add(matrix: THREE.Matrix4): { chunk: number; idx: number } {
|
||
if (this.currentChunkSize >= this.CHUNK_SIZE) this.addChunk();
|
||
const chunkIdx = this.chunks.length - 1;
|
||
const chunk = this.chunks[chunkIdx];
|
||
const idx = this.currentChunkSize++;
|
||
chunk.setMatrixAt(idx, matrix);
|
||
chunk.count = idx + 1;
|
||
chunk.instanceMatrix.needsUpdate = true;
|
||
return { chunk: chunkIdx, idx };
|
||
}
|
||
|
||
get object(): THREE.Group { return this.group; }
|
||
}
|
||
```
|
||
|
||
### 3. Partial buffer update (updateRange)
|
||
```typescript
|
||
// 매 매 frame 일부만 변경 시 매 전체 upload 회피
|
||
function updateInstanceRange(
|
||
mesh: THREE.InstancedMesh, startIdx: number, count: number
|
||
) {
|
||
mesh.instanceMatrix.updateRange.offset = startIdx * 16; // mat4 = 16 floats
|
||
mesh.instanceMatrix.updateRange.count = count * 16;
|
||
mesh.instanceMatrix.needsUpdate = true;
|
||
}
|
||
```
|
||
|
||
### 4. WebGPU StorageBuffer 동적 grow
|
||
```typescript
|
||
import { storage, instanceIndex } from 'three/tsl';
|
||
|
||
class GPUDynamicInstances {
|
||
private buffer: THREE.StorageBufferAttribute;
|
||
private capacity: number;
|
||
|
||
constructor(initial = 1024) {
|
||
this.capacity = initial;
|
||
this.buffer = new THREE.StorageBufferAttribute(initial, 16);
|
||
}
|
||
|
||
grow(needed: number) {
|
||
const newCap = Math.max(this.capacity * 2, needed);
|
||
const newBuf = new THREE.StorageBufferAttribute(newCap, 16);
|
||
// 매 GPU-side copy: compute pass 로 old → new
|
||
// 매 또는 CPU readback 후 재upload (매 비싸지만 단순)
|
||
newBuf.array.set(this.buffer.array);
|
||
this.buffer = newBuf;
|
||
this.capacity = newCap;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5. Defragment (free list 가 너무 커지면)
|
||
```typescript
|
||
defragment(dim: DynamicInstancedMesh) {
|
||
// 매 freeList sort 후 살아있는 instance 를 앞으로 compact
|
||
// 매 user-facing index 가 바뀌므로 index map 도 업데이트해야 함
|
||
// ... (구현은 use case 별 — id↔index 매핑 유지가 매 critical)
|
||
}
|
||
```
|
||
|
||
### 6. dispose 와 GPU memory 관리
|
||
```typescript
|
||
function disposeInstanced(mesh: THREE.InstancedMesh) {
|
||
mesh.geometry.dispose();
|
||
if (Array.isArray(mesh.material)) mesh.material.forEach(m => m.dispose());
|
||
else mesh.material.dispose();
|
||
mesh.dispose(); // 매 InstancedMesh 자체 buffer
|
||
}
|
||
```
|
||
|
||
## 매 결정 기준
|
||
| 상황 | Approach |
|
||
|---|---|
|
||
| 매 instance 수가 천천히 변동 | Geometric grow ×2 |
|
||
| 매 instance 수 매 frame 격변 | Pre-alloc max + count 조절 |
|
||
| 매 매우 큰 N + streaming | Chunked (CHUNK_SIZE 4-16k) |
|
||
| 매 frequent delete | Free list + zero-scale hide |
|
||
| 매 free list ≥ 30% | Defragment 한 번 |
|
||
| 매 GPU-driven spawn | WebGPU StorageBuffer + compute |
|
||
|
||
**기본값**: 매 generic dynamic case → DynamicInstancedMesh (×2 grow + free list + zero-scale remove).
|
||
|
||
## 🔗 Graph
|
||
- 부모: [[InstancedMesh 최적화]] · [[Three.js]]
|
||
- 변형: [[BatchedMesh]] · [[GPU Particle Systems]]
|
||
- 응용: [[Realtime Editors]] · [[Voxel Streaming]]
|
||
- Adjacent: [[Free List]] · [[Geometric Resize]] · [[Buffer Pool]]
|
||
|
||
## 🤖 LLM 활용
|
||
**언제**: dynamic spawn/despawn 시스템 설계, free list vs chunked 의 trade-off 설명.
|
||
**언제 X**: 매 specific WebGL driver memory bug.
|
||
|
||
## ❌ 안티패턴
|
||
- **매 add 마다 new InstancedMesh**: 매 GPU alloc storm — 반드시 amortize.
|
||
- **dispose() 빠뜨림**: 매 grow 후 옛 mesh GPU buffer 의 leak.
|
||
- **Linear grow (+1, +1)**: O(N²) total copy. 매 geometric (×1.5 or ×2) 만 사용.
|
||
- **remove 후 splice**: 매 모든 후속 idx shift → 비싸다. 매 zero-scale + free list.
|
||
- **매 free list 만 쓰고 defrag 없음**: 매 hide 된 instance 도 vertex shader 실행 (zero-scale 은 깎임).
|
||
|
||
## 🧪 검증 / 중복
|
||
- Verified (Three.js r172 source `src/objects/InstancedMesh.js`).
|
||
- 신뢰도 A.
|
||
|
||
## 🕓 Changelog
|
||
| 날짜 | 변경 |
|
||
|---|---|
|
||
| 2026-05-08 | Phase 1 |
|
||
| 2026-05-10 | Manual cleanup — dynamic grow + free list + chunked |
|