jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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.

4 MIN READ Updated JUL 12, 2026

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.

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

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.GroupObject3D 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 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.positioncụ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ằng position, đừng nằm ngay tâm pivot.
  • Vật sai chỗ? Bạn đang đọc position cục bộ nhưng cần getWorldPosition().
  • Camera ortho trông phẳng/trống? Hộp size sai so với aspect — tính lại left/right từ width/height.

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

  1. 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.
  2. Nghiêng quỹ đạo: xoay earthPivot một chút theo x và xem đường mặt trăng nghiêng theo.
  3. 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ẻ.