Three.js – 材質與 Shader 進階
主題:ShaderMaterial 與 RawShaderMaterial
簡介
在 WebGL 與 Three.js 的開發過程中,自訂 Shader 是實現各種特殊視覺效果的關鍵。雖然 Three.js 內建的 MeshStandardMaterial、MeshPhongMaterial 等已經涵蓋了大多數常見需求,但當你需要 完全掌控渲染流程、或是想要實作獨特的光照、噪波、後期處理時,就必須直接編寫 GLSL 程式碼。
ShaderMaterial 與 RawShaderMaterial 正是為此而設計的兩種材質類型:
ShaderMaterial:在 Three.js 的渲染管線上提供一層抽象,會自動注入常用的 uniform、attribute 以及內建的變數(如modelViewMatrix、projectionMatrix)。適合想保留 Three.js 便利性,同時加入自訂片段或頂點程式碼的開發者。RawShaderMaterial:完全裸露的 GLSL 程式碼,不會自動加入任何預設變數。你必須自行處理所有矩陣、光照、骨骼等資訊。這讓程式碼更接近原生 WebGL,也更具彈性,但相對需要更多的基礎知識。
本篇文章將深入說明兩者的差異、使用時機、常見陷阱與最佳實踐,並提供多個可直接運行的範例,協助你在實務專案中快速上手。
核心概念
1. ShaderMaterial 的工作原理
ShaderMaterial 會在 Three.js 內部自動完成以下幾件事:
| 項目 | 內容 | 為何重要 |
|---|---|---|
| 內建 Uniform | modelViewMatrix、projectionMatrix、normalMatrix、cameraPosition 等 |
讓你在 GLSL 中直接使用模型、視圖、投影資訊,不必自行傳遞。 |
| 內建 Attribute | position、normal、uv、color 等 |
自動綁定幾何體的屬性,省去繁雜的 gl.bindBuffer 工作。 |
| 自動編譯 | 當材質屬性改變時,Three.js 會重新編譯 shader | 確保程式碼與參數同步,減少手動管理的錯誤。 |
支援 defines、extensions |
可在 material 設定 defines、extensions,影響編譯結果 |
方便在不同平台或需求下切換功能(例如 GL_OES_standard_derivatives)。 |
基本範例:彩色立方體
import * as THREE from 'three';
// 1. 建立場景與相機
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(2, 2, 4);
camera.lookAt(0, 0, 0);
// 2. 建立渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 3. 定義頂點與片段 shader
const vertexShader = `
varying vec3 vPosition;
void main() {
vPosition = position; // 直接使用內建 attribute
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
varying vec3 vPosition;
void main() {
// 依據模型座標的 X/Y/Z 產生漸層顏色
gl_FragColor = vec4(abs(vPosition), 1.0);
}
`;
// 4. 建立 ShaderMaterial
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
// 若需要自訂 uniform,可在此加入
// uniforms: { time: { value: 0 } }
});
// 5. 建立幾何體與 Mesh
const geometry = new THREE.BoxGeometry(1, 1, 1);
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// 6. 動畫迴圈
function animate(time) {
requestAnimationFrame(animate);
cube.rotation.x = time * 0.001;
cube.rotation.y = time * 0.0015;
renderer.render(scene, camera);
}
animate();
重點:在
ShaderMaterial中,你仍然可以使用uniforms、defines、transparent、depthWrite等常見屬性,且 Three.js 會自動把modelViewMatrix、projectionMatrix等矩陣注入 shader。
2. RawShaderMaterial:從零開始的自由度
RawShaderMaterial 會 不做任何自動注入,因此你必須自行提供所有必需的變數與程式碼。這樣的好處是:
- 完全掌控 vertex 與 fragment 的輸入/輸出,適合 全自訂渲染流程(例如 G‑Buffer、延遲渲染)。
- 可以直接使用 GLSL ES 1.0 標準,不受 Three.js 的預設限制。
- 更容易與其他 WebGL 框架或原生程式碼共用 shader。
然而,失去自動注入也意味著 必須自行傳遞矩陣,而且若忘記某個必要的 uniform,渲染結果會出現「全黑」或「錯誤的座標」等問題。
基本範例:手動傳遞矩陣的旋轉平面
import * as THREE from 'three';
// ---------------------------------------------------
// 1. 基本設定(同上)
// ---------------------------------------------------
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 10);
camera.position.set(0, 0, 2);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// ---------------------------------------------------
// 2. 自訂 Uniform(手動傳遞矩陣)
// ---------------------------------------------------
const uniforms = {
uModelViewMatrix: { value: new THREE.Matrix4() },
uProjectionMatrix: { value: new THREE.Matrix4() },
uTime: { value: 0.0 }
};
// ---------------------------------------------------
// 3. RawShaderMaterial 的 shader 程式碼
// ---------------------------------------------------
const vertexShader = `
attribute vec3 position;
uniform mat4 uModelViewMatrix;
uniform mat4 uProjectionMatrix;
void main() {
gl_Position = uProjectionMatrix * uModelViewMatrix * vec4(position, 1.0);
}
`;
const fragmentShader = `
precision mediump float;
uniform float uTime;
void main() {
// 產生隨時間變化的顏色
vec3 col = 0.5 + 0.5 * cos(uTime + vec3(0.0, 2.0, 4.0));
gl_FragColor = vec4(col, 1.0);
}
`;
const material = new THREE.RawShaderMaterial({
vertexShader,
fragmentShader,
uniforms,
// 必須自行指定 attribute 名稱,否則會找不到
// 這裡不需要額外設定,Three.js 會自動把 geometry 的 attribute 以相同名稱綁定
});
// ---------------------------------------------------
// 4. 建立幾何體(單純平面)
// ---------------------------------------------------
const geometry = new THREE.PlaneGeometry(1.5, 1.5);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// ---------------------------------------------------
// 5. 動畫迴圈:更新矩陣與時間 uniform
// ---------------------------------------------------
function animate(time) {
requestAnimationFrame(animate);
uniforms.uTime.value = time * 0.001;
// 手動計算 modelViewMatrix(等同於 camera.matrixWorldInverse * mesh.matrixWorld)
mesh.updateMatrixWorld(); // 確保 world matrix 正確
const modelView = new THREE.Matrix4()
.multiplyMatrices(camera.matrixWorldInverse, mesh.matrixWorld);
uniforms.uModelViewMatrix.value.copy(modelView);
uniforms.uProjectionMatrix.value.copy(camera.projectionMatrix);
renderer.render(scene, camera);
}
animate();
技巧:
RawShaderMaterial中的attribute必須與幾何體的屬性名稱完全相同(例如position、normal、uv),否則會在編譯時拋出錯誤。
3. 何時選擇 ShaderMaterial,何時使用 RawShaderMaterial
| 條件 | 建議使用 | 說明 |
|---|---|---|
| 需要快速開發、只改寫片段或小幅調整 | ShaderMaterial |
只要在已有的 Three.js 渲染管線上加點小技巧,省去手動傳遞矩陣的麻煩。 |
| 要實作完整的自訂光照模型、延遲渲染或 G‑Buffer | RawShaderMaterial |
必須自行控制所有輸入,避免 Three.js 預設的變數干擾。 |
| 想與其他 WebGL 程式碼共用同一套 shader | RawShaderMaterial |
完全裸露的程式碼最容易搬移。 |
需要使用 Three.js 內建的 lights、shadowMap |
ShaderMaterial(結合 lights: true) |
Three.js 會自動注入光照相關 uniform。 |
想在 shader 中直接使用 #include <...> 片段 |
ShaderMaterial |
Three.js 內建的 ShaderChunk 只能在 ShaderMaterial 中使用。 |
4. 程式碼範例彙總
以下提供 五個實用範例,涵蓋不同需求與技巧。
4.1 變色球體(使用 ShaderMaterial + uniforms)
const sphereGeo = new THREE.SphereGeometry(0.8, 64, 64);
const sphereMat = new THREE.ShaderMaterial({
uniforms: {
uColor: { value: new THREE.Color(0xff0000) },
uTime: { value: 0 }
},
vertexShader: `
varying vec3 vNormal;
void main() {
vNormal = normalMatrix * normal;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform vec3 uColor;
uniform float uTime;
varying vec3 vNormal;
void main() {
// 使用法線與時間產生漸變光澤
float intensity = dot(normalize(vNormal), vec3(0.0,0.0,1.0));
vec3 col = uColor * (0.5 + 0.5 * sin(uTime + intensity*5.0));
gl_FragColor = vec4(col, 1.0);
}
`
});
scene.add(new THREE.Mesh(sphereGeo, sphereMat));
說明:
uniforms允許在程式外部動態更新顏色與時間,適合製作動畫效果。
4.2 2D 條紋噪波(RawShaderMaterial 完全自訂)
const plane = new THREE.PlaneGeometry(2, 2);
const rawMat = new THREE.RawShaderMaterial({
uniforms: {
uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
uTime: { value: 0 }
},
vertexShader: `
attribute vec3 position;
void main() {
gl_Position = vec4(position, 1.0); // 直接使用 clip space
}
`,
fragmentShader: `
precision mediump float;
uniform vec2 uResolution;
uniform float uTime;
// Simple 2D noise based on sine waves
float noise(vec2 p) {
return sin(p.x*10.0 + uTime) * sin(p.y*10.0 + uTime);
}
void main() {
vec2 uv = gl_FragCoord.xy / uResolution;
float n = noise(uv * 5.0);
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0);
}
`
});
scene.add(new THREE.Mesh(plane, rawMat));
重點:使用
gl_FragCoord直接取得螢幕座標,避免任何矩陣運算。
4.3 透明貼圖 + Alpha 測試(ShaderMaterial + alphaTest)
const tex = new THREE.TextureLoader().load('leaf.png');
const leafMat = new THREE.ShaderMaterial({
uniforms: { map: { value: tex } },
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
`,
fragmentShader: `
uniform sampler2D map;
varying vec2 vUv;
void main() {
vec4 col = texture2D(map, vUv);
// 只保留 alpha 超過 0.2 的像素
if(col.a < 0.2) discard;
gl_FragColor = col;
}
`,
transparent: true, // 讓 Three.js 正確排序
alphaTest: 0.2 // 也可以使用 material.alphaTest
});
scene.add(new THREE.Mesh(new THREE.PlaneGeometry(1,1), leafMat));
技巧:
transparent:true+alphaTest能同時支援 半透明 與 硬剪裁,在渲染樹葉、紙張等素材時非常有用。
4.4 多光源自訂 Phong(ShaderMaterial + lights:true)
const phongMat = new THREE.ShaderMaterial({
lights: true, // 讓 Three.js 注入光照 uniform
vertexShader: `
varying vec3 vNormal;
varying vec3 vViewPosition;
void main() {
vNormal = normalize(normalMatrix * normal);
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
vViewPosition = -mvPosition.xyz;
gl_Position = projectionMatrix * mvPosition;
}
`,
fragmentShader: `
uniform vec3 diffuse;
uniform vec3 specular;
uniform float shininess;
varying vec3 vNormal;
varying vec3 vViewPosition;
// Three.js 注入的光源資訊
struct DirectionalLight {
vec3 direction;
vec3 color;
};
uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];
void main() {
vec3 normal = normalize(vNormal);
vec3 viewDir = normalize(vViewPosition);
vec3 totalDiffuse = vec3(0.0);
vec3 totalSpecular = vec3(0.0);
for(int i=0; i<NUM_DIR_LIGHTS; i++) {
vec3 lightDir = normalize(directionalLights[i].direction);
// Diffuse
float diffFactor = max(dot(normal, lightDir), 0.0);
totalDiffuse += directionalLights[i].color * diffFactor;
// Specular
vec3 reflectDir = reflect(-lightDir, normal);
float specFactor = pow(max(dot(viewDir, reflectDir), 0.0), shininess);
totalSpecular += directionalLights[i].color * specFactor;
}
vec3 finalColor = diffuse * totalDiffuse + specular * totalSpecular;
gl_FragColor = vec4(finalColor, 1.0);
}
`,
uniforms: {
diffuse: { value: new THREE.Color(0x156289) },
specular: { value: new THREE.Color(0x111111) },
shininess:{ value: 30 }
}
});
scene.add(new THREE.Mesh(new THREE.BoxGeometry(1,1,1), phongMat));
說明:只要把
lights: true設為true,Three.js 會自動把NUM_DIR_LIGHTS、directionalLights等資料注入,讓自訂 Phong 變得相當簡潔。
4.5 後處理濾鏡(RawShaderMaterial + RenderTarget)
// 1. 建立 RenderTarget
const renderTarget = new THREE.WebGLRenderTarget(window.innerWidth, window.innerHeight);
// 2. 渲染場景到 RenderTarget
renderer.setRenderTarget(renderTarget);
renderer.render(scene, camera);
renderer.setRenderTarget(null); // 回到螢幕
// 3. 使用 RawShaderMaterial 把 RenderTarget 畫面當作貼圖做後處理
const postMat = new THREE.RawShaderMaterial({
uniforms: {
uTexture: { value: renderTarget.texture },
uResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }
},
vertexShader: `
attribute vec3 position;
attribute vec2 uv;
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = vec4(position, 1.0);
}
`,
fragmentShader: `
precision mediump float;
uniform sampler2D uTexture;
uniform vec2 uResolution;
varying vec2 vUv;
void main() {
vec4 col = texture2D(uTexture, vUv);
// 簡單的亮度增強
col.rgb = pow(col.rgb, vec3(0.8));
gl_FragColor = col;
}
`
});
const quad = new THREE.Mesh(new THREE.PlaneGeometry(2, 2), postMat);
scene.add(quad);
renderer.render(scene, camera);
應用:在需要 多階段渲染(如 Bloom、HDR Tone Mapping)時,
RawShaderMaterial提供最直接的方式讀取前一階段的 framebuffer。
常見陷阱與最佳實踐
| 陷阱 | 可能的症狀 | 解決方案或最佳實踐 |
|---|---|---|
| 忘記更新 Uniform | 物件靜止、顏色不變 | 在動畫迴圈中 uniforms.xxx.value = newValue,或使用 material.needsUpdate = true(僅在改變 shader 程式碼時)。 |
| 矩陣傳遞不正確(RawShaderMaterial) | 物件出現在螢幕外、縮放異常 | 確保 modelViewMatrix = camera.matrixWorldInverse * mesh.matrixWorld,projectionMatrix 直接取自相機。 |
precision 未宣告(在 fragment shader) |
Chrome/Firefox 報錯 precision qualifier missing |
在 fragment shader 開頭加入 precision mediump float;(或 highp) |
過度使用 discard |
大量片段被剔除導致 GPU 低效 | 盡量使用 alphaTest 或 pre‑multiply alpha,只有在必要時才 discard。 |
忽略 transparent 設定 |
透明物件渲染順序錯亂 | 若需要半透明,必須把 transparent: true 設為 true,並盡量使用 深度排序(renderer.sortObjects = true)。 |
| Shader 編譯失敗訊息不清 | console 只顯示 GLSL: compilation error |
在 Chrome 開發者工具的 Sources → WebGL 中檢視完整錯誤訊息,或使用 console.error(material.program.getErrorLog())(Three.js r155+)。 |
忘記為 RawShaderMaterial 指定 attribute 名稱 |
attribute aPosition not found |
確認 attribute vec3 position; 與幾何體的 position attribute 名稱一致。 |
最佳實踐
- 先用
ShaderMaterial原型:在不確定需求時,先用ShaderMaterial撰寫,確保渲染結果正確後,再視需求遷移至RawShaderMaterial。 - 將常用程式碼抽成
ShaderChunk:即使是自訂 shader,也可利用 Three.js 的THREE.ShaderChunk共享常見函式(如packing.glsl、common.glsl),提升維護性。 - 使用
uniforms物件集中管理:將所有可變參數放在同一個uniforms物件裡,方便在動畫迴圈或 UI 控制面板中即時調整。 - 啟用
GPUProfiler:開發階段使用 Chrome 的 WebGL GPU Profiler,觀察 shader 執行時間,避免過度複雜的運算導致掉幀。 - 兼容性測試:在手機、平板與桌面瀏覽器上測試,特別是
highp可能在舊版手機上不支援,必要時降級為mediump。
實際應用場景
| 場景 | 為什麼需要自訂 Shader | 推薦使用 |
|---|---|---|
| 特殊光照模型(例如 Cartoony、Cel‑Shading) | 內建 PBR 無法直接達成卡通渲染 | ShaderMaterial(配合 lights:true) |
| GPU 粒子系統(大量點雲、煙火) | 需要在 vertex shader 中直接計算位置與大小 | RawShaderMaterial(避免額外的 uniform) |
| 後處理濾鏡(Bloom、FXAA、色彩分級) | 需要讀取前一階段 framebuffer,並在 fragment 中做運算 | RawShaderMaterial + RenderTarget |
| 多階段延遲渲染(G‑Buffer) | 必須分離 albedo、normal、depth 等資訊 | RawShaderMaterial(完整掌控輸出) |
| 動態貼圖生成(Procedural Noise、SDF) | GLSL 中即時產生圖案,避免載入大量紋理 | ShaderMaterial(若仍需要相機資訊)或 RawShaderMaterial(純 2D) |
總結
ShaderMaterial保留了 Three.js 的便利性,自動注入矩陣、光照與常用變數,適合大多數需要自訂片段或小幅改寫頂點的情境。RawShaderMaterial拋棄所有自動注入,讓開發者可以從零開始編寫 GLSL,對於 延遲渲染、後處理、GPU 粒子 等高階需求尤為重要。- 兩者的選擇並非二選一,而是 根據需求與開發階段 靈活切換:先用
ShaderMaterial快速驗證概念,必要時再切換至RawShaderMaterial取得更高的彈性與效能。
掌握這兩種材質的差異與使用技巧,將讓你在 Three.js 中的視覺表現不再受限,無論是 卡通渲染、實時噪波、還是高階的後處理管線,都能得心應手。祝你玩得開心,創造出令人驚豔的 Web 3D 作品!