本文 AI 產出,尚未審核

Three.js — 材質 (Materials)

主題:PBR 材質與 Roughness / Metalness


簡介

在 3D 網頁開發中,PBR(Physically Based Rendering) 已成為呈現真實感的標準模型。相較於傳統的 Phong 或 Lambert,PBR 能更精確地模擬光線與材質的交互作用,讓金屬、塑膠、玻璃等各種表面都能在瀏覽器裡呈現出自然的光澤與粗糙度。

Three.js 在 r106 之後提供了完整的 PBR 支援,核心材料是 MeshStandardMaterial(基於 Metal‑Roughness 工作流程)與 MeshPhysicalMaterial(加入了透明度、clearcoat 等進階參數)。本篇文章將聚焦於 RoughnessMetalness 兩大屬性,說明它們的物理意涵、使用方式,以及在實務開發中常見的陷阱與最佳實踐,幫助初學者快速上手、讓中級開發者更上一層樓。


核心概念

1. PBR 的基本原理

PBR 依據 能量守恆微表面分布(Microfacet Theory)來計算光照。主要的參數有:

參數 說明 典型取值範圍
Metalness 判斷材質是否為金屬。金屬會直接使用 Base Color 作為反射光的顏色,非金屬則使用 Diffuse(漫反射) 0.0(非金屬) ~ 1.0(純金屬)
Roughness 控制表面的微觀粗糙度,值越高表面越散射、光澤越低 0.0(光滑鏡面) ~ 1.0(完全粗糙)
Base Color 基本顏色(Albedo),對金屬與非金屬皆適用 0~1 的 RGB 向量或貼圖
Environment Map 環境光照貼圖,用於模擬間接光反射 HDR / CubeTexture

MeshStandardMaterial 預設使用 Metal‑Roughness 工作流程,只要正確設定 metalnessroughness 以及對應的貼圖,就能得到相當真實的渲染結果。

2. Metalness 的意義

  • 金屬 (Metalness = 1):光線的反射顏色直接來自 Base Color,不會有獨立的漫反射。金屬表面通常呈現高光澤、低散射。
  • 非金屬 (Metalness = 0):光線的反射顏色取自 環境貼圖,而 Base Color 只影響漫反射部分。大部分的塑膠、木材、石材屬於此類。

小技巧:若只想快速測試金屬與非金屬的差異,可以把 material.metalness = 0.01.0 交替觀察光照變化。

3. Roughness 的意義

Roughness 控制 微表面法線分布(Normal Distribution Function, NDF),決定光線在表面上的散射程度:

  • Roughness = 0.0:完美鏡面,反射光線幾乎不散射。
  • Roughness = 1.0:完全粗糙,光線被均勻散射,表面呈現漫反射。

在實務中,大多數材質的 Roughness 介於 0.2 ~ 0.8,根據材質的磨損程度、表面處理(拋光、砂紙)調整。

4. 相關貼圖

貼圖 作用 建議檔案格式
Metalness Map 用灰階值控制每個像素的金屬度 PNG、JPG(灰階)
Roughness Map 用灰階值控制每個像素的粗糙度 PNG、JPG(灰階)
Metal‑Roughness Combined Map 一張貼圖同時包含 Metal (B) 與 Roughness (G) 通道 GLTF 常用此方式
Normal Map 添加細節凹凸感,不影響金屬/粗糙度 PNG、TGA(線性)
EnvMap 環境反射貼圖,提供間接光 HDR、CubeTexture

注意:貼圖的 色彩空間 必須與材質相符;Base ColorMetalness/Roughness 貼圖通常使用 sRGB,而 NormalAOMetalnessRoughness 需使用 Linear


程式碼範例

以下範例展示從最簡單的 PBR 基礎到完整的貼圖應用,全部使用 Three.js r158+(支援 renderer.outputEncoding = THREE.sRGBEncoding)。

1️⃣ 基本 PBR 材質:金屬與非金屬對比

// 建立場景、相機與渲染器(略)
const geometry = new THREE.SphereGeometry(1, 64, 64);

// 非金屬球體
const nonMetal = new THREE.MeshStandardMaterial({
  color: 0x156289,          // 基礎顏色
  metalness: 0.0,           // 非金屬
  roughness: 0.5            // 中等粗糙度
});
const sphereNonMetal = new THREE.Mesh(geometry, nonMetal);
sphereNonMetal.position.x = -2;
scene.add(sphereNonMetal);

// 金屬球體
const metal = new THREE.MeshStandardMaterial({
  color: 0xffffff,          // 金屬顏色通常使用白色
  metalness: 1.0,           // 完全金屬
  roughness: 0.2            // 較光滑
});
const sphereMetal = new THREE.Mesh(geometry, metal);
sphereMetal.position.x = 2;
scene.add(sphereMetal);

說明:兩個球體僅差 metalnesscolor,在同一光源下即可明顯看到金屬球體的高光與環境反射差異。

2️⃣ 使用 Roughness & Metalness 貼圖(單獨貼圖)

const texLoader = new THREE.TextureLoader();

const material = new THREE.MeshStandardMaterial({
  map: texLoader.load('textures/wood_albedo.jpg'),            // 漫反射貼圖
  metalnessMap: texLoader.load('textures/metalness.png'),    // 金屬度貼圖(黑白)
  roughnessMap: texLoader.load('textures/roughness.png'),    // 粗糙度貼圖(黑白)
  normalMap: texLoader.load('textures/normal.jpg')           // 法線貼圖提升細節
});

// 確保貼圖使用正確的色彩空間
material.map.encoding = THREE.sRGBEncoding;
material.metalnessMap.encoding = THREE.LinearEncoding;
material.roughnessMap.encoding = THREE.LinearEncoding;

// 建立模型
const box = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), material);
scene.add(box);

小技巧:若貼圖是 灰階,可直接用 new THREE.TextureLoader().load(...).encoding = THREE.LinearEncoding,避免因色彩空間錯誤導致金屬度或粗糙度偏差。

3️⃣ 使用 Metal‑Roughness 合併貼圖(GLTF 常見)

// 假設貼圖的 R 通道 = Base Color, G 通道 = Roughness, B 通道 = Metalness
const mrTexture = texLoader.load('textures/metal_roughness_combined.png');
mrTexture.encoding = THREE.LinearEncoding; // 必須是線性空間

const material = new THREE.MeshStandardMaterial({
  map: texLoader.load('textures/albedo.png'), // Base Color
  metalnessMap: mrTexture,
  roughnessMap: mrTexture,
  metalness: 1.0,    // 預設為 1,實際值由貼圖 B 通道決定
  roughness: 1.0     // 預設為 1,實際值由貼圖 G 通道決定
});

// 只取 G/B 通道
material.metalnessMap.channel = 'b';   // Three.js 內建不支援,需要自行在 shader 中裁切,這裡示意
material.roughnessMap.channel = 'g';

說明:GLTF 標準將 Metalness 放在 B 通道、Roughness 放在 G 通道,減少貼圖數量。若使用 GLTFLoader,Three.js 會自動幫你解析。

4️⃣ 加入環境貼圖(HDRI)提升金屬反射

// 產生 PMREM(Prefiltered Mipmapped Radiance Environment Map)
const pmremGenerator = new THREE.PMREMGenerator(renderer);
pmremGenerator.compileEquirectangularShader();

const hdrEquirect = new THREE.RGBELoader()
  .setDataType(THREE.UnsignedByteType) // 若使用 .hdr 檔,改為 FloatType
  .load('textures/hdr/royal_esplanade_1k.hdr', texture => {
    const envMap = pmremGenerator.fromEquirectangular(texture).texture;
    scene.environment = envMap;          // 設定全局環境貼圖
    texture.dispose();
    pmremGenerator.dispose();
  });

// 金屬球體(使用環境貼圖)
const metalSphere = new THREE.MeshStandardMaterial({
  color: 0xffffff,
  metalness: 1.0,
  roughness: 0.05,
  envMapIntensity: 1.0
});
const sphere = new THREE.Mesh(new THREE.SphereGeometry(1, 64, 64), metalSphere);
scene.add(sphere);

要點:使用 PMREMGenerator 產生的環境貼圖會自動產生不同 MIP 級別,讓 roughness 變化時仍能得到正確的模糊反射。

5️⃣ 設定渲染器以支援 PBR(物理正確光照)

renderer.physicallyCorrectLights = true;   // 啟用光線衰減與能量守恆
renderer.outputEncoding = THREE.sRGBEncoding; // 螢幕輸出 sRGB
renderer.toneMapping = THREE.ACESFilmicToneMapping; // 高動態範圍
renderer.toneMappingExposure = 1.0;

說明:若未開啟 physicallyCorrectLights,光源的衰減會不符合實際物理,導致金屬反射看起來過亮或過暗。


常見陷阱與最佳實踐

陷阱 為什麼會發生 解決方案
貼圖色彩空間錯誤 metalnessMaproughnessMap 使用了預設的 sRGB 編碼 手動設定 texture.encoding = THREE.LinearEncoding
環境貼圖未經 PMREM 處理 直接使用 HDR/Equirectangular 會在不同 Roughness 下產生不自然的模糊 使用 PMREMGeneratorRGBELoader 搭配 pmremGenerator.fromEquirectangular
金屬度與粗糙度值同時為 0 會產生「無光」的材質,失去 PBR 的意義 確保 metalnessroughness 至少有一個大於 0.0
忘記開啟 renderer.physicallyCorrectLights 光源衰減不符合實際,導致亮度不均 在初始化渲染器時加入 renderer.physicallyCorrectLights = true
過度使用高解析度貼圖 記憶體與載入時間急劇上升 根據目標平台(手機、桌面)選擇適當的 MIP 級別,使用 texture.minFilter = THREE.LinearMipmapLinearFilter
Normal Map 與 Roughness 同時使用時未調整 UV 兩張貼圖的 UV 不一致會產生錯位 確認模型的 UV 展開一致,或在材質上設定 material.map.channel = 'g' 等自訂屬性(需要自訂 shader)

最佳實踐

  1. 使用線性空間貼圖:金屬與粗糙度貼圖務必設為 LinearEncoding,避免色彩空間導致的亮度偏差。
  2. 環境貼圖使用 HDR:HDRI 能提供更寬廣的亮度範圍,使金屬反射更自然。
  3. PMREM + Tone Mapping:結合 PMREMGeneratorACESFilmicToneMapping,可在不同顯示設備上保留高光細節。
  4. 分層測試:先僅使用 metalnessroughness 數值測試,再逐步加入貼圖、法線、環境貼圖,便於定位問題。
  5. 記憶體管理:載入完畢後呼叫 texture.dispose() 釋放不再使用的資源,特別是在單頁應用(SPA)裡。

實際應用場景

場景 為何使用 PBR (Metal‑Roughness) 具體實作要點
商品展示 (e‑commerce) 顧客需要看到金屬、塑膠、玻璃等材質的真實光澤與反射 使用 MeshStandardMaterial + 高品質 HDR 環境貼圖,配合 OrbitControls 讓使用者自由旋轉檢視
建築可視化 建築外牆、金屬框架、玻璃幕牆的光影變化至關重要 透過 MeshPhysicalMaterialclearcoattransmission 參數,並使用 PMREMGenerator 處理天空盒
遊戲資產 (WebGL) 需要在低至中等效能的裝置上仍保持材質真實感 Metal‑Roughness 合併貼圖 減少貼圖數量,使用 CompressedTextureLoader 壓縮貼圖
AR/VR 交互 真實感的材質提升沉浸感 結合 WebXRManager,確保渲染器的 outputEncodingtoneMapping 與頭盔的顯示特性匹配
教育教學平台 示範物理光照模型的概念 建立可調整 metalnessroughness 的 UI(如 dat.GUI),即時觀察材質變化

總結

  • PBR 透過 MetalnessRoughness 兩大參數,讓開發者能以最少的設定即得到高度真實的材質效果。
  • 正確的 貼圖色彩空間環境貼圖的 PMREM 處理、以及 renderer.physicallyCorrectLights 的開啟,是避免常見渲染問題的關鍵。
  • 在實務開發中,從簡單的數值調整到完整的貼圖流程,都建議採用 分層測試 的方式,以快速定位問題並優化效能。
  • 透過本篇提供的 範例程式碼陷阱清單最佳實踐,即使是剛接觸 Three.js 的新手,也能快速上手 PBR 材質,進一步在商品展示、建築可視化、Web 遊戲等多種場景中發揮。

掌握 Metal‑Roughness 工作流程,讓你的 Web 3D 作品在光影上不再平淡,真正達到「看得見的真實」!