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.
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.
Ý 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
Worldchứa cácBodycó 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 Body có khố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?
dtchưa kẹp, hoặcmaxSubStepsquá thấp so với frame rate. - Box collider bằng nửa kích thước hiển thị?
Boxcủ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
dtkhổng lồ — kẹp nó. - Mesh và body lệch nhau? Bạn quên sao cả
positionlẫnquaternionmỗi frame. - FPS chết với nhiều body? Bật
allowSleep, dùngSAPBroadphase, đơn giản hoá collider — hoặc chuyển sang Rapier.
Luyện tập, không thì coi như chưa học
- 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.
- 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.
- 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.