Three.js 後製特效 – RenderPass 與 ShaderPass 完全攻略
簡介
在 3D 網頁應用中,單純的幾何渲染往往難以滿足視覺衝擊力。後製特效(Post‑Processing) 能在最終影像輸出前加入各式濾鏡、光暈、景深等效果,讓場景更具電影感。Three.js 內建的 EffectComposer 搭配 RenderPass、ShaderPass,提供一套彈性極高且易於擴充的管線(pipeline)機制。
本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者一步步掌握 RenderPass 與 ShaderPass 的使用方式,讓你能在專案中快速加入自訂的後製效果。
核心概念
1. EffectComposer ─ 後製管線的核心
EffectComposer 是一個管理多個 Pass(通道)的容器。它會先把場景渲染到一個 RenderTarget(離屏緩衝區),再依序將每個 Pass 的輸出作為下一個 Pass 的輸入,最終把結果呈現在螢幕上。
import * as THREE from 'three';
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 建立 composer
const composer = new EffectComposer(renderer);
2. RenderPass ─ 基本的渲染階段
RenderPass 的工作非常直接:把 相機、場景 渲染到 EffectComposer 的第一個 RenderTarget。它是所有後製效果的起點,沒有它,後面的 Pass 就找不到來源影像。
// scene、camera 已在前面建立
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
Tip:若想在渲染前臨時隱藏某些物件,只要在加入
RenderPass前把它們的visible設為false,渲染完畢後再恢復即可。
3. ShaderPass ─ 自訂 GLSL 濾鏡
ShaderPass 允許你把任何符合 Three.js ShaderMaterial 結構的 GLSL 程式碼,當作一個 Pass 使用。它接收兩個主要屬性:
| 屬性 | 說明 |
|---|---|
uniforms |
供外部程式注入的 uniform 變數(如時間、解析度) |
vertexShader / fragmentShader |
完整的 GLSL 程式碼,最常只需要自訂 fragmentShader,vertexShader 多半使用預設的全螢幕四邊形頂點著色器 |
// 範例:簡易的反相濾鏡
const InvertShader = {
uniforms: {
tDiffuse: { value: null } // 由 composer 自動傳入當前影像
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
varying vec2 vUv;
void main() {
vec4 color = texture2D(tDiffuse, vUv);
gl_FragColor = vec4(1.0 - color.rgb, color.a);
}
`
};
const invertPass = new ShaderPass(InvertShader);
composer.addPass(invertPass);
4. Pass 的執行順序
EffectComposer 會 依加入的順序 依次執行 Pass。若想在某個 Pass 結束後直接輸出畫面(例如 debug),可把該 Pass 的 renderToScreen 設為 true,之後的 Pass 會自動被略過。
// 只顯示反相結果,不再執行後續 Pass
invertPass.renderToScreen = true;
5. 多 Pass 組合 – 常見範例
| 組合 | 目的 |
|---|---|
RenderPass → BloomPass → ShaderPass (色彩分級) |
先渲染場景、再加上光暈、最後調整色調 |
RenderPass → SMAAPass → FilmPass |
抗鋸齒 + 老電影顆粒感 |
RenderPass → DepthPass → SSAOPass |
產生深度圖後套用環境遮蔽(SSAO) |
程式碼範例
以下提供 5 個實用範例,涵蓋從最基礎到稍微進階的應用。所有範例均使用 EffectComposer、RenderPass、ShaderPass,並附上說明。
範例 1:基本的 RenderPass + 反相 ShaderPass
// 基礎設定 (scene、camera、renderer 已建立)
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
// 反相濾鏡 (見上文 InvertShader)
const invertPass = new ShaderPass(InvertShader);
composer.addPass(invertPass);
// 動畫迴圈
function animate() {
requestAnimationFrame(animate);
// 只需要呼叫 composer,而不是 renderer
composer.render();
}
animate();
說明:這是最簡單的後製管線,展示了如何把渲染結果送入自訂 Shader。
範例 2:加入時間 Uniform 的波浪變形
const WaveShader = {
uniforms: {
tDiffuse: { value: null },
time: { value: 0.0 },
amplitude: { value: 0.05 } // 變形幅度
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform sampler2D tDiffuse;
uniform float time;
uniform float amplitude;
varying vec2 vUv;
void main() {
// 以正弦波扭曲 UV
vec2 uv = vUv;
uv.y += sin(uv.x * 10.0 + time) * amplitude;
vec4 color = texture2D(tDiffuse, uv);
gl_FragColor = color;
}
`
};
const wavePass = new ShaderPass(WaveShader);
composer.addPass(wavePass);
// 在 animate 中更新 time
function animate() {
requestAnimationFrame(animate);
wavePass.uniforms.time.value = performance.now() / 1000; // 秒為單位
composer.render();
}
animate();
說明:透過 time uniform,畫面會產生水波般的扭曲效果,適合 UI 過場或水面模擬。
範例 3:結合 Bloom(發光)與色彩分級
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass.js';
import { ColorCorrectionShader } from 'three/examples/jsm/shaders/ColorCorrectionShader.js';
// 基礎 RenderPass
composer.addPass(new RenderPass(scene, camera));
// BloomPass
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // strength
0.4, // radius
0.85 // threshold
);
composer.addPass(bloomPass);
// 色彩分級 (使用內建的 ColorCorrectionShader)
const colorPass = new ShaderPass(ColorCorrectionShader);
colorPass.uniforms.powRGB.value = new THREE.Vector3(1.2, 1.1, 1.0); // 加強紅綠
composer.addPass(colorPass);
說明:先加上光暈,再以色彩分級微調整體氛圍。UnrealBloomPass 已內建於 Three.js 範例中,直接引入即可。
範例 4:使用 DepthTexture 產生簡易的景深(DOF)
// 1. 讓 renderer 支援深度緩衝區
renderer.autoClear = false;
renderer.setClearColor(0x000000);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
// 2. 為渲染目標加入 depthTexture
const renderTarget = new THREE.WebGLRenderTarget(
window.innerWidth,
window.innerHeight,
{ depthTexture: new THREE.DepthTexture(), depthBuffer: true }
);
const composer = new EffectComposer(renderer, renderTarget);
composer.addPass(new RenderPass(scene, camera));
// 3. Depth of Field Shader (簡化版)
const DOFShader = {
uniforms: {
tDiffuse: { value: null },
tDepth: { value: null },
focus: { value: 1.0 }, // 焦點距離
aperture: { value: 0.025 }, // 光圈大小
maxblur: { value: 0.01 } // 最大模糊
},
vertexShader: `...` , // 同前例
fragmentShader: `
uniform sampler2D tDiffuse;
uniform sampler2D tDepth;
uniform float focus;
uniform float aperture;
uniform float maxblur;
varying vec2 vUv;
float getDepth(const in vec2 screenPosition) {
return texture2D(tDepth, screenPosition).x;
}
void main() {
float depth = getDepth(vUv);
float factor = smoothstep(focus - aperture, focus + aperture, depth);
vec2 blur = vec2(maxblur) * factor;
vec4 color = vec4(0.0);
// 簡易 9 取樣模糊
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) {
vec2 offset = vec2(float(x), float(y)) * blur;
color += texture2D(tDiffuse, vUv + offset);
}
}
gl_FragColor = color / 9.0;
}
`
};
const dofPass = new ShaderPass(DOFShader);
dofPass.needsSwap = true; // 必須交換緩衝區
composer.addPass(dofPass);
說明:此範例示範如何把 depthTexture 帶入自訂 Shader,實作簡易的景深效果。實務上可根據需求調整 focus、aperture、maxblur。
範例 5:在手機端使用 SMAA 抗鋸齒 + Film Grain
import { SMAAPass } from 'three/examples/jsm/postprocessing/SMAAPass.js';
import { FilmPass } from 'three/examples/jsm/postprocessing/FilmPass.js';
// RenderPass
composer.addPass(new RenderPass(scene, camera));
// SMAA 抗鋸齒
const smaaPass = new SMAAPass(window.innerWidth, window.innerHeight);
composer.addPass(smaaPass);
// FilmPass (顆粒感 + 暈染)
const filmPass = new FilmPass(
0.35, // noise intensity
0.025, // scanline intensity
648, // scanline count
false // grayscale
);
composer.addPass(filmPass);
說明:手機 GPU 常見抗鋸齒不足的問題,SMAA 能以較低成本取得接近 MSAA 的效果;再加上 FilmPass,能快速產生老電影風格的顆粒與掃描線。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| Pass 執行順序錯誤 | 後製效果依賴前一個 Pass 的輸出,順序錯誤會導致畫面異常。 | 先加入 RenderPass,再依需求加入其他 Pass,必要時使用 renderToScreen 進行測試。 |
| Uniform 未更新 | 自訂 Shader 需要外部資料(時間、解析度)時,忘記在動畫迴圈中更新。 | 在 animate() 中顯式更新 pass.uniforms.xxx.value,或使用 Clock。 |
| 解析度改變未同步 | 視窗縮放時 EffectComposer、RenderTarget、Pass 的尺寸仍舊是舊值。 |
監聽 resize 事件,呼叫 composer.setSize()、smaaPass.setSize() 等。 |
| 過多 Pass 造成效能瓶頸 | 每個 Pass 都會產生一次全螢幕渲染,過多會降低 FPS。 | 只保留必要的特效,使用 THREE.Layers 或 mask 只對特定物件套用後製。 |
| DepthTexture 失效 | 未在 WebGLRenderTarget 中啟用 depthTexture,或在 RenderPass 前未設定 renderer.autoClear = false。 |
如範例 4 所示,正確建立 depthTexture 並在 composer 使用相同的 renderTarget。 |
最佳實踐
- 分離開發與調試:先只跑
RenderPass,確認場景正確;再逐步加入每個ShaderPass,同時使用pass.renderToScreen = true觀察單獨效果。 - 使用
THREE.Clock:統一管理時間 uniform,避免performance.now()帶來的非同步問題。 - 針對不同平台調整參數:桌面可使用較高的
blur、bloomStrength;手機則降低以維持 60 FPS。 - 緩存 Shader:若多處使用相同的自訂 Shader,請先建立一次
ShaderMaterial,再透過new ShaderPass(material)重複使用,減少編譯開銷。 - 利用
WebGLRenderer.info:觀察render.calls、render.triangles、memory.programs,確保加入的 Pass 未造成資源泄漏。
實際應用場景
| 場景 | 使用的 Pass 組合 | 效果說明 |
|---|---|---|
| 遊戲 UI 轉場 | RenderPass → ShaderPass (波浪) → FadePass |
先渲染遊戲畫面,再以波浪扭曲產生過場,最後淡出。 |
| 產品展示網站 | RenderPass → UnrealBloomPass → ColorCorrectionShader |
加上柔和光暈與色彩分級,提升商品質感。 |
| 虛擬實境 (VR) 影片 | RenderPass → SMAAPass → FilmPass |
抗鋸齒 + 老電影顆粒感,營造懷舊氛圍。 |
| 室內建築可視化 | RenderPass → DepthPass → SSAOPass → BloomPass |
深度圖產生 SSAO,提升空間感;再加光暈強調光源。 |
| 教育互動平台 | RenderPass → ShaderPass (反相) → OutlinePass |
反相效果突顯重點物件,搭配輪廓線更易於教學說明。 |
總結
RenderPass 與 ShaderPass 是 Three.js 後製特效 的兩大基石。透過 EffectComposer 我們可以把渲染流程抽象成一條條可堆疊的管線,從最基礎的畫面輸出,到自訂的 GLSL 濾鏡、深度效果、抗鋸齒與顆粒感,都能以 模組化、可重用 的方式實作。
本文從概念說明、完整程式碼範例、常見陷阱與最佳實踐,以及實務應用場景,提供了 1500+ 字 的全方位指南。只要掌握以下要點,就能在自己的 Three.js 專案中快速加入專業級的後製特效:
- 先建立 RenderPass,確保影像來源正確。
- 自訂 ShaderPass 時,務必提供
tDiffuseuniform,並在動畫迴圈中更新任何動態 uniform。 - 注意 Pass 執行順序 與 視窗尺寸同步,避免效能與畫面異常。
- 根據平台調整參數,並善用
EffectComposer的setSize、renderToScreen、needsSwap等屬性。
掌握這些技巧後,你將能在網頁 3D 項目中,輕鬆打造出「光暈、景深、顆粒、色彩分級」等多樣化的視覺效果,為使用者帶來更沉浸、更具衝擊力的體驗。祝開發順利,玩得開心!