jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Three.js from Zero to Senior · Part 1 — Foundations & Your First Scene

Start Three.js the right way: what WebGL really is, the three pillars (Scene, Camera, Renderer), the render loop, and your first lit, orbiting mesh — with a live scene you can drag, zoom, and inspect.

5 MIN READ Updated JUL 12, 2026

Chào mừng. Trong 25 phần, ta đi từ “tôi dán một đoạn Three.js rồi cầu cho nó hiện ra” tới chỗ thiết kế, đo và vận hành một runtime 3D production như một tech lead — bắt đầu bằng scene nhỏ nhất ở đây.

Sự thật đầu tiên: Three.js không phải phép thuật 3D — nó là một lớp thân thiện bọc quanh WebGL. WebGL nói chuyện với GPU bằng code thô và dài dòng (hàng trăm dòng chỉ để vẽ một tam giác); Three.js cho bạn các đối tượng Scene, Mesh, Camera để bạn nghĩ theo vật thể, không phải buffer và shader.

Nghịch demo trước — kéo để xoay, cuộn để zoom, và xem số liệu draw call với FPS chạy trực tiếp. Rồi mới đọc vì sao nó chạy.

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

Ba trụ cột

Mọi app Three.js, từ một logo xoay tới một configurator cỡ AAA, đều dựng từ cùng ba đối tượng. Khắc bộ ba này vào đầu:

  • Scene — cái hộp chứa. Mọi thứ bạn muốn vẽ (mesh, đèn, camera) đều được thêm vào đây.
  • Camera — điểm nhìn. Nó quyết định bạn nhìn từ đâu và ống kính rộng cỡ nào.
  • Renderer — người vẽ. Nó lấy scene, nhìn qua camera, và vẽ pixel lên một <canvas>.
   Scene  ──┐
            ├──►  Renderer.render(scene, camera)  ──►  <canvas>
   Camera ──┘

Setup — scene thật nhỏ nhất

Three.js phát hành dưới dạng ES module. Cách hiện đại để nạp nó trong file HTML thuần là dùng import map trỏ tới CDN — không cần build step.

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
  }
}
</script>
<script type="module">
  import * as THREE from 'three';
  // ... your scene here
</script>

Giờ là ba trụ cột, theo thứ tự:

// 1. Scene — the container.
const scene = new THREE.Scene();

// 2. Camera — PerspectiveCamera(fov, aspect, near, far).
const camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 100);
camera.position.set(3, 2, 5);

// 3. Renderer — paints onto a <canvas>.
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(width, height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));

Bốn con số của camera đều quan trọng. fov là góc nhìn dọc tính bằng độ (chính là slider “lens zoom” trong demo); aspect phải khớp tỉ lệ canvas nếu không mọi thứ bị méo; nearfar là mặt phẳng cắt — vật gần hơn near hay xa hơn far sẽ không được vẽ.

Mesh = geometry + material

Bạn không vẽ hình trực tiếp — bạn tạo một mesh, ghép một geometry (đỉnh của hình) với một material (cách bề mặt phản ứng với ánh sáng).

const geometry = new THREE.BoxGeometry(1.5, 1.5, 1.5);
const material = new THREE.MeshStandardMaterial({ color: 0xc8ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

Chạy nó lên bạn sẽ thấy… chẳng gì ngoài một bóng đen. Đó là cái bẫy số 1 của người mới: MeshStandardMaterial dựa trên vật lý, nên khi scene không có đèn thì nó đen thật sự. Thêm một cái đèn:

const key = new THREE.DirectionalLight(0xffffff, 2.2);
key.position.set(4, 6, 3);
scene.add(key);
scene.add(new THREE.AmbientLight(0x404040, 1.5)); // soft fill so shadows aren't pure black

Render loop — nhịp tim

Một lệnh renderer.render(scene, camera) vẽ một frame. Muốn animation, bạn đăng ký một loop đồng bộ với tần số quét màn hình. Three.js khuyến nghị setAnimationLoop() để tương thích tốt với renderer và WebXR.

const timer = new THREE.Timer();
timer.connect(document); // tránh delta khổng lồ sau khi tab bị ẩn

function tick(timestamp) {
  timer.update(timestamp);      // update đúng một lần mỗi frame
  const dt = timer.getDelta();  // seconds since last frame

  cube.rotation.y += dt * 1.0;  // multiply by dt → speed is frame-rate independent
  cube.rotation.x += dt * 0.5;

  renderer.render(scene, camera);
}
renderer.setAnimationLoop(tick);

Chi tiết phân biệt junior với senior: nhân chuyển động với delta (giây mỗi frame), đừng nhân với hằng số cố định. Nếu không, cube của bạn sẽ xoay nhanh gấp đôi trên màn 120 Hz so với 60 Hz.

Resize và pixel ratio — phần ít ai dạy

Một canvas không co giãn theo cửa sổ trông như bị hỏng ngay lần kéo đầu. Đồng bộ renderer và camera với kích thước thật của canvas mỗi frame (rẻ thôi, vì Three.js bỏ qua công việc khi không có gì đổi):

function resize() {
  const w = canvas.clientWidth;
  const h = canvas.clientHeight;
  if (canvas.width !== w || canvas.height !== h) {
    renderer.setSize(w, h, false);     // false = don't override CSS size
    camera.aspect = w / h;
    camera.updateProjectionMatrix();   // MUST call after changing camera params
  }
}

Hai phản xạ senior ở đây: chặn setPixelRatio2 (màn retina 3× nếu không sẽ render gấp 9× số pixel mà gần như không đẹp hơn), và luôn gọi camera.updateProjectionMatrix() sau khi đổi fov, aspect, near, far — quên nó chính là lý do lỗi “slider FOV của tôi không làm gì cả”.

OrbitControls — để người dùng nhìn quanh

Hành vi kéo-để-xoay trong demo là một control add-on của Three.js, import từ đường dẫn addons:

import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

const controls = new OrbitControls(camera, canvas);
controls.enableDamping = true;     // smooth, weighty feel

// inside the render loop:
controls.update();                 // required when damping is on

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

  • Màn đen? Bạn dùng material phụ thuộc ánh sáng (Standard, Phong, Lambert) mà không có đèn. Thêm DirectionalLight.
  • Bị kéo dãn/bẹp? aspect của camera không khớp canvas. Sửa trong resize().
  • Xoay nhanh chậm khác nhau giữa các màn hình? Bạn quên nhân với delta.
  • Đổi FOV/near/far không ăn? Bạn quên camera.updateProjectionMatrix().

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

  1. Biến cube thành sphere — đổi BoxGeometry thành SphereGeometry(1, 32, 16) và để ý đổ bóng mượt.
  2. Phá đèn — gỡ DirectionalLight và tự xác nhận cái bẫy màn đen.
  3. Thêm cube thứ hai ở position.set(2.5, 0, 0) và cho nó màu khác.

Phần tiếp theo

Giờ bạn đã dựng được một scene có ánh sáng, tương tác, từ con số 0. Nhưng một cái hộp với một quả cầu chưa đưa bạn đi xa — scene thật trộn nhiều hình và bề mặt. Ở Phần 2 ta dạo qua mọi geometry dựng sẵn và mọi loại material — Basic, Lambert, Phong, Standard, Physical — và bạn sẽ biết chính xác khi nào nên dùng cái nào, với một gallery cho bạn trộn thử trực tiếp.