本文 AI 產出,尚未審核

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。

最佳實踐

  1. 分離開發與調試:先只跑 RenderPass,確認場景正確;再逐步加入每個 ShaderPass,同時使用 pass.renderToScreen = true 觀察單獨效果。
  2. 使用 THREE.Clock:統一管理時間 uniform,避免 performance.now() 帶來的非同步問題。
  3. 針對不同平台調整參數:桌面可使用較高的 blur、bloomStrength;手機則降低以維持 60 FPS。
  4. 緩存 Shader:若多處使用相同的自訂 Shader,請先建立一次 ShaderMaterial,再透過 new ShaderPass(material) 重複使用,減少編譯開銷。
  5. 利用 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 時,務必提供 tDiffuse uniform,並在動畫迴圈中更新任何動態 uniform。
  • 注意 Pass 執行順序 與 視窗尺寸同步,避免效能與畫面異常。
  • 根據平台調整參數,並善用 EffectComposer 的 setSize、renderToScreen、needsSwap 等屬性。

掌握這些技巧後,你將能在網頁 3D 項目中,輕鬆打造出「光暈、景深、顆粒、色彩分級」等多樣化的視覺效果,為使用者帶來更沉浸、更具衝擊力的體驗。祝開發順利,玩得開心!