Three.js 教學 – 材質(Materials)
主題:自訂 ShaderMaterial 介紹
簡介
在 Three.js 中,材質(Material) 負責決定物件表面的外觀與光照行為。雖然內建的 MeshStandardMaterial、MeshPhongMaterial 等已能滿足大多數需求,但當你想要實現獨特的視覺效果(例如噪波變形、動態光影、程式化紋理)時,內建材質往往捉襟見肘。
ShaderMaterial 正是為此而設——它允許開發者直接撰寫 GLSL 頂點與片段著色器,完全掌控渲染流程。透過自訂著色器,你可以:
- 產生與時間、使用者互動相關的動畫
- 結合多張貼圖、噪波函式或程式產生的顏色
- 實作特殊的光照模型或後期效果
本篇文章將從概念說明、實作範例到常見陷阱與最佳實踐,帶你一步步熟悉 ShaderMaterial,讓你在 Three.js 專案中自由發揮創意。
核心概念
1. ShaderMaterial 的基本結構
ShaderMaterial 需要兩段 GLSL 程式碼:
| 種類 | 功能 | 典型變數 |
|---|---|---|
| 頂點著色器 (vertexShader) | 變換頂點座標、傳遞自訂屬性 | position, modelViewMatrix, projectionMatrix, attribute |
| 片段著色器 (fragmentShader) | 計算每個像素的顏色 | gl_FragColor, varying 由頂點著色器傳來的資料 |
const material = new THREE.ShaderMaterial({
vertexShader: `...`,
fragmentShader: `...`,
uniforms: { /* uniform 變數 */ },
side: THREE.DoubleSide, // 依需求設定渲染面向
});
註:
uniform是在 CPU 與 GPU 之間傳遞的全域變數,常用於傳遞時間、貼圖、顏色等。
2. 常用內建變數與語法
| 變數 | 所屬階層 | 說明 |
|---|---|---|
position |
attribute | 原始模型頂點座標(模型空間) |
modelViewMatrix |
uniform | 模型 → 相機視圖的變換矩陣 |
projectionMatrix |
uniform | 透視或正交投影矩陣 |
gl_Position |
built‑in | 最終輸出的裁切座標(必須在頂點著色器設定) |
gl_FragColor |
built‑in | 片段著色器的最終顏色輸出(WebGL2 建議使用 out vec4) |
3. Uniform 與 Attribute 的差異
- Uniform:一次性傳入,所有頂點/片段共用。適合時間、解析度、貼圖等全局資訊。
- Attribute:每個頂點都有自己的值。Three.js 會自動將
position、normal、uv等屬性傳入頂點著色器。若需要自訂屬性,必須先在BufferGeometry中宣告,再於ShaderMaterial的attributes(已在 r152 被onBeforeCompile取代)中使用。
程式碼範例
以下示範 4 個常見且實用的 ShaderMaterial 範例,從最簡單的顏色漸層到結合噪波與時間的動畫效果。每段程式碼皆附上說明,方便你直接套用或改寫。
範例 1️⃣ 基礎顏色漸層
只使用 uv 座標在片段著色器中混合兩種顏色,適合作為測試或簡易背景。
// 建立 geometry
const geometry = new THREE.PlaneGeometry(2, 2);
// ShaderMaterial
const material = new THREE.ShaderMaterial({
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv; // 把 UV 傳給 fragment
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
uniform vec3 colorA; // 起始顏色
uniform vec3 colorB; // 結束顏色
void main() {
vec3 col = mix(colorA, colorB, vUv.y); // 依 UV.y 混色
gl_FragColor = vec4(col, 1.0);
}
`,
uniforms: {
colorA: { value: new THREE.Color(0x2194ce) },
colorB: { value: new THREE.Color(0xe91e63) },
},
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
重點:
mix()會根據vUv.y在兩個顏色之間線性插值,產生上下漸層效果。
範例 2️⃣ 動態噪波變形(頂點位移)
利用 glsl-noise 函式在頂點著色器中加入時間驅動的噪波,讓平面產生波浪般的起伏。
// 需要安裝 npm 套件:three/examples/jsm/math/ImprovedNoise.js
import { ImprovedNoise } from 'three/examples/jsm/math/ImprovedNoise.js';
const geometry = new THREE.PlaneGeometry(5, 5, 128, 128); // 高細分
const material = new THREE.ShaderMaterial({
vertexShader: `
uniform float uTime;
varying vec2 vUv;
// Simplex noise function (內嵌或 external)
// 這裡使用 three.js 內建的 ImprovedNoise 產生噪波
float noise(vec3 p) {
return fract(sin(dot(p, vec3(12.9898,78.233,45.164))) * 43758.5453);
}
void main() {
vUv = uv;
vec3 pos = position;
float n = noise(vec3(pos.x * 2.0, pos.y * 2.0, uTime));
pos.z += n * 0.5; // 依噪波高度位移
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(pos, 1.0);
}
`,
fragmentShader: `
varying vec2 vUv;
void main() {
gl_FragColor = vec4(vUv, 0.5, 1.0); // 把 UV 直接當顏色示意
}
`,
uniforms: {
uTime: { value: 0 },
},
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 動畫迴圈
function animate(time) {
material.uniforms.uTime.value = time * 0.001; // 秒為單位
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
技巧:若需要更高品質的噪波,可直接嵌入
glsl-noise的 GLSL 版,或使用three-noise套件提供的函式。
範例 3️⃣ 以貼圖作為顏色來源(Texture Uniform)
將外部圖片作為貼圖,並在片段著色器中加入簡易的亮度調整。
const texture = new THREE.TextureLoader().load('textures/brick_diffuse.jpg');
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;
texture.repeat.set(2, 2);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.ShaderMaterial({
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D uMap;
uniform float uBrightness; // 0~2
varying vec2 vUv;
void main() {
vec4 texColor = texture2D(uMap, vUv);
texColor.rgb *= uBrightness; // 調整亮度
gl_FragColor = texColor;
}
`,
uniforms: {
uMap: { value: texture },
uBrightness: { value: 1.2 },
},
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
說明:
sampler2D代表 2D 貼圖,texture2D()用於取樣。uBrightness讓你在程式內即時改變貼圖的亮度,適合製作「開關燈」或「日夜循環」的效果。
範例 4️⃣ 交互式顏色變化(使用鼠標座標)
將滑鼠在畫面上的位置傳遞給 shader,讓物體根據距離產生顏色漸層。
const geometry = new THREE.SphereGeometry(0.8, 64, 64);
const material = new THREE.ShaderMaterial({
vertexShader: `
varying vec3 vPos;
void main() {
vPos = position; // 世界座標在模型空間
gl_Position = projectionMatrix *
modelViewMatrix *
vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec2 uMouse; // 正規化座標 (0~1)
uniform vec3 uBaseColor;
varying vec3 vPos;
void main() {
// 計算螢幕空間的距離,簡化為 XY 距離
float d = distance(vPos.xy, uMouse * 2.0 - 1.0);
float factor = smoothstep(0.0, 0.5, d); // 0~0.5 內部顏色變化
vec3 col = mix(uBaseColor, vec3(1.0, 1.0, 1.0), factor);
gl_FragColor = vec4(col, 1.0);
}
`,
uniforms: {
uMouse: { value: new THREE.Vector2(0.5, 0.5) },
uBaseColor: { value: new THREE.Color(0x1565c0) },
},
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 更新滑鼠座標
window.addEventListener('pointermove', (e) => {
const x = e.clientX / window.innerWidth;
const y = 1 - e.clientY / window.innerHeight; // Y 翻轉
material.uniforms.uMouse.value.set(x, y);
});
要點:
smoothstep會讓顏色過渡更自然,避免突兀的硬邊。此範例可作為「光源追蹤」或「互動式高亮」的基礎。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| Uniform 更新過於頻繁 | 每幀大量 uniform 設定會拖慢 GPU。 |
只在需要變化時更新,或使用 THREE.Clock 計算差值。 |
忘記宣告 precision |
在 WebGL2 中若未指定 precision mediump float; 會報錯。 |
在 shader 頂部加上 precision highp float;(或 mediump)。 |
| 頂點數過高導致性能問題 | 高細分平面或球體在 CPU 端生成資料成本大。 | 盡量使用 InstancedMesh 或 LOD;只在需要變形的物件上使用高細分。 |
貼圖未設置 needsUpdate |
動態改變貼圖內容(如 render target)後未標記更新。 | texture.needsUpdate = true; |
使用 gl_FragColor 與 WebGL2 |
WebGL2 推薦使用 out vec4,舊寫法仍可用但可能警告。 |
改寫為 out vec4 fragColor; 並在 fragmentShader 最後 fragColor = ...; |
最佳實踐
- 模組化 shader:將常用函式(噪波、顏色轉換)抽成獨立字串,使用
THREE.ShaderChunk或import合併,方便維護。 - 使用
onBeforeCompile針對內建材質擴充:若只需要在MeshStandardMaterial上加一點自訂效果,直接覆寫 shader 更省事。 - 避免過度使用
varying:每個varying都會占用插值單元,過多會導致硬體限制。只傳遞必要資訊。 - 利用
THREE.MathUtils產生隨機或噪波參數:保持程式碼與 Three.js 風格一致。 - 測試不同平台:手機 GPU 對指令數量更敏感,務必在低階裝置上測試效能與兼容性。
實際應用場景
| 場景 | 為何使用 ShaderMaterial |
|---|---|
| 水面波紋 | 需要即時根據風向、時間產生波動與折射效果,傳統貼圖無法表現。 |
| 自訂光照模型 | 如 Toon 渲染、Cel‑Shade 或電影級的 Subsurface Scattering,需自行計算光照公式。 |
| 資料視覺化 | 透過顏色、形狀與動畫直接映射數據值(例如熱圖、流場),可在 fragment 中即時轉換。 |
| 互動式 UI 效果 | 按鈕懸浮、光暈、漸層過渡等 UI 元件可用 shader 渲染,提升流暢度與視覺一致性。 |
| VR/AR 內的特效 | 低延遲、單張貼圖的程式化效果(如光暈、粒子)在 VR 中尤為重要。 |
總結
ShaderMaterial 為 Three.js 提供了 無限制的渲染可能:從簡單的顏色漸層到複雜的噪波變形、貼圖混合、互動式效果,都可以在 GLSL 中自行實作。掌握以下幾點,能讓你在開發過程中事半功倍:
- 了解內建變數與 uniform/attribute 的差別,正確傳遞資料。
- 使用模組化、可重用的 shader 片段,減少重複程式碼。
- 遵循最佳實踐(精度宣告、適量 varyings、適度更新 uniform),避免常見效能瓶頸。
- 將 shader 與 Three.js 的其他功能結合(如
onBeforeCompile、InstancedMesh),打造更高效的場景。
只要多加練習、善用範例與社群資源,你就能在 Three.js 中運用 ShaderMaterial,創造出令人驚豔的 3D 互動體驗。祝你玩得開心,寫出好看的自訂材質!