Three.js 課程 – 燈光(Lights)
主題:Shadow 設定與光影最佳化
簡介
在 3D 網頁應用中,光與影是營造真實感的關鍵元素。沒有陰影的場景往往顯得平淡、缺乏層次;相反,適當的陰影不僅能突顯物件的空間位置,還能引導使用者的視線,提升互動體驗。
然而,陰影計算本身是相當耗資源的工作,若未加以優化,容易造成畫面卡頓、手機裝置過熱等問題。本文將從 Three.js 的基礎陰影概念說起,示範常見的設定方式,並提供 光影最佳化 的實務技巧,幫助你在保證畫質的同時維持流暢的效能。
核心概念
1. 陰影的基本流程
光源產生深度圖(Shadow Map)
在渲染每一幀時,Three.js 會先以光源的視角渲染一次場景,產生一張灰階的深度圖。這張圖記錄了光線到達每個表面的最近距離。在主相機渲染時比對深度
當最終畫面以使用者相機渲染時,GPU 會將每個片段(fragment)的深度與對應的 Shadow Map 進行比較,若片段位於光源與深度圖之間,則被判定為「在陰影」並調整顏色。陰影貼圖的解析度與過濾
解析度越高、過濾方式越精細,陰影邊緣越平滑,但計算量也隨之上升。
2. 必備的陰影設定步驟
| 步驟 | 說明 | 相關屬性 |
|---|---|---|
| 啟用渲染器陰影 | renderer.shadowMap.enabled = true; |
renderer.shadowMap.type(陰影過濾類型) |
| 設定光源為投射陰影 | 只要光源支援投射陰影(大多數光源),必須把 castShadow 設為 true。 |
light.castShadow |
| 讓物件投射或接受陰影 | 物件的 castShadow、receiveShadow 必須正確設定。 |
mesh.castShadow、mesh.receiveShadow |
| 調整 Shadow Map 參數 | 包含解析度、相機近遠剪裁面、投影範圍等。 | light.shadow.mapSize、light.shadow.camera |
3. 常見光源與陰影支援
| 光源類型 | 是否支援陰影 | 陰影特性 |
|---|---|---|
| DirectionalLight | ✅ | 適合太陽光,使用 OrthographicCamera 產生平行光陰影。 |
| SpotLight | ✅ | 聚光燈,使用 PerspectiveCamera,可設定聚光角度與衰減。 |
| PointLight | ✅ | 點光源,會產生 6 張面向不同方向的 Shadow Map(Cubemap),計算最昂貴。 |
| AmbientLight | ❌ | 只提供環境光,無方向性,無陰影。 |
| HemisphereLight | ❌ | 同上,僅提供漸層環境光。 |
4. 程式碼範例
以下示範 5 個實用範例,涵蓋不同光源、陰影解析度與最佳化技巧。每段程式碼均附有說明註解,方便直接套用。
4.1 基本陰影設定(DirectionalLight)
// 1. 建立渲染器並啟用陰影
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true; // 開啟陰影功能
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 使用柔和陰影
document.body.appendChild(renderer.domElement);
// 2. 建立場景與相機
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100);
camera.position.set(5, 5, 5);
camera.lookAt(0, 0, 0);
// 3. 加入平面(接收陰影)與立方體(投射陰影)
const planeGeo = new THREE.PlaneGeometry(10, 10);
const planeMat = new THREE.MeshStandardMaterial({ color: 0x808080 });
const plane = new THREE.Mesh(planeGeo, planeMat);
plane.rotation.x = -Math.PI / 2;
plane.receiveShadow = true; // 接收陰影
scene.add(plane);
const boxGeo = new THREE.BoxGeometry(1, 1, 1);
const boxMat = new THREE.MeshStandardMaterial({ color: 0x156289 });
const box = new THREE.Mesh(boxGeo, boxMat);
box.position.y = 0.5;
box.castShadow = true; // 投射陰影
scene.add(box);
// 4. 設定 DirectionalLight 並投射陰影
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(5, 10, 5);
dirLight.castShadow = true; // 允許投射陰影
// 調整陰影相機(Orthographic)範圍,減少不必要的計算
dirLight.shadow.camera.left = -10;
dirLight.shadow.camera.right = 10;
dirLight.shadow.camera.top = 10;
dirLight.shadow.camera.bottom = -10;
dirLight.shadow.camera.near = 0.5;
dirLight.shadow.camera.far = 50;
// 陰影圖解析度(越高越清晰,但開銷大)
dirLight.shadow.mapSize.width = 1024;
dirLight.shadow.mapSize.height = 1024;
scene.add(dirLight);
// 5. 渲染迴圈
function animate() {
requestAnimationFrame(animate);
box.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
重點:
dirLight.shadow.camera的範圍要盡量與場景尺寸相匹配,避免產生過大的 Shadow Map,浪費 GPU 記憶體。
4.2 SpotLight 陰影與聚光角度
// SpotLight 只在錐形範圍內投射陰影
const spot = new THREE.SpotLight(0xffaa00, 1);
spot.position.set(0, 8, 0);
spot.angle = Math.PI / 6; // 錐形半徑(30°)
spot.penumbra = 0.2; // 銳利度 (0~1)
spot.castShadow = true;
// 調整 SpotLight 的陰影相機(Perspective)
spot.shadow.camera.near = 0.5;
spot.shadow.camera.far = 30;
spot.shadow.mapSize.set(1024, 1024);
spot.shadow.bias = -0.0001; // 防止陰影條紋(shadow acne)
scene.add(spot);
scene.add(spot.target); // 目標點,可讓光束指向任意物件
spot.target.position.set(0, 0, 0);
技巧:
shadow.bias調整陰影偏移可以減少 shadow acne(陰影條紋)問題。若出現「光斑」或「漏光」可微調此值。
4.3 PointLight 陰影的 Cubemap 優化
// PointLight 產生 6 張 Shadow Map(最耗資源),只在必要時使用
const point = new THREE.PointLight(0xffffff, 0.8, 100);
point.position.set(-5, 5, -5);
point.castShadow = true;
// 降低解析度或限制遠剪裁面以減輕負擔
point.shadow.mapSize.width = 512; // 低於 1024 可顯著提升效能
point.shadow.mapSize.height = 512;
point.shadow.camera.near = 0.5;
point.shadow.camera.far = 25; // 只在近距離投射陰影
scene.add(point);
最佳化:若場景中只有少數物件需要點光源陰影,將
point.castShadow設為false,改用 光暈(bloom)或貼圖 來模擬。
4.4 動態陰影更新(只在必要時更新)
// 預設每幀都重新渲染 Shadow Map,對於靜態光源可改為手動更新
renderer.shadowMap.autoUpdate = false; // 關閉自動更新
// 只在光源或投射物移動時呼叫
function updateShadow() {
renderer.shadowMap.needsUpdate = true;
}
// 例如:光源旋轉時
function animate() {
requestAnimationFrame(animate);
dirLight.position.x = Math.sin(Date.now() * 0.001) * 5;
dirLight.position.z = Math.cos(Date.now() * 0.001) * 5;
updateShadow(); // 只在光源位置改變時更新
renderer.render(scene, camera);
}
animate();
說明:
renderer.shadowMap.autoUpdate預設為true,每幀都重新計算陰影。對於 靜態場景 或 光源不變 的情況,關閉自動更新可大幅減少 GPU 負擔。
4.5 使用 THREE.AccumulativeShadowMap 進行多光源陰影合併(進階最佳化)
// 需要額外匯入:import { AccumulativeShadowMap } from 'three/examples/jsm/renderers/AccumulativeShadowMap.js';
const accumShadow = new THREE.AccumulativeShadowMap(renderer, {
size: 1024, // Shadow Map 大小
opacity: 0.5, // 合併後的透明度
frames: 8 // 需要累積的幀數,越大越平滑
});
scene.add(accumShadow);
// 將想要投射陰影的物件加入 accumShadow
accumShadow.addLight(dirLight);
accumShadow.addLight(spot);
// 每幀呼叫 accumShadow.render()
function animate() {
requestAnimationFrame(animate);
accumShadow.render(scene, camera);
}
animate();
優點:
AccumulativeShadowMap透過多幀累積,能在較低解析度下產生較平滑的陰影,適合 VR/AR 或 手機端 的性能受限情境。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| Shadow Acne(陰影條紋) | 陰影深度與表面深度相差過小,導致自陰影。 | 調整 light.shadow.bias、使用 NormalBias 或改變 shadow.mapSize。 |
| Peter‑Panning(漂浮陰影) | 陰影偏離物件,顯得漂浮。 | 增加 bias(正值)或使用 PCFSoftShadowMap。 |
| 過大的 Shadow Map | 解析度過高、相機範圍過大,耗費大量記憶體。 | 依場景大小調整 shadow.camera 近遠剪裁面與 mapSize。 |
| 多點光源陰影 | PointLight 產生 6 張貼圖,效能急速下降。 | 只在必要時啟用 castShadow,或改用 光暈、光照貼圖。 |
| 陰影更新頻率過高 | 每幀重新生成 Shadow Map,手機端帧率低於 30 FPS。 | 設置 renderer.shadowMap.autoUpdate = false,僅在光源或投射物變動時更新。 |
| 陰影與材質不相容 | 某些自訂 Shader 未考慮 receiveShadow。 |
在 Shader 中加入 #include <shadowmap_pars_fragment> 與 #include <shadowmap_fragment>。 |
最佳實踐總結:
- 先規劃光源類型:太陽光用
DirectionalLight,聚光燈用SpotLight,點光源盡量避免陰影或降低解析度。 - 限制 Shadow Camera 範圍:只包住需要投射陰影的區域,減少不必要的計算。
- 適度降低解析度:在手機端 512×512 已足夠,桌面端可依需求提升至 2048×2048。
- 使用軟陰影(PCFSoft)或 Accumulative Shadow:在保持畫質的同時降低噪點。
- 僅在必要時更新:靜態場景關閉自動更新,大幅提升幀率。
實際應用場景
建築可視化
- 使用
DirectionalLight模擬太陽光,配合 時間軸 動態改變光源位置與陰影長短,提供日照分析。 - 陰影相機範圍僅限建築周圍,解析度設定為 1024×1024,兼顧精細與效能。
- 使用
遊戲中的角色光照
- 角色使用
SpotLight作為手電筒,陰影只在角色前方的狹小錐形區域產生,shadow.mapSize設為 512×512,降低計算。 - 透過
renderer.shadowMap.autoUpdate = false,只有角色移動或光源方向改變時才更新陰影。
- 角色使用
虛擬展覽(VR)
- 多光源環境(環境光 + 點光源),點光源關閉陰影,改用 光暈(bloom)模擬光斑。
- 使用
AccumulativeShadowMap以低解析度提供柔和陰影,減少 VR 裝置的渲染負擔。
產品展示(電商)
- 高光材質的商品使用
DirectionalLight+SpotLight產生微妙陰影,提升立體感。 - 針對每個商品模型只渲染一次陰影,之後使用 貼圖(shadow baked)來減少即時計算。
- 高光材質的商品使用
總結
陰影是 Three.js 中提升真實感的關鍵,但同時也是效能瓶頸所在。 透過本文的 五個範例、常見陷阱 與 最佳實踐,你可以:
- 正確啟用與設定各類光源的陰影屬性
- 依場景需求調整 Shadow Map 解析度、相機範圍與過濾方式
- 有效降低不必要的計算,讓手機與桌面端都能保持流暢的幀率
在實務開發中,建議先 規劃光源與陰影需求,再根據裝置性能選擇最適合的設定。掌握了這些技巧,你的 Three.js 專案將不僅視覺上更具吸引力,也能在各種平台上穩定運行。祝你在 3D 網頁開發的路上,光影相伴,創造出令人驚豔的互動體驗!