--- id: wiki-2026-0508-20k-skinned-instances-demo title: 20k Skinned Instances Demo (Three.js) category: 10_Wiki/Topics status: verified canonical_id: self aliases: [InstancedMesh2 demo, large skinned mesh, three.js performance demo, GPU skinning] duplicate_of: none source_trust_level: B confidence_score: 0.85 verification_status: applied tags: [three-js, webgl, performance, skinned-mesh, gpu-skinning, lod, frustum-culling, instancing] raw_sources: [] last_reinforced: 2026-05-09 github_commit: pending tech_stack: language: TypeScript framework: Three.js / WebGL --- # 20k Skinned Instances Demo ## πŸ“Œ ν•œ 쀄 톡찰 > **20,000 character 의 same scene + 5 draw call only**. agargaro 의 InstancedMesh2 + frustum culling + LOD + animation throttle. **Mobile 도 3000+ instance 60fps**. ## πŸ“– 핡심 ### λ§€ demo 의 capability - **Desktop**: 20,000 skinned instance @ 60 FPS. - **Mobile**: 3,000 instance @ 60 FPS. - **Draw calls**: 5 only (despite 20k unit). - **GPU**: integrated 도 OK. β†’ λ§€ traditional 의 100-1000 instance 의 limit 의 break. ### λ§€ optimization technique #### 1. Frustum culling - λ§€ camera 의 outside instance 의 skip. - λ§€ bone update 의 only visible. #### 2. Distance-based animation - Near: 60 FPS bone update. - Mid: 30 FPS. - Far: 10 FPS. - Very far: 0 (static pose). β†’ λ§€ update cost 의 80% saving. #### 3. Multi-LOD - LOD 0: λ§€ detailed mesh + bone. - LOD 1: λ§€ simplified mesh. - LOD 2: λ§€ imposter (billboard). β†’ λ§€ distance 의 different polygon count. #### 4. GPU skinning - λ§€ bone matrix 의 texture μ €μž₯. - λ§€ vertex shader 의 calculate. - CPU 의 free. #### 5. Single material / atlas - λ§€ instance 의 same material. - λ§€ atlas texture (1 texture, multiple variation). #### 6. Indirect rendering - GPU 의 λ§€ instance 의 visibility κ²°μ •. - CPU β†’ GPU transfer μ΅œμ†Œν™”. ### λ§€ architecture ``` Scene └── InstancedMesh2 (1) β”œβ”€β”€ Geometry: skinned mesh (1) β”œβ”€β”€ Material: shared (1) β”œβ”€β”€ BoneTexture: λ§€ instance 의 bone matrix (RGBA float) └── Per-instance: β”œβ”€β”€ Position β”œβ”€β”€ Rotation β”œβ”€β”€ Scale β”œβ”€β”€ Color (optional) └── Animation state (frame, speed) ``` β†’ 1 InstancedMesh2 = 1 draw call. ### λ§€ use case #### Game (RTS / open world) - λ§€ 1000+ unit (StarCraft 식). - λ§€ crowd (city, stadium). #### Visualization - λ§€ large dataset (data point 의 character). - λ§€ scientific (molecule, particle). #### Simulation - λ§€ swarm. - λ§€ evacuation. - λ§€ pedestrian. #### Web 3D - λ§€ metaverse-style. - λ§€ large social space. ## πŸ’» Code ### Setup ```typescript import { InstancedMesh2 } from '@three.ez/instanced-mesh'; import * as THREE from 'three'; // Load skinned mesh const loader = new GLTFLoader(); const gltf = await loader.loadAsync('character.glb'); const skinnedMesh = gltf.scene.children[0] as THREE.SkinnedMesh; // Create InstancedMesh2 const instancedMesh = new InstancedMesh2( skinnedMesh.geometry, skinnedMesh.material, { capacity: 20000, createEntities: true, skinned: true, // GPU skinning } ); // Add instances for (let i = 0; i < 20000; i++) { instancedMesh.addInstances(1, (instance) => { instance.position.set( (Math.random() - 0.5) * 1000, 0, (Math.random() - 0.5) * 1000 ); instance.rotation.y = Math.random() * Math.PI * 2; }); } scene.add(instancedMesh); ``` ### Frustum culling + distance animation ```typescript function animate() { const cameraPos = camera.position; instancedMesh.instances.forEach((instance, i) => { const dist = instance.position.distanceTo(cameraPos); // Distance-based update rate if (dist < 50) { instance.updateAnimation(deltaTime); // 60 FPS } else if (dist < 200) { if (frame % 2 === 0) instance.updateAnimation(deltaTime * 2); // 30 FPS } else if (dist < 500) { if (frame % 6 === 0) instance.updateAnimation(deltaTime * 6); // 10 FPS } // > 500: no animation update (static pose) }); // Auto frustum culling instancedMesh.performFrustumCulling(camera); renderer.render(scene, camera); } ``` ### Multi-LOD ```typescript const lod0 = new InstancedMesh2(highPolyGeo, mat, { capacity: 5000, skinned: true }); const lod1 = new InstancedMesh2(midPolyGeo, mat, { capacity: 10000, skinned: true }); const lod2 = new InstancedMesh2(impostorGeo, mat, { capacity: 5000 }); function updateLOD(instances) { instances.forEach((inst, i) => { const d = inst.distanceToCamera(camera); if (d < 50) inst.assignTo(lod0); else if (d < 200) inst.assignTo(lod1); else inst.assignTo(lod2); }); } ``` ### GPU skinning (custom shader) ```glsl // Vertex shader attribute vec4 skinIndices; attribute vec4 skinWeights; uniform sampler2D boneTexture; // λ§€ instance 의 bone matrix uniform float boneTextureSize; mat4 getBoneMatrix(float index, float instanceIndex) { float u = (index * 4.0 + 0.5) / boneTextureSize; float v = (instanceIndex + 0.5) / boneTextureSize; return mat4( texture2D(boneTexture, vec2(u, v)), texture2D(boneTexture, vec2(u + 1.0/boneTextureSize, v)), texture2D(boneTexture, vec2(u + 2.0/boneTextureSize, v)), texture2D(boneTexture, vec2(u + 3.0/boneTextureSize, v)) ); } void main() { mat4 boneMat = getBoneMatrix(skinIndices.x, gl_InstanceID) * skinWeights.x + getBoneMatrix(skinIndices.y, gl_InstanceID) * skinWeights.y + getBoneMatrix(skinIndices.z, gl_InstanceID) * skinWeights.z + getBoneMatrix(skinIndices.w, gl_InstanceID) * skinWeights.w; vec4 transformed = boneMat * vec4(position, 1.0); gl_Position = projectionMatrix * modelViewMatrix * transformed; } ``` β†’ λ§€ vertex 의 GPU κ°€ calculate. ### Performance metric ```typescript const stats = new Stats(); document.body.appendChild(stats.dom); function animate() { stats.begin(); // ... render stats.end(); requestAnimationFrame(animate); } console.log({ draws: renderer.info.render.calls, triangles: renderer.info.render.triangles, geometries: renderer.info.memory.geometries, }); // Goal: draws < 10, FPS = 60 ``` ## πŸ€” κ²°μ • κΈ°μ€€ | Instance count | Approach | |---|---| | < 100 | Native skinned mesh (each its own) | | 100-1000 | InstancedMesh2 + frustum cull | | 1000-10000 | + LOD + distance animation | | 10000+ | + GPU skinning + impostor | | Mobile | 3000 max + heavy LOD | **κΈ°λ³Έκ°’**: InstancedMesh2 + 5 optimization (frustum, LOD, animation throttle, GPU skin, atlas). ## πŸ”— Graph - λΆ€λͺ¨: [[Three-js-Performance]] Β· [[SkinnedMesh]] Β· [[Instancing]] - λ³€ν˜•: [[BatchedMesh]] - μ‘μš©: [[Crowd-Simulation]] - 기술: [[GPU-Skinning]] Β· [[Frustum Culling]] Β· [[Level-of-Detail]] ## πŸ€– LLM ν™œμš© **μ–Έμ œ**: λ§€ large character scene 의 design. λ§€ mobile / web 3D 의 performance. **μ–Έμ œ X**: λ§€ small scene. λ§€ specific Unity / Unreal (different). ## ❌ μ•ˆν‹°νŒ¨ν„΄ - **Native skinned + 1000 instance**: 30 FPS. - **No LOD + variable distance**: GPU waste. - **Bone update 60 FPS λ§€ instance**: CPU 의 bottleneck. - **No frustum cull**: hidden update. - **Multiple material per instance**: λ§€ draw call 의 multiply. ## πŸ§ͺ 검증 / 쀑볡 - Applied (agargaro 의 demo). - 신뒰도 B (GitHub repo, real demo). - Related: [[agargaro-libraries]] Β· [[Three-js-Performance]]. ## πŸ•“ Changelog | λ‚ μ§œ | λ³€κ²½ | |---|---| | 2026-05-08 | Phase 1 | | 2026-05-09 | Manual cleanup β€” 5 optimization + Three.js code + GPU skinning shader |