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.
A flat, evenly-lit scene is the unmistakable tell of a beginner. {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.} Lighting is what turns a collection of meshes into a place — it carves out form, sets mood, and tells the eye where to look. {Á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.}
Drive the rig: switch light types, swing the light around, and toggle shadows. {Điều khiển dàn đèn: đổi loại đèn, xoay đèn quanh, và bật/tắt bóng.} Watch the draw-call counter when shadows turn on. {Để ý bộ đếm draw call khi bật bóng.}
Open the full demo {Mở demo đầy đủ}: /tools/threejs-lights-shadows-demo/.
The five lights {Năm loại đèn}
Lights split into two jobs: direct lights that have a position/direction and cast shadows, and fill lights that wash the whole scene cheaply. {Đè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 — the sun {DirectionalLight — mặt trời}
Parallel rays from one direction, infinitely far away. {Tia song song từ một hướng, ở xa vô tận.} Only the direction matters, not the distance — moving it closer changes nothing but where shadows fall. {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.} This is your key light for outdoor scenes. {Đâ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 — the bulb {PointLight — bóng đèn}
Radiates in all directions from a point, with intensity falling off over distance. {Toả mọi hướng từ một điểm, cường độ giảm theo khoảng cách.} Think lightbulb, candle, fireball. {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 — the stage light {SpotLight — đèn sân khấu}
A cone from a point toward a target, with an angle, penumbra (soft edge), and falloff. {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.} Flashlight, headlight, theatre spot. {Đè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 — the fill {AmbientLight & HemisphereLight — đèn fill}
AmbientLight adds a flat, directionless base so shadows aren’t pure black. {AmbientLight thêm một nền phẳng, không hướng để bóng không đen tuyền.} HemisphereLight is smarter: a sky color from above and a ground color from below, giving cheap, natural outdoor fill. {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ẻ.} Neither casts shadows. {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
The senior recipe for most scenes: one directional key + a hemisphere or ambient fill. {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.} Start there before adding spots and points. {Bắt đầu từ đó trước khi thêm spot và point.}
Shadows — three switches, all required {Bóng đổ — ba công tắc, đều bắt buộc}
Real-time shadows in Three.js are off by default and need three things wired together — miss any one and you get no shadow with no error. {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.PCFSoftShadowMap; // 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;
This three-part dance is the most common “where are my shadows?” bug. {Điệu nhảy ba phần này là lỗi “bóng của tôi đâu?” phổ biến nhất.} The ground needs receiveShadow; the objects need castShadow; the renderer and light need their switches too. {Mặt đất cần receiveShadow; vật thể cần castShadow; renderer và đèn cũng cần bật.}
Tuning the shadow camera {Tinh chỉnh shadow camera}
A shadow is rendered from the light’s point of view into a depth texture — the shadow map. {Bóng được render từ điểm nhìn của đèn vào một texture độ sâu — shadow map.} Two knobs decide its quality: {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();
If your shadows look blocky or have a weird cut-off, the shadow camera’s frustum is wrong — either too large (low effective resolution) or too small (shadows clipped). {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).} Add a THREE.CameraHelper(sun.shadow.camera) to see the frustum while tuning. {Thêm THREE.CameraHelper(sun.shadow.camera) để nhìn thấy frustum khi tinh chỉnh.}
The cost — why seniors are stingy with shadows {Chi phí — vì sao senior keo kiệt với bóng}
Every shadow-casting light renders the scene again into its shadow map each frame. {Mỗi đèn đổ bóng render lại scene một lần nữa vào shadow map mỗi frame.} A PointLight shadow is the worst: it needs six renders (a cube map), one per direction. {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.} Watch the draw-call counter jump in the demo when you enable a point-light shadow. {Xem bộ đếm draw call nhảy trong demo khi bật bóng point-light.}
Senior tactics: {Chiến thuật senior:}
- Cast shadows from one light, not all of them. {Đổ bóng từ một đèn, không phải tất cả.}
- For static scenes, set
sun.shadow.autoUpdate = falseand update once. {Với scene tĩnh, đặtsun.shadow.autoUpdate = falsevà cập nhật một lần.} - Keep the shadow camera frustum tight and the map size as small as looks acceptable. {Giữ frustum shadow camera chật và map size nhỏ nhất mà vẫn chấp nhận được.}
- Fake contact shadows with a blurred texture on the ground when you can — far cheaper than a real shadow map. {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.}
The master’s warnings {Lời cảnh báo của sư phụ}
- No shadows? Check all three switches:
renderer.shadowMap.enabled,light.castShadow, and per-objectcastShadow/receiveShadow. {Không có bóng? Kiểm cả ba công tắc.} - SpotLight does nothing? You forgot
scene.add(spot.target). {SpotLight không ăn? Bạn quênscene.add(spot.target).} - Blocky / clipped shadows? Tune
shadow.camerafrustum and raiseshadow.mapSize. {Bóng vỡ hạt/bị cắt? Tinh chỉnh frustum và tăngshadow.mapSize.} - Frame rate tanked? You’re probably casting shadows from multiple (or point) lights. {FPS tụt? Có lẽ bạn đang đổ bóng từ nhiều đèn (hoặc point light).}
Practice, or it didn’t happen {Luyện tập, không thì coi như chưa học}
- Key + fill {Key + fill}: set up one
DirectionalLightwith shadows plus a lowHemisphereLight, and compare to ambient-only. {dựng mộtDirectionalLightcó bóng cộng mộtHemisphereLightyếu, so với chỉ ambient.} - Cost test {Thử chi phí}: switch the demo to PointLight with shadows and watch draw calls roughly multiply. {đổi demo sang PointLight có bóng và xem draw call nhân lên.}
- Tighten the frustum {Siết frustum}: in code, shrink the directional shadow camera bounds until the shadow sharpens. {trong code, thu nhỏ biên shadow camera của directional cho tới khi bóng nét hơn.}
What’s next {Phần tiếp theo}
Your scene now has form and mood. {Scene của bạn giờ có hình khối và tâm trạng.} But the surfaces are still solid colours — real materials have grain, scratches, and reflections. {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.} In Part 5 we go deep on PBR textures: UV mapping, the albedo/normal/roughness/metalness/AO maps, color management, and environment maps that let metal reflect the world — with a live material you can dress one map at a time. {Ở 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.}