jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Three.js from Zero to Senior · Part 4 — Lights & Shadows

Make a scene look 3D: the five light types (directional, point, spot, ambient, hemisphere), when to use each, real-time shadow maps, and the performance cost seniors watch — with a live light rig you can drive.

4 MIN READ Updated JUL 12, 2026

Một scene phẳng, sáng đều là dấu hiệu không lẫn vào đâu của người mới. Ánh sáng là thứ biến một mớ mesh thành một không gian — nó tạc ra hình khối, đặt tâm trạng, và mách mắt nên nhìn đâu.

Điều khiển dàn đèn: đổi loại đèn, xoay đèn quanh, và bật/tắt bóng. Để ý bộ đếm draw call khi bật bóng.

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

Năm loại đèn

Đèn chia làm hai việc: đèn trực tiếp có vị trí/hướng và đổ bóng, và đèn fill phủ sáng cả scene một cách rẻ.

DirectionalLight — mặt trời

Tia song song từ một hướng, ở xa vô tận. Chỉ hướng mới quan trọng, không phải khoảng cách — kéo nó lại gần chẳng đổi gì ngoài chỗ bóng rơi. Đây là đèn chính cho cảnh ngoài trời.

const sun = new THREE.DirectionalLight(0xffffff, 2.5);
sun.position.set(5, 8, 3); // direction = from this point toward (0,0,0)
scene.add(sun);

PointLight — bóng đèn

Toả mọi hướng từ một điểm, cường độ giảm theo khoảng cách. Nghĩ tới bóng đèn, ngọn nến, quả cầu lửa.

const bulb = new THREE.PointLight(0xffee88, 30, 60); // color, intensity, distance
bulb.position.set(0, 4, 0);

SpotLight — đèn sân khấu

Một hình nón từ một điểm tới target, có angle, penumbra (mép mềm), và độ tắt dần. Đèn pin, đèn pha, đèn rọi sân khấu.

const spot = new THREE.SpotLight(0xffffff, 25, 60, Math.PI / 6, 0.3);
spot.position.set(0, 8, 0);
spot.target.position.set(0, 0, 0);
scene.add(spot, spot.target); // remember to add the target!

AmbientLight & HemisphereLight — đèn fill

AmbientLight thêm một nền phẳng, không hướng để bóng không đen tuyền. HemisphereLight khôn hơn: màu trời từ trên và màu đất từ dưới, cho fill ngoài trời tự nhiên mà rẻ. Cả hai đều không đổ bóng.

scene.add(new THREE.AmbientLight(0xffffff, 0.3));
scene.add(new THREE.HemisphereLight(0x88bbff, 0x332200, 0.6)); // sky, ground, intensity

Công thức senior cho hầu hết scene: một directional làm key + một hemisphere hoặc ambient làm fill. Bắt đầu từ đó trước khi thêm spot và point.

Bóng đổ — ba công tắc, đều bắt buộc

Bóng real-time trong Three.js mặc định tắt và cần ba thứ nối với nhau — thiếu một cái là không có bóng mà cũng chẳng báo lỗi.

// 1. Tell the renderer to compute shadows.
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFShadowMap; // softer edges

// 2. Tell the light to cast.
sun.castShadow = true;

// 3. Tell objects to cast and/or receive.
mesh.castShadow = true;
ground.receiveShadow = true;

Điệu nhảy ba phần này là lỗi “bóng của tôi đâu?” phổ biến nhất. Mặt đất cần receiveShadow; vật thể cần castShadow; renderer và đèn cũng cần bật.

Tinh chỉnh shadow camera

Bóng được render từ điểm nhìn của đèn vào một texture độ sâu — shadow map. Hai núm quyết định chất lượng:

sun.shadow.mapSize.set(2048, 2048); // resolution — higher = crisper but pricier

// A DirectionalLight's shadow uses an ORTHOGRAPHIC camera. Make it tightly
// wrap your scene — too big and the shadow map resolution is wasted on empty space.
const c = sun.shadow.camera;
c.left = -10; c.right = 10; c.top = 10; c.bottom = -10;
c.near = 0.5; c.far = 30;
c.updateProjectionMatrix();

Nếu bóng trông vỡ hạt hoặc bị cắt lạ, frustum của shadow camera sai — hoặc quá to (độ phân giải hiệu dụng thấp) hoặc quá nhỏ (bóng bị cắt). Thêm THREE.CameraHelper(sun.shadow.camera) để nhìn thấy frustum khi tinh chỉnh.

Chi phí — vì sao senior keo kiệt với bóng

Mỗi đèn đổ bóng render lại scene một lần nữa vào shadow map mỗi frame. Bóng của PointLight tệ nhất: cần sáu lần render (cube map), mỗi hướng một lần. Xem bộ đếm draw call nhảy trong demo khi bật bóng point-light.

Chiến thuật senior:

  • Đổ bóng từ một đèn, không phải tất cả.
  • Với scene tĩnh, đặt sun.shadow.autoUpdate = false và cập nhật một lần.
  • Giữ frustum shadow camera chật và map size nhỏ nhất mà vẫn chấp nhận được.
  • Giả bóng tiếp xúc bằng một texture mờ trên mặt đất khi có thể — rẻ hơn nhiều so với shadow map thật.

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

  • Không có bóng? Kiểm cả ba công tắc.
  • SpotLight không ăn? Bạn quên scene.add(spot.target).
  • Bóng vỡ hạt/bị cắt? Tinh chỉnh frustum và tăng shadow.mapSize.
  • FPS tụt? Có lẽ bạn đang đổ bóng từ nhiều đèn (hoặc point light).

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

  1. Key + fill: dựng một DirectionalLight có bóng cộng một HemisphereLight yếu, so với chỉ ambient.
  2. Thử chi phí: đổi demo sang PointLight có bóng và xem draw call nhân lên.
  3. Siết frustum: trong code, thu nhỏ biên shadow camera của directional cho tới khi bóng nét hơn.

Phần tiếp theo

Scene của bạn giờ có hình khối và tâm trạng. Nhưng bề mặt vẫn là màu đặc — vật liệu thật có vân, vết xước, và phản chiếu. Ở Phần 5 ta đào sâu texture PBR: ánh xạ UV, các map albedo/normal/roughness/metalness/AO, quản lý màu, và environment map để kim loại phản chiếu thế giới — với một material cho bạn khoác từng map một.