本文 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
讓物件投射或接受陰影 物件的 castShadowreceiveShadow 必須正確設定。 mesh.castShadowmesh.receiveShadow
調整 Shadow Map 參數 包含解析度、相機近遠剪裁面、投影範圍等。 light.shadow.mapSizelight.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 網頁開發的路上,光影相伴,創造出令人驚豔的互動體驗!