本文 AI 產出,尚未審核

Three.js 課程 – 燈光(Lights)

主題:Shadow 設定與光影最佳化


簡介

在 3D 網頁應用中,光與影是營造真實感的關鍵元素。沒有陰影的場景往往顯得平淡、缺乏層次;相反,適當的陰影不僅能突顯物件的空間位置,還能引導使用者的視線,提升互動體驗。
然而,陰影計算本身是相當耗資源的工作,若未加以優化,容易造成畫面卡頓、手機裝置過熱等問題。本文將從 Three.js 的基礎陰影概念說起,示範常見的設定方式,並提供 光影最佳化 的實務技巧,幫助你在保證畫質的同時維持流暢的效能。


核心概念

1. 陰影的基本流程

  1. 光源產生深度圖(Shadow Map)
    在渲染每一幀時,Three.js 會先以光源的視角渲染一次場景,產生一張灰階的深度圖。這張圖記錄了光線到達每個表面的最近距離。

  2. 在主相機渲染時比對深度
    當最終畫面以使用者相機渲染時,GPU 會將每個片段(fragment)的深度與對應的 Shadow Map 進行比較,若片段位於光源與深度圖之間,則被判定為「在陰影」並調整顏色。

  3. 陰影貼圖的解析度與過濾
    解析度越高、過濾方式越精細,陰影邊緣越平滑,但計算量也隨之上升。


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>。

最佳實踐總結:

  1. 先規劃光源類型:太陽光用 DirectionalLight,聚光燈用 SpotLight,點光源盡量避免陰影或降低解析度。
  2. 限制 Shadow Camera 範圍:只包住需要投射陰影的區域,減少不必要的計算。
  3. 適度降低解析度:在手機端 512×512 已足夠,桌面端可依需求提升至 2048×2048。
  4. 使用軟陰影(PCFSoft)或 Accumulative Shadow:在保持畫質的同時降低噪點。
  5. 僅在必要時更新:靜態場景關閉自動更新,大幅提升幀率。

實際應用場景

  1. 建築可視化

    • 使用 DirectionalLight 模擬太陽光,配合 時間軸 動態改變光源位置與陰影長短,提供日照分析。
    • 陰影相機範圍僅限建築周圍,解析度設定為 1024×1024,兼顧精細與效能。
  2. 遊戲中的角色光照

    • 角色使用 SpotLight 作為手電筒,陰影只在角色前方的狹小錐形區域產生,shadow.mapSize 設為 512×512,降低計算。
    • 透過 renderer.shadowMap.autoUpdate = false,只有角色移動或光源方向改變時才更新陰影。
  3. 虛擬展覽(VR)

    • 多光源環境(環境光 + 點光源),點光源關閉陰影,改用 光暈(bloom)模擬光斑。
    • 使用 AccumulativeShadowMap 以低解析度提供柔和陰影,減少 VR 裝置的渲染負擔。
  4. 產品展示(電商)

    • 高光材質的商品使用 DirectionalLight + SpotLight 產生微妙陰影,提升立體感。
    • 針對每個商品模型只渲染一次陰影,之後使用 貼圖(shadow baked)來減少即時計算。

總結

陰影是 Three.js 中提升真實感的關鍵,但同時也是效能瓶頸所在。 透過本文的 五個範例、常見陷阱 與 最佳實踐,你可以:

  • 正確啟用與設定各類光源的陰影屬性
  • 依場景需求調整 Shadow Map 解析度、相機範圍與過濾方式
  • 有效降低不必要的計算,讓手機與桌面端都能保持流暢的幀率

在實務開發中,建議先 規劃光源與陰影需求,再根據裝置性能選擇最適合的設定。掌握了這些技巧,你的 Three.js 專案將不僅視覺上更具吸引力,也能在各種平台上穩定運行。祝你在 3D 網頁開發的路上,光影相伴,創造出令人驚豔的互動體驗!