jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Three.js from Zero to Senior · Part 8 — Performance & Instancing

Think like a profiler: why draw calls are the real budget, InstancedMesh for thousands of objects, geometry merging, frustum culling and LOD, disposing GPU memory, and reading renderer.info — with a live instanced-vs-separate benchmark.

Anyone can make a scene with ten objects run at 60 fps. {Ai cũng làm được scene mười vật thể chạy 60 fps.} A senior makes a scene with ten thousand objects run at 60 fps — and the skill that separates them is understanding where frames actually go. {Senior làm được scene mười nghìn vật thể chạy 60 fps — và kỹ năng phân biệt là hiểu frame thật sự đi đâu.}

The demo draws the same thousands of cubes two ways. {Demo vẽ cùng hàng nghìn khối theo hai cách.} Switch between InstancedMesh and separate meshes, raise the count, and watch draw calls and FPS tell two completely different stories. {Đổi giữa InstancedMesh và mesh riêng, tăng số lượng, và xem draw call với FPS kể hai câu chuyện hoàn toàn khác.}

Open the full demo {Mở demo đầy đủ}: /tools/threejs-instancing-demo/.

Draw calls — the budget that actually matters {Draw call — ngân sách thật sự quan trọng}

The instinct is to blame triangle count, but on modern GPUs the bottleneck is usually draw calls: each mesh is one command the CPU sends to the GPU (“set this geometry, this material, draw”). {Bản năng là đổ lỗi cho số tam giác, nhưng trên GPU hiện đại điểm nghẽn thường là draw call: mỗi mesh là một lệnh CPU gửi cho GPU (“đặt geometry này, material này, vẽ”).} A thousand separate meshes = a thousand commands per frame, and the CPU spends all its time talking instead of the GPU drawing. {Một nghìn mesh riêng = một nghìn lệnh mỗi frame, và CPU dành hết thời gian nói thay vì GPU vẽ.}

You can read the live count any time: {Bạn đọc được số liệu trực tiếp bất cứ lúc nào:}

console.log(renderer.info.render.calls);     // draw calls this frame
console.log(renderer.info.render.triangles);  // triangles this frame
console.log(renderer.info.memory.geometries, renderer.info.memory.textures);

renderer.info is your profiler. {renderer.info là profiler của bạn.} If calls is in the hundreds or thousands, that’s your problem — not the polygon count. {Nếu calls lên hàng trăm hay nghìn, đó là vấn đề — không phải số polygon.}

InstancedMesh — thousands of objects, one draw call {InstancedMesh — hàng nghìn vật thể, một draw call}

When you need many copies of the same geometry and material — trees, crowd, asteroids, bricks — InstancedMesh draws them all in a single call. {Khi cần nhiều bản sao của cùng geometry và material — cây, đám đông, thiên thạch, gạch — InstancedMesh vẽ tất cả trong một lần gọi.} You give it a per-instance matrix (position/rotation/scale) and optionally a per-instance color: {Bạn cấp cho nó ma trận mỗi instance (vị trí/xoay/tỉ lệ) và tuỳ chọn màu mỗi instance:}

const mesh = new THREE.InstancedMesh(geometry, material, COUNT);
const dummy = new THREE.Object3D();      // a scratch object to build matrices

for (let i = 0; i < COUNT; i++) {
  dummy.position.set(x, y, z);
  dummy.rotation.y = Math.random() * Math.PI;
  dummy.updateMatrix();                  // compose into dummy.matrix
  mesh.setMatrixAt(i, dummy.matrix);     // write the instance transform
  mesh.setColorAt(i, color.setHSL(h, 0.7, 0.55));
}
mesh.instanceMatrix.needsUpdate = true;  // upload to the GPU
scene.add(mesh);

If you animate instances every frame, set mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) so the GPU expects frequent updates. {Nếu bạn animate instance mỗi frame, đặt setUsage(THREE.DynamicDrawUsage) để GPU biết sẽ cập nhật thường xuyên.} The trade-off: every instance shares one material, so they can’t have different shaders — only different transforms and (instance) colors. {Đánh đổi: mọi instance dùng chung một material, nên không thể có shader khác nhau — chỉ khác transform và màu (instance).}

In the demo, 5,000 cubes as one InstancedMesh = 1 draw call; the same 5,000 as separate meshes = 5,000 draw calls and a CPU-bound stutter. {Trong demo, 5.000 khối là một InstancedMesh = 1 draw call; cũng 5.000 đó dạng mesh riêng = 5.000 draw call và giật do nghẽn CPU.}

Merging static geometry {Gộp geometry tĩnh}

For objects that never move and share a material but have different shapes, merge them into one geometry at build time: {Với vật thể không bao giờ di chuyển, dùng chung material nhưng khác hình, gộp chúng thành một geometry lúc dựng:}

import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
const merged = mergeGeometries([wallGeo, floorGeo, pillarGeo]);
scene.add(new THREE.Mesh(merged, sharedMaterial)); // 1 draw call for the lot

Instancing is for same shape, many transforms; merging is for different shapes, static layout. {Instancing dành cho cùng hình, nhiều transform; gộp dành cho khác hình, bố cục tĩnh.} Both collapse many draw calls into one. {Cả hai gom nhiều draw call thành một.}

Culling and level-of-detail {Culling và mức chi tiết}

Three.js already does frustum culling — objects outside the camera’s view are skipped automatically. {Three.js đã làm frustum culling — vật thể ngoài tầm nhìn camera bị bỏ qua tự động.} You can help it by keeping bounding volumes correct, and disable it (mesh.frustumCulled = false) only for things like skyboxes that should never be culled. {Bạn có thể hỗ trợ bằng cách giữ bounding volume đúng, và chỉ tắt nó cho những thứ như skybox không nên bị cull.}

For far-away objects, swap in cheaper versions with LOD: {Với vật thể ở xa, đổi sang phiên bản rẻ hơn bằng LOD:}

const lod = new THREE.LOD();
lod.addLevel(highDetailMesh, 0);   // < 15 units away
lod.addLevel(midDetailMesh, 15);
lod.addLevel(lowDetailMesh, 40);   // far away → simplest mesh
scene.add(lod);                    // Three.js picks the level by camera distance

Disposing — the leak nobody sees {Dispose — rò rỉ không ai thấy}

GPU resources (geometries, materials, textures, render targets) are not garbage-collected when you scene.remove() an object. {Tài nguyên GPU (geometry, material, texture, render target) không được thu gom rác khi bạn scene.remove() một vật thể.} You must dispose them explicitly, or VRAM creeps up until the tab crashes — especially in single-page apps that mount/unmount scenes: {Bạn phải dispose tường minh, nếu không VRAM tăng dần tới khi tab sập — nhất là trong SPA mount/unmount scene:}

function disposeMesh(mesh) {
  mesh.geometry.dispose();
  const mats = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
  for (const m of mats) {
    for (const key in m) { const v = m[key]; if (v && v.isTexture) v.dispose(); }
    m.dispose();
  }
}

Watch renderer.info.memory.geometries over time — if it only ever grows, you have a leak. {Theo dõi renderer.info.memory.geometries theo thời gian — nếu nó chỉ tăng, bạn có rò rỉ.}

A senior’s performance checklist {Checklist hiệu năng của senior}

  • Reuse geometries and materials — never create them inside a loop or per frame. {Tái dùng geometry và material — không tạo trong loop hay mỗi frame.}
  • Instance repeated objects; merge static ones. {Instance vật lặp; gộp vật tĩnh.}
  • Cap pixel ratio to ~2; resolution is the silent frame-killer on mobile. {Giới hạn pixel ratio ~2; độ phân giải là sát thủ frame thầm lặng trên mobile.}
  • Use LOD and let frustum culling work. {Dùng LOD và để frustum culling làm việc.}
  • Dispose everything you remove. {Dispose mọi thứ bạn gỡ.}
  • Profile with renderer.info before optimizing — measure, don’t guess. {Đo bằng renderer.info trước khi tối ưu — đo, đừng đoán.}

The master’s warnings {Lời cảnh báo của sư phụ}

  • Low FPS with simple geometry? It’s draw calls, not triangles — check renderer.info.render.calls. {FPS thấp với geometry đơn giản? Là draw call, không phải tam giác.}
  • Instances all stacked at the origin? You forgot instanceMatrix.needsUpdate = true. {Instance dồn hết ở gốc? Bạn quên instanceMatrix.needsUpdate = true.}
  • Memory creeps up over time? You’re removing objects without disposing their GPU resources. {Bộ nhớ tăng dần? Bạn gỡ vật thể mà không dispose tài nguyên GPU.}
  • Instancing didn’t help? Your objects had different materials — instancing needs one shared material. {Instancing không giúp? Vật thể của bạn khác material — instancing cần một material chung.}

Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}

  1. Find the cliff {Tìm vách}: in the demo, separate mode — raise the count until FPS drops below 30, note it, then switch to instanced at the same count. {chế độ separate — tăng số lượng tới khi FPS dưới 30, ghi lại, rồi đổi sang instanced cùng số lượng.}
  2. Read the profiler {Đọc profiler}: log renderer.info.render.calls in both modes and confirm 1 vs N. {log renderer.info.render.calls ở cả hai chế độ và xác nhận 1 và N.}
  3. Leak hunt {Săn rò rỉ}: rebuild the scene repeatedly without disposing and watch memory.geometries climb. {dựng lại scene liên tục mà không dispose và xem memory.geometries leo.}

What’s next {Phần tiếp theo}

Your scenes are fast and pretty — but still passive; the user can only orbit. {Scene của bạn nhanh và đẹp — nhưng vẫn thụ động; người dùng chỉ xoay được.} In Part 9 we make them interactive: the Raycaster for picking objects under the cursor, hover and click handling, drag controls, and turning a 3D scene into a real UI — with a live demo where you hover and select objects in a grid. {Ở Phần 9 ta làm chúng tương tác: Raycaster để chọn vật dưới con trỏ, xử lý hover và click, điều khiển kéo, và biến scene 3D thành UI thật — với demo cho bạn hover và chọn vật trong lưới.}