jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Three.js from Zero to Senior · Part 12 — Physics with cannon-es

Make things move on their own: the render-world / physics-world split, rigid bodies and colliders, the fixed-timestep loop, syncing meshes to bodies, friction and restitution, and when to reach for Rapier — with a live physics sandbox.

4 MIN READ Updated JUL 12, 2026

Three.js là một bộ render — nó vẽ những gì bạn bảo, đúng chỗ bạn bảo. Nó không biết gì về trọng lực, va chạm, hay quán tính. Để vật rơi, nảy, và chồng lên nhau chân thực, bạn gắn thêm một engine vật lý tính chuyển động, rồi sao kết quả lên mesh mỗi frame.

Click sàn trong sandbox để thả vật — chúng va chạm, lăn, và lắng thành đống. Điều khiển trọng lực và độ nảy trực tiếp.

Mở demo toàn màn hình

Ý tưởng cốt lõi: hai thế giới, giữ đồng bộ

Đây là mô hình tư duy duy nhất khiến mọi thứ sáng tỏ: có hai thế giới song song.

  • Render world (Three.js) — Scene, Mesh, material, đèn. Cái người dùng thấy.
  • Physics world (cannon-es) — một World chứa các Body có khối lượng, vận tốc, và hình va chạm. Cái quyết định vật đi đâu.

Chúng không bao giờ chia sẻ object — chỉ chia sẻ số. Mỗi frame, physics world bước tới, rồi mỗi mesh sao vị trí và xoay của body.

Dựng physics world

import * as CANNON from 'cannon-es';

const world = new CANNON.World();
world.gravity.set(0, -9.82, 0);                 // m/s², pointing down
world.broadphase = new CANNON.SAPBroadphase(world); // faster broad-phase for many bodies
world.allowSleep = true;                        // bodies at rest stop being simulated

allowSleep là một món lợi hiệu năng miễn phí: body đã lắng sẽ ngủ và bị bỏ qua cho tới khi có gì đó đụng vào.

Body và collider

Một Bodykhối lượng (0 = tĩnh, bất động) và một hoặc nhiều hình va chạm. Quan trọng: collider là một hình riêng, đơn giản hơn so với render mesh:

// Render: a detailed Three mesh.
const mesh = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), material);
scene.add(mesh);

// Physics: a simple box collider. Note Cannon's half-extents (half the size).
const body = new CANNON.Body({ mass: 1 });
body.addShape(new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5)));
body.position.set(0, 5, 0);
world.addBody(body);

Với body động, ưu tiên collider lồi đơn giản — hộp, cầu, capsule hoặc compound — thay vì copy nguyên mesh hiển thị. Nhân vật thường là capsule; thùng có thể là box; toà nhà là compound. Trimesh vẫn hợp lý cho terrain/môi trường tĩnh khi profile và broadphase cho phép; điều cần tránh là dynamic concave collider đắt tiền theo thói quen.

Vòng lặp fixed-timestep

Vật lý ổn định hơn khi solver tiến theo bước cố định. Fixed step là điều kiện cần cho hành vi lặp lại, nhưng chưa tự tạo determinism: input order, random seed, floating-point và version engine vẫn phải được khóa.

const timer = new THREE.Timer();
timer.connect(document);
function animate(timestamp) {
  timer.update(timestamp);
  const dt = Math.min(timer.getDelta(), 1 / 30);   // policy: drop excess wall time

  world.step(1 / 60, dt, 3);
  //         ^fixed     ^elapsed ^max substeps

  // Sync: copy each body's transform onto its mesh.
  for (const { mesh, body } of objects) {
    mesh.position.copy(body.position);
    mesh.quaternion.copy(body.quaternion);   // Three & Cannon agree on quaternion layout
  }
  renderer.render(scene, camera);
}
renderer.setAnimationLoop(animate);

Ba tham số quan trọng: step(fixedTimeStep, deltaTime, maxSubSteps). Engine chạy số bước cố định 1/60 cần thiết để bù thời gian thực, giới hạn bởi maxSubSteps. Kẹp dt là một quyết định sản phẩm: nó tránh “spiral of death” nhưng chủ động bỏ thời gian, nên mô phỏng sẽ slow-motion dưới tải. Part 21 dựng accumulator, interpolation và catch-up policy đầy đủ.

Material: ma sát và độ nảy

Material vật lý không phải material render — chúng mô tả cách bề mặt tương tác:

const mat = new CANNON.Material('default');
const contact = new CANNON.ContactMaterial(mat, mat, {
  friction: 0.3,      // 0 = ice, high = grippy
  restitution: 0.3,   // 0 = no bounce, 1 = perfectly bouncy
});
world.addContactMaterial(contact);
world.defaultContactMaterial = contact;

Slider độ nảy của sandbox sửa restitution trực tiếp — kéo lên 1 và các vật biến thành bóng siêu nảy.

cannon-es và Rapier — engine nào?

  • cannon-es — JavaScript thuần, nhỏ, không cần cài đặt, hoàn hảo để học và scene nhẹ (demo này dùng). Bản fork được bảo trì của cannon.js gốc.
  • Rapier — Rust biên dịch ra WebAssembly, nhanh và ổn định hơn hẳn cho hàng nghìn body hoặc game production. Cài đặt nhỉnh hơn (nạp wasm bất đồng bộ).

Học khái niệm trên cannon-es; chuyển sang Rapier khi hiệu năng đòi hỏi. Mô hình hai-thế-giới giống nhau ở cả hai.

Lời cảnh báo của sư phụ

  • Vật lún qua sàn hay rung? dt chưa kẹp, hoặc maxSubSteps quá thấp so với frame rate.
  • Box collider bằng nửa kích thước hiển thị? Box của Cannon nhận nửa-cạnh — một nửa kích thước đầy đủ.
  • Mọi thứ bắn lên trời? Tab nền nạp một dt khổng lồ — kẹp nó.
  • Mesh và body lệch nhau? Bạn quên sao cả position lẫn quaternion mỗi frame.
  • FPS chết với nhiều body? Bật allowSleep, dùng SAPBroadphase, đơn giản hoá collider — hoặc chuyển sang Rapier.

Luyện tập, không thì coi như chưa học

  1. Quét độ nảy: trong sandbox, đặt restitution về 0 rồi 1 và thả một quả cầu từ trên cao — cảm nhận khác biệt.
  2. Không trọng lực: kéo trọng lực về 0 và mưa các vật — chúng trôi thay vì rơi.
  3. Thử tải: bấm “Rain 20” liên tục và xem số awake/sleeping — body ngủ là miễn phí.

Phần tiếp theo

Giờ bạn làm được scene có shader custom và simulation vật lý. Phần 13 chuyển những kỹ thuật rời rạc thành một product surface thật: room planner/configurator với placement, selection, persistence và lifecycle.