Three.js from Zero to Senior · Part 3 — Transforms, Scene Graph & Cameras
Position, rotation, scale and the parent-child scene graph where children inherit transforms — plus Perspective vs Orthographic cameras and OrbitControls, with a live sun-earth-moon rig you can drive.
Tới giờ mọi vật đều ngồi một mình ở gốc toạ độ. Scene thật là các phân cấp: bàn tay theo cánh tay, cánh tay theo thân, mặt trăng theo hành tinh, hành tinh theo mặt trời. Làm chủ scene graph là bạn mở khoá rig, hệ mặt trời, và mọi bài toán “gắn cái này vào cái kia”.
Điều khiển demo: tăng Sun rotation và xem trái đất cùng mặt trăng của nó quay theo. Rồi scale mặt trời và để ý cả hệ scale theo. Sự kế thừa đó chính là toàn bộ bài học.
Transform — vị trí, xoay, tỉ lệ
Mọi Object3D (mesh, group, camera, đèn đều kế thừa từ nó) mang ba thuộc tính biến đổi.
mesh.position.set(x, y, z); // move — units are arbitrary, you decide the scale
mesh.rotation.set(rx, ry, rz); // rotate — RADIANS, not degrees
mesh.scale.set(sx, sy, sz); // resize — 1 = original, 2 = double
Hai cái bẫy ngay đây. Xoay tính bằng radian, nên một phần tư vòng là Math.PI / 2, không phải 90. Và đây là các object sống — bạn có thể chỉnh một trục mà không cần dựng lại vector:
mesh.position.x += 0.1;
mesh.rotation.y = THREE.MathUtils.degToRad(45); // convert when you think in degrees
Scene graph — con kế thừa cha
Khi bạn gọi scene.add(mesh), bạn biến mesh thành con của scene. Nhưng bất kỳ Object3D nào cũng có thể làm cha. Quy tắc vàng: transform của con được áp chồng lên transform của cha. Di chuyển cha, con đi theo. Xoay cha, con quay vòng.
Một THREE.Group là Object3D rỗng — “điểm xoay” hay “khớp rig” vô hình hoàn hảo:
const sun = new THREE.Group();
scene.add(sun);
sun.add(sunMesh);
// earthPivot is OFFSET from the sun, then added as its child.
const earthPivot = new THREE.Group();
earthPivot.position.set(3.5, 0, 0); // 3.5 units out from the sun
sun.add(earthPivot);
earthPivot.add(earthMesh);
// moonPivot is a child of earthPivot — it inherits BOTH rotations.
const moonPivot = new THREE.Group();
moonPivot.position.set(1.3, 0, 0);
earthPivot.add(moonPivot);
moonPivot.add(moonMesh);
Giờ là phép màu trong render loop:
sun.rotation.y += dt * 0.4; // earth + moon both orbit the sun
earthPivot.rotation.y += dt * 1.2; // moon orbits the earth (on top of sun's rotation)
moonPivot.rotation.y += dt * 3.0; // moon spins around its own pivot
Chỉ xoay sun thôi cũng làm trái đất và mặt trăng quay quanh nó, vì cả hai sống trong không gian toạ độ của sun. Sự kết hợp này — transform cục bộ nhân dần lên cây thành transform thế giới — chính là cách bộ xương nhân vật và cánh tay robot hoạt động.
Không gian cục bộ vs thế giới
mesh.position là cục bộ — tương đối với cha của nó. Để lấy vị trí thật trong scene (sau khi mọi xoay và scale của cha đã áp), hãy hỏi vị trí thế giới:
const worldPos = new THREE.Vector3();
moonMesh.getWorldPosition(worldPos); // the moon's true location in the scene
HUD của demo in đúng cái này — xem vị trí thế giới của mặt trăng vẽ ra một đường loằng ngoằng khi cả hai pivot xoay. Phân biệt được không gian cục bộ và thế giới là phản xạ cấp senior; lẫn lộn chúng là nguồn gốc vô số lỗi “sao vật của tôi sai chỗ”.
Hai camera — Perspective vs Orthographic
Bấm nút camera trong demo và xem phép chiếu đổi.
PerspectiveCamera mô phỏng mắt người: vật ở xa nhỏ lại, đường song song hội tụ về điểm tụ. Nó là mặc định cho game, xem sản phẩm, mọi thứ cần cảm giác thật.
new THREE.PerspectiveCamera(fov, aspect, near, far);
OrthographicCamera không có phối cảnh: vật giữ nguyên kích thước bất kể khoảng cách, đường song song vẫn song song. Đây là vẻ của phần mềm CAD, game chiến thuật isometric, và HUD 2D.
// defined by a box, not an angle: left, right, top, bottom, near, far
new THREE.OrthographicCamera(-w, w, h, -h, 0.1, 100);
Khi resize, cả hai cần cập nhật biên — và nhớ updateProjectionMatrix():
// perspective
camera.aspect = w / h;
// orthographic
camera.left = -size * (w / h); camera.right = size * (w / h);
camera.top = size; camera.bottom = -size;
camera.updateProjectionMatrix();
OrbitControls, đổi mục tiêu
Khi bạn đổi camera đang dùng, trỏ controls sang camera mới:
controls.object = camera;
controls.update();
OrbitControls xoay quanh controls.target (mặc định là gốc). Đặt target vào chủ thể để xoay giống như đang soi vật đó, không phải quay quanh khoảng trống.
Lời cảnh báo của sư phụ
- Xoay bằng độ? Không — radian. Dùng
THREE.MathUtils.degToRad(deg). - Con không chịu quay? Nó phải được
add()vào cha (group) đang xoay, và lệch bằngposition, đừng nằm ngay tâm pivot. - Vật sai chỗ? Bạn đang đọc
positioncục bộ nhưng cầngetWorldPosition(). - Camera ortho trông phẳng/trống? Hộp size sai so với aspect — tính lại
left/righttừwidth/height.
Luyện tập, không thì coi như chưa học
- Thêm hành tinh thứ hai: một pivot con khác của
sunở khoảng cách và tốc độ khác. - Nghiêng quỹ đạo: xoay
earthPivotmột chút theoxvà xem đường mặt trăng nghiêng theo. - Tách mặt trăng:
scene.add(moonMesh)thay vìmoonPivot.add(...)và xem nó ngừng đi theo — bằng chứng của kế thừa.
Phần tiếp theo
Giờ bạn dựng, biến đổi, và ngắm được một scene nhiều vật. Tuy vậy nó vẫn hơi phẳng — ánh sáng phẳng là dấu hiệu của scene người mới. Ở Phần 4 ta làm nó trông 3D thật: đèn directional, point, spot, fill kiểu ambient và hemisphere, và shadow map real-time — kèm chi phí hiệu năng của từng loại và cách senior giữ bóng đổ rẻ.