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.
Welcome, gà mờ. {Chào mừng, gà mờ.} For the next ten parts I’ll take you from “I paste a Three.js snippet and pray it renders” to “I can build, light, optimise, and ship real-time 3D like a senior.” {Trong mười phần tới tôi sẽ đưa bạn từ “tôi dán một đoạn Three.js rồi cầu cho nó hiện ra” lên “tôi dựng, chiếu sáng, tối ưu và ship 3D real-time như một senior.”}
Here is the first truth: Three.js is not 3D magic — it is a friendly layer over WebGL. {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 talks to your GPU in raw, verbose code (hundreds of lines for a single triangle); Three.js gives you Scene, Mesh, and Camera objects so you can think in things, not in buffers and shaders. {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.}
Play with the demo first — drag to orbit, scroll to zoom, and watch the live readout of draw calls and FPS. Then read why it works. {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.}
Open the full demo {Mở demo đầy đủ}: /tools/threejs-first-scene-demo/.
The three pillars {Ba trụ cột}
Every Three.js app, from a spinning logo to a AAA-style configurator, is built from the same three objects. {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.} Burn this trio into your memory: {Khắc bộ ba này vào đầu:}
- Scene — the container. Everything you want to draw (meshes, lights, cameras) gets added to it. {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 — the point of view. It decides where you look from and how wide the lens is. {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 — the painter. It takes the scene, looks through the camera, and draws pixels onto a
<canvas>. {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 — the smallest real scene {Setup — scene thật nhỏ nhất}
Three.js ships as ES modules. The modern way to load it in a plain HTML file is an import map pointing at a CDN — no build step needed. {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.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
// ... your scene here
</script>
Now the three pillars, in order: {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));
The four camera numbers matter. {Bốn con số của camera đều quan trọng.} fov is the vertical field of view in degrees (the “lens zoom” slider in the demo); aspect must match your canvas width/height or everything looks stretched; near and far are the clipping planes — anything closer than near or farther than far is not drawn. {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; near và far là mặt phẳng cắt — vật gần hơn near hay xa hơn far sẽ không được vẽ.}
A mesh = geometry + material {Mesh = geometry + material}
You don’t draw shapes directly — you create a mesh, which pairs a geometry (the shape’s vertices) with a material (how the surface reacts to light). {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);
Run that and you’ll see… nothing but a black silhouette. {Chạy nó lên bạn sẽ thấy… chẳng gì ngoài một bóng đen.} That’s the #1 beginner trap: MeshStandardMaterial is physically based, so with no light in the scene it is genuinely black. {Đó 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ự.} Add a light: {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
The render loop — the heartbeat {Render loop — nhịp tim}
A single renderer.render(scene, camera) draws one frame. For animation you call it every frame via requestAnimationFrame, which syncs to the display’s refresh rate (usually 60 fps). {Một lệnh renderer.render(scene, camera) vẽ một frame. Muốn animation, bạn gọi nó mỗi frame qua requestAnimationFrame, thứ đồng bộ với tần số quét màn hình (thường 60 fps).}
const clock = new THREE.Clock();
function tick() {
const dt = clock.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);
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
The detail that separates juniors from seniors: multiply movement by delta (seconds per frame), never by a fixed constant. {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.} Otherwise your cube spins twice as fast on a 120 Hz monitor as on a 60 Hz one. {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 and pixel ratio — the polish nobody teaches {Resize và pixel ratio — phần ít ai dạy}
A canvas that doesn’t resize with the window looks broken on the first drag. {Một canvas không co giãn theo cửa sổ trông như bị hỏng ngay lần kéo đầu.} Match the renderer and camera to the canvas’s real size every frame (cheap, because Three.js skips the work when nothing changed): {Đồ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
}
}
Two senior reflexes here: cap setPixelRatio at 2 (a 3× retina phone would otherwise render 9× the pixels for almost no visible gain), and always call camera.updateProjectionMatrix() after touching fov, aspect, near, or far — forgetting it is why “my FOV slider does nothing” bugs happen. {Hai phản xạ senior ở đây: chặn setPixelRatio ở 2 (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 — let the user look around {OrbitControls — để người dùng nhìn quanh}
The drag-to-orbit behaviour in the demo is one of Three.js’s add-on controls, imported from the addons path: {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
The master’s warnings {Lời cảnh báo của sư phụ}
- Black screen? You used a light-dependent material (
Standard,Phong,Lambert) with no light. Add aDirectionalLight. {Màn đen? Bạn dùng material phụ thuộc ánh sáng (Standard,Phong,Lambert) mà không có đèn. ThêmDirectionalLight.} - Stretched / squashed? Your camera
aspectdoesn’t match the canvas. Fix it inresize(). {Bị kéo dãn/bẹp?aspectcủa camera không khớp canvas. Sửa trongresize().} - Spins at different speeds on different screens? You forgot to multiply by
delta. {Xoay nhanh chậm khác nhau giữa các màn hình? Bạn quên nhân vớidelta.} - FOV/near/far change does nothing? You forgot
camera.updateProjectionMatrix(). {Đổi FOV/near/far không ăn? Bạn quêncamera.updateProjectionMatrix().}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- Make the cube a sphere {Biến cube thành sphere} — swap
BoxGeometryforSphereGeometry(1, 32, 16)and notice the smooth shading. {đổiBoxGeometrythànhSphereGeometry(1, 32, 16)và để ý đổ bóng mượt.} - Break the light {Phá đèn} — remove the
DirectionalLightand confirm the black-screen trap for yourself. {gỡDirectionalLightvà tự xác nhận cái bẫy màn đen.} - Add a second cube {Thêm cube thứ hai} at
position.set(2.5, 0, 0)and give it a different colour. {ởposition.set(2.5, 0, 0)và cho nó màu khác.}
What’s next {Phần tiếp theo}
You can now stand up a lit, interactive scene from scratch. {Giờ bạn đã dựng được một scene có ánh sáng, tương tác, từ con số 0.} But a box and a sphere won’t carry you far — real scenes mix many shapes and surfaces. {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.} In Part 2 we tour every built-in geometry and every material type — Basic, Lambert, Phong, Standard, Physical — and you’ll learn exactly when each one earns its place, with a gallery you can mix and match live. {Ở 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.}