## Giới thiệu Three.js là thư viện JavaScript phổ biến để dựng 3D trên web bằng WebGL, giúp thu hẹp khoảng cách giữa API WebGL thấp cấp và những gì developer cần để nhanh chóng dựng một scene 3D tương tác. Bài viết này hướng dẫn bạn từ một canvas trống tới một scene 3D tối giản nhưng đầy đủ gồm camera, renderer, một object dạng hộp, ánh sáng và vòng lặp animation. Mục tiêu: bạn có thể hiểu ý nghĩa từng thành phần, triển khai code hoàn chỉnh, xử lý resize và debug các lỗi thường gặp.

1:04 ước tính · Chưa có giọng vi-VN
Three.js giúp developer dựng nội dung 3D trên web mà không phải tự triển khai toàn bộ tầng WebGL thấp cấp. Với một scene tối giản, bạn chỉ cần hiểu đúng vài thành phần cốt lõi: Scene giữ các object, Camera xác định vùng nhìn, Renderer vẽ scene lên canvas, Mesh kết hợp Geometry với Material, Light tác động lên những material có phản ứng ánh sáng và render loop cập nhật hình theo từng frame.
Bài này đi từ trang trắng tới một scene 3D đầu tiên có cube, PerspectiveCamera, WebGLRenderer, DirectionalLight, xử lý responsive và vòng lặp animation. Mục tiêu là tạo một nền tảng đủ sạch để sau đó mở rộng sang model glTF, controls, texture, shadow hoặc game loop mà không bị nhầm lẫn giữa các lớp vấn đề.

Một ứng dụng Three.js cơ bản có Renderer, Scene và Camera. Scene là root của scene graph; các Mesh, Light, Group và Object3D có thể nằm trong cây này. Mesh đại diện cho một Geometry được vẽ bằng một Material. Camera xác định phần không gian sẽ xuất hiện trong khung hình và Renderer biến kết quả thành ảnh 2D trên canvas.
Khi đọc code Three.js, hãy luôn đặt câu hỏi object đang thuộc lớp nào. Geometry chỉ chứa dữ liệu hình; Material mô tả cách bề mặt được render; Mesh kết hợp hai thứ đó và có transform; Light không phải Material; Camera không phải object hiển thị bắt buộc trong Scene nhưng vẫn có transform như Object3D. Tách đúng khái niệm giúp debug nhanh hơn nhiều.
Nếu dùng Vite hoặc bundler tương tự, cài package three và import bằng ES module. Nếu thử nhanh trực tiếp trong browser, dùng type="module" kết hợp import map hoặc CDN có khóa version. Với project thực tế, không nên trỏ tới một bản CDN không cố định phiên bản vì update bất ngờ có thể làm thay đổi API.
npm install threeHTML tối thiểu:
<!doctype html>
<html lang="vi">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Three.js First Scene</title>
<style>
html, body { margin: 0; height: 100%; }
canvas { width: 100%; height: 100%; display: block; }
</style>
</head>
<body>
<canvas id="c"></canvas>
<script type="module" src="/src/main.js"></script>
</body>
</html>Scene là container gốc. PerspectiveCamera sử dụng bốn thông số quan trọng: field of view, aspect, near và far. Renderer nhận canvas rồi render Scene từ góc nhìn Camera.
import * as THREE from 'three';
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
60,
canvas.clientWidth / canvas.clientHeight,
0.1,
100
);
camera.position.set(2, 2, 4);
camera.lookAt(0, 0, 0);
fov là góc nhìn theo chiều dọc; aspect phải phản ánh tỷ lệ hiển thị của canvas; near và far tạo hai clipping plane. Object trước near hoặc sau far sẽ không được vẽ. Camera vì thế không chỉ là vị trí người nhìn mà còn xác định một thể tích nhìn thấy.
Không nên đặt near cực nhỏ và far cực lớn chỉ để thấy mọi thứ. Khoảng depth quá rộng làm giảm độ chính xác của depth buffer và có thể sinh artefact. Hãy chọn near lớn nhất và far nhỏ nhất mà scene thực sự cần. Nếu gameplay có cả vật thể rất gần lẫn phong cảnh cực xa, nên cân nhắc giải pháp kiến trúc thay vì chỉ kéo hai giá trị này tới cực đoan.
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
color: 0x4f8cff,
roughness: 0.55,
metalness: 0.1,
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);BoxGeometry chứa dữ liệu hình học của hộp. MeshStandardMaterial mô tả bề mặt theo workflow PBR và cần ánh sáng để nhìn đúng. Mesh ghép Geometry + Material với transform của object trong scene. Từ đây bạn có thể thay BoxGeometry bằng SphereGeometry, PlaneGeometry hoặc geometry từ model loader mà phần Scene/Camera/Renderer vẫn giữ nguyên.
MeshBasicMaterial không bị ảnh hưởng bởi light, nên rất hữu ích để kiểm geometry, overlay, helper hoặc object cần màu phẳng. Ngược lại, MeshStandardMaterial, MeshPhongMaterial và các material lighting khác cần nguồn sáng phù hợp. Nếu dùng material chịu ánh sáng nhưng scene không có light, object có thể tối đến mức bạn tưởng renderer hoặc camera bị lỗi.
Khi gặp màn hình đen, một trick debug tốt là tạm thay material bằng MeshBasicMaterial. Nếu object xuất hiện, camera và geometry nhiều khả năng đang ổn; vấn đề nằm ở lighting/material. Đây là cách thu hẹp nguyên nhân thay vì chỉnh ngẫu nhiên nhiều phần cùng lúc.
const keyLight = new THREE.DirectionalLight(0xffffff, 3);
keyLight.position.set(-1, 2, 4);
scene.add(keyLight);
const fillLight = new THREE.AmbientLight(0xffffff, 0.35);
scene.add(fillLight);DirectionalLight mô phỏng các tia sáng song song và có hướng. AmbientLight bổ sung mức sáng đều giúp vùng không nhận key light không chìm hoàn toàn. Khi học, nên bắt đầu với setup đơn giản để thấy rõ material phản ứng với nguồn sáng trước khi thêm environment map, HDRI, nhiều light hoặc post-processing.
Với DirectionalLight, position xác định hướng tương đối so với target. Nếu sau này dùng shadow, light còn có shadow camera riêng và chính camera đó quyết định vùng được tạo shadow map.
function animate(time) {
const t = time * 0.001;
cube.rotation.x = t * 0.6;
cube.rotation.y = t * 0.9;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);requestAnimationFrame cung cấp timestamp và chạy callback theo nhịp render của browser. Trong mỗi frame, cập nhật state của scene trước rồi gọi renderer.render(scene, camera). Không nên tăng rotation bằng một lượng cố định phụ thuộc FPS nếu gameplay cần tốc độ nhất quán; dùng thời gian hoặc delta time để hành vi không khác quá nhiều giữa màn hình 60 Hz và 144 Hz.
import * as THREE from 'three';
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111318);
const camera = new THREE.PerspectiveCamera(60, 2, 0.1, 100);
camera.position.set(2, 2, 4);
camera.lookAt(0, 0, 0);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({
color: 0x4f8cff,
roughness: 0.55,
metalness: 0.1,
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
const keyLight = new THREE.DirectionalLight(0xffffff, 3);
keyLight.position.set(-1, 2, 4);
scene.add(keyLight);
scene.add(new THREE.AmbientLight(0xffffff, 0.35));
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const pixelRatio = window.devicePixelRatio || 1;
const maxPixelCount = 3840 * 2160;
let width = Math.floor(canvas.clientWidth * pixelRatio);
let height = Math.floor(canvas.clientHeight * pixelRatio);
const pixelCount = width * height;
const scale = pixelCount > maxPixelCount
? Math.sqrt(maxPixelCount / pixelCount)
: 1;
width = Math.floor(width * scale);
height = Math.floor(height * scale);
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) renderer.setSize(width, height, false);
return needResize;
}
function animate(time) {
const t = time * 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
cube.rotation.x = t * 0.6;
cube.rotation.y = t * 0.9;
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);Một lỗi phổ biến là CSS làm canvas rộng/cao đúng giao diện nhưng drawing buffer không được cập nhật, dẫn tới hình kéo giãn hoặc mờ. Khi kích thước hiển thị thay đổi, kiểm tra clientWidth/clientHeight, resize renderer khi cần, sau đó cập nhật camera.aspect và gọi camera.updateProjectionMatrix().
Canvas có hai lớp kích thước cần phân biệt: kích thước CSS quyết định nó chiếm bao nhiêu diện tích trên trang; kích thước drawing buffer quyết định renderer thực sự tính bao nhiêu pixel. Nếu chỉ thay CSS mà không đồng bộ camera và buffer, tỷ lệ hình hoặc độ nét có thể sai.
Màn hình Retina và smartphone có thể có devicePixelRatio cao. Nếu render theo toàn bộ mật độ pixel, GPU phải xử lý nhiều pixel hơn đáng kể. Với scene nhẹ điều này có thể chấp nhận được; với game hoặc editor nặng, việc giới hạn drawing buffer thường hợp lý hơn.
Đừng chỉ copy renderer.setPixelRatio(window.devicePixelRatio) rồi coi là tối ưu. Hãy đo FPS, GPU time và target device. Dynamic resolution hoặc cap số pixel có thể cho trải nghiệm ổn định hơn trên thiết bị yếu.

Khi object gồm nhiều bộ phận — nhân vật, xe, robot, hệ hành tinh — scene graph giúp nhóm các node. Child dùng transform tương đối so với parent, nên di chuyển parent kéo toàn bộ cụm theo. Ví dụ tay là child của thân, vũ khí là child của bàn tay; xoay thân sẽ làm cả tay và vũ khí di chuyển theo.
Không nên lạm dụng nesting quá sâu nếu cấu trúc không mang ý nghĩa. Scene graph càng phức tạp thì việc debug transform, world matrix và lifecycle càng khó. Hãy nhóm theo quan hệ chuyển động hoặc ownership thực tế.
Nếu không thấy object, kiểm tra theo thứ tự: Mesh đã scene.add() chưa; camera có nhìn đúng vị trí không; near/far có cắt object không; material có cần light không; renderer có thực sự render sau update không. Có thể thêm AxesHelper, GridHelper, CameraHelper hoặc tạm đổi sang MeshBasicMaterial để giảm số biến cần suy đoán.
Nếu canvas méo sau resize, kiểm camera aspect và projection matrix. Nếu hình mờ, kiểm drawing buffer so với kích thước CSS. Nếu GPU memory tăng dần sau khi đổi scene, kiểm lifecycle của geometry, material và texture.

Z-fighting thường xuất hiện khi hai surface gần như đồng phẳng hoặc khi khoảng near/far làm depth precision quá thấp. Hướng xử lý đầu tiên là đặt near/far hợp lý và tránh geometry trùng mặt. logarithmicDepthBuffer có thể hữu ích cho một số scene tỷ lệ cực lớn nhưng có trade-off về tương thích và hiệu năng, vì vậy không nên bật theo phản xạ.
Three.js dùng shadow map cho nhiều trường hợp. Để shadow hoạt động, bật shadow map trên renderer, bật castShadow cho light phù hợp, đặt castShadow trên object tạo bóng và receiveShadow trên bề mặt nhận bóng.
renderer.shadowMap.enabled = true;
keyLight.castShadow = true;
cube.castShadow = true;
// ground.receiveShadow = true;Với DirectionalLight, shadow camera định nghĩa khu vực được render vào shadow map. Nếu vùng quá nhỏ, object bên ngoài sẽ mất bóng; nếu vùng quá lớn, cùng một texture shadow map phải trải trên diện tích rộng hơn và chi tiết có thể giảm. Dùng CameraHelper cho shadow camera là cách trực quan để kiểm vùng này.
Shadow có chi phí đáng kể vì scene phải được render thêm từ góc nhìn của light tạo bóng. Vì vậy production thường giới hạn số light cast shadow, shadow map size và số object thực sự cần tham gia shadow pass.
Remove một Mesh khỏi Scene không có nghĩa toàn bộ GPU resource được giải phóng ngay. Geometry, Material và Texture được tái sử dụng giữa nhiều object, nên Three.js không thể tự biết lúc nào bạn thực sự không còn cần chúng. Khi lifecycle kết thúc, hãy dispose những resource mà ứng dụng sở hữu.
scene.remove(cube);
geometry.dispose();
material.dispose();Nếu material sử dụng texture, cần quản lý texture tương ứng. Với app có nhiều level hoặc viewer liên tục thay model, thiếu dispose dễ tạo memory leak và cuối cùng dẫn đến WebGL context loss.
Hai nút thắt phổ biến là số lần GPU phải draw và số pixel phải render. Hàng nghìn object rời rạc có thể tạo nhiều draw calls; màn hình HD-DPI cộng post-processing và nhiều shadow pass có thể tăng fill-rate rất mạnh. Giải pháp tùy scene: InstancedMesh, merge geometry có chủ đích, LOD, texture compression, giới hạn render resolution hoặc giảm shadow.
Không nên tối ưu bằng cảm giác. Dùng browser performance tools, renderer.info và profiler phù hợp để biết bottleneck nằm ở CPU, draw calls, shader, texture upload hay fill-rate.

Multi-view hữu ích cho editor, minimap, split screen hoặc debug camera. Bạn có thể dùng viewport/scissor để render nhiều khu vực của cùng canvas. Tuy nhiên mỗi view có thể làm tăng công việc render, nên đây là tính năng UX hoặc debug chứ không phải kỹ thuật tối ưu mặc định.
Demo ưu tiên tính rõ ràng: một Scene, một Camera, vài Mesh, một Light, render loop đơn giản. Production cần thêm asset loading, error handling, state management, resource lifecycle, performance budget và khả năng phục hồi khi tab ẩn hoặc context bị mất.
Với model 3D, glTF thường phù hợp hơn việc hard-code geometry phức tạp. Với texture, cần cân nhắc kích thước và compression. Với game, update logic và render logic nên có cấu trúc rõ thay vì dồn mọi thứ vào một function animate dài hàng trăm dòng.
Một scene Three.js đầu tiên chỉ cần vài khái niệm nhưng mỗi khái niệm phải được hiểu đúng: Scene tổ chức object, Camera giới hạn vùng nhìn, Renderer vẽ lên canvas, Mesh ghép Geometry với Material, Light quyết định cách material chịu sáng và animation loop cập nhật scene theo thời gian. Khi thêm responsive, HD-DPI, depth precision và shadow, hãy debug theo đúng lớp vấn đề.
Sau khi chạy ổn demo này, bước tiếp theo hợp lý là thêm OrbitControls, import một model glTF, quản lý asset loading, xây một lớp scene/game state và bắt đầu đo performance trên thiết bị mục tiêu.
Three.js Manual — Fundamentals: https://threejs.org/manual/en/fundamentals.html
Three.js Manual — Responsive Design: https://threejs.org/manual/en/responsive.html
Three.js Manual — Cameras: https://threejs.org/manual/en/cameras.html
Three.js Manual — Shadows: https://threejs.org/manual/en/shadows.html




Đăng ký miễn phí, lấy link riêng và giới thiệu NextGZ cho người cần học tiếng Trung hoặc Digital Art.
Cộng đồng thực chiến
Tham gia nhóm để nhận tài nguyên, cập nhật công cụ và trao đổi cách xây dựng digital business cùng AI.
Tham gia nhóm ZaloCộng đồng sáng tạo
Kết nối với cộng đồng Digital Art, chia sẻ tác phẩm và học hỏi quy trình sáng tạo mới.
Tham gia DiscordCộng đồng thực chiến
Tham gia nhóm để nhận tài nguyên, cập nhật công cụ và trao đổi cách xây dựng digital business cùng AI.
Tham gia nhóm ZaloCộng đồng sáng tạo
Kết nối với cộng đồng Digital Art, chia sẻ tác phẩm và học hỏi quy trình sáng tạo mới.
Tham gia Discord
Bình luận
0 bình luận
Đăng nhập để tham gia thảo luận cùng cộng đồng!
Đăng nhập ngayĐang tải bình luận...