Three.js — 材質 (Materials)
主題:PBR 材質與 Roughness / Metalness
簡介
在 3D 網頁開發中,PBR(Physically Based Rendering) 已成為呈現真實感的標準模型。相較於傳統的 Phong 或 Lambert,PBR 能更精確地模擬光線與材質的交互作用,讓金屬、塑膠、玻璃等各種表面都能在瀏覽器裡呈現出自然的光澤與粗糙度。
Three.js 在 r106 之後提供了完整的 PBR 支援,核心材料是 MeshStandardMaterial(基於 Metal‑Roughness 工作流程)與 MeshPhysicalMaterial(加入了透明度、clearcoat 等進階參數)。本篇文章將聚焦於 Roughness 與 Metalness 兩大屬性,說明它們的物理意涵、使用方式,以及在實務開發中常見的陷阱與最佳實踐,幫助初學者快速上手、讓中級開發者更上一層樓。
核心概念
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 工作流程,只要正確設定 metalness、roughness 以及對應的貼圖,就能得到相當真實的渲染結果。
2. Metalness 的意義
- 金屬 (Metalness = 1):光線的反射顏色直接來自 Base Color,不會有獨立的漫反射。金屬表面通常呈現高光澤、低散射。
- 非金屬 (Metalness = 0):光線的反射顏色取自 環境貼圖,而 Base Color 只影響漫反射部分。大部分的塑膠、木材、石材屬於此類。
小技巧:若只想快速測試金屬與非金屬的差異,可以把
material.metalness = 0.0與1.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 Color與Metalness/Roughness貼圖通常使用 sRGB,而Normal、AO、Metalness、Roughness需使用 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);
說明:兩個球體僅差
metalness與color,在同一光源下即可明顯看到金屬球體的高光與環境反射差異。
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,光源的衰減會不符合實際物理,導致金屬反射看起來過亮或過暗。
常見陷阱與最佳實踐
| 陷阱 | 為什麼會發生 | 解決方案 |
|---|---|---|
| 貼圖色彩空間錯誤 | metalnessMap、roughnessMap 使用了預設的 sRGB 編碼 |
手動設定 texture.encoding = THREE.LinearEncoding |
| 環境貼圖未經 PMREM 處理 | 直接使用 HDR/Equirectangular 會在不同 Roughness 下產生不自然的模糊 | 使用 PMREMGenerator 或 RGBELoader 搭配 pmremGenerator.fromEquirectangular |
| 金屬度與粗糙度值同時為 0 | 會產生「無光」的材質,失去 PBR 的意義 | 確保 metalness 或 roughness 至少有一個大於 0.0 |
忘記開啟 renderer.physicallyCorrectLights |
光源衰減不符合實際,導致亮度不均 | 在初始化渲染器時加入 renderer.physicallyCorrectLights = true |
| 過度使用高解析度貼圖 | 記憶體與載入時間急劇上升 | 根據目標平台(手機、桌面)選擇適當的 MIP 級別,使用 texture.minFilter = THREE.LinearMipmapLinearFilter |
| Normal Map 與 Roughness 同時使用時未調整 UV | 兩張貼圖的 UV 不一致會產生錯位 | 確認模型的 UV 展開一致,或在材質上設定 material.map.channel = 'g' 等自訂屬性(需要自訂 shader) |
最佳實踐
- 使用線性空間貼圖:金屬與粗糙度貼圖務必設為
LinearEncoding,避免色彩空間導致的亮度偏差。 - 環境貼圖使用 HDR:HDRI 能提供更寬廣的亮度範圍,使金屬反射更自然。
- PMREM + Tone Mapping:結合
PMREMGenerator與ACESFilmicToneMapping,可在不同顯示設備上保留高光細節。 - 分層測試:先僅使用
metalness、roughness數值測試,再逐步加入貼圖、法線、環境貼圖,便於定位問題。 - 記憶體管理:載入完畢後呼叫
texture.dispose()釋放不再使用的資源,特別是在單頁應用(SPA)裡。
實際應用場景
| 場景 | 為何使用 PBR (Metal‑Roughness) | 具體實作要點 |
|---|---|---|
| 商品展示 (e‑commerce) | 顧客需要看到金屬、塑膠、玻璃等材質的真實光澤與反射 | 使用 MeshStandardMaterial + 高品質 HDR 環境貼圖,配合 OrbitControls 讓使用者自由旋轉檢視 |
| 建築可視化 | 建築外牆、金屬框架、玻璃幕牆的光影變化至關重要 | 透過 MeshPhysicalMaterial 加 clearcoat、transmission 參數,並使用 PMREMGenerator 處理天空盒 |
| 遊戲資產 (WebGL) | 需要在低至中等效能的裝置上仍保持材質真實感 | 以 Metal‑Roughness 合併貼圖 減少貼圖數量,使用 CompressedTextureLoader 壓縮貼圖 |
| AR/VR 交互 | 真實感的材質提升沉浸感 | 結合 WebXRManager,確保渲染器的 outputEncoding 與 toneMapping 與頭盔的顯示特性匹配 |
| 教育教學平台 | 示範物理光照模型的概念 | 建立可調整 metalness、roughness 的 UI(如 dat.GUI),即時觀察材質變化 |
總結
- PBR 透過 Metalness 與 Roughness 兩大參數,讓開發者能以最少的設定即得到高度真實的材質效果。
- 正確的 貼圖色彩空間、環境貼圖的 PMREM 處理、以及 renderer.physicallyCorrectLights 的開啟,是避免常見渲染問題的關鍵。
- 在實務開發中,從簡單的數值調整到完整的貼圖流程,都建議採用 分層測試 的方式,以快速定位問題並優化效能。
- 透過本篇提供的 範例程式碼、陷阱清單 與 最佳實踐,即使是剛接觸 Three.js 的新手,也能快速上手 PBR 材質,進一步在商品展示、建築可視化、Web 遊戲等多種場景中發揮。
掌握 Metal‑Roughness 工作流程,讓你的 Web 3D 作品在光影上不再平淡,真正達到「看得見的真實」!