本文 AI 產出,尚未審核

Three.js 後製特效 – RenderPass 與 ShaderPass 完全攻略


簡介

在 3D 網頁應用中,單純的幾何渲染往往難以滿足視覺衝擊力。後製特效(Post‑Processing) 能在最終影像輸出前加入各式濾鏡、光暈、景深等效果,讓場景更具電影感。Three.js 內建的 EffectComposer 搭配 RenderPassShaderPass,提供一套彈性極高且易於擴充的管線(pipeline)機制。

本篇文章將從概念說明、實作範例、常見陷阱與最佳實踐,帶領讀者一步步掌握 RenderPassShaderPass 的使用方式,讓你能在專案中快速加入自訂的後製效果。


核心概念

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 程式碼,最常只需要自訂 fragmentShadervertexShader 多半使用預設的全螢幕四邊形頂點著色器
// 範例:簡易的反相濾鏡
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 個實用範例,涵蓋從最基礎到稍微進階的應用。所有範例均使用 EffectComposerRenderPassShaderPass,並附上說明。

範例 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,實作簡易的景深效果。實務上可根據需求調整 focusaperturemaxblur

範例 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
解析度改變未同步 視窗縮放時 EffectComposerRenderTargetPass 的尺寸仍舊是舊值。 監聽 resize 事件,呼叫 composer.setSize()smaaPass.setSize() 等。
過多 Pass 造成效能瓶頸 每個 Pass 都會產生一次全螢幕渲染,過多會降低 FPS。 只保留必要的特效,使用 THREE.Layersmask 只對特定物件套用後製。
DepthTexture 失效 未在 WebGLRenderTarget 中啟用 depthTexture,或在 RenderPass 前未設定 renderer.autoClear = false 如範例 4 所示,正確建立 depthTexture 並在 composer 使用相同的 renderTarget。

最佳實踐

  1. 分離開發與調試:先只跑 RenderPass,確認場景正確;再逐步加入每個 ShaderPass,同時使用 pass.renderToScreen = true 觀察單獨效果。
  2. 使用 THREE.Clock:統一管理時間 uniform,避免 performance.now() 帶來的非同步問題。
  3. 針對不同平台調整參數:桌面可使用較高的 blurbloomStrength;手機則降低以維持 60 FPS。
  4. 緩存 Shader:若多處使用相同的自訂 Shader,請先建立一次 ShaderMaterial,再透過 new ShaderPass(material) 重複使用,減少編譯開銷。
  5. 利用 WebGLRenderer.info:觀察 render.callsrender.trianglesmemory.programs,確保加入的 Pass 未造成資源泄漏。

實際應用場景

場景 使用的 Pass 組合 效果說明
遊戲 UI 轉場 RenderPass → ShaderPass (波浪) → FadePass 先渲染遊戲畫面,再以波浪扭曲產生過場,最後淡出。
產品展示網站 RenderPass → UnrealBloomPass → ColorCorrectionShader 加上柔和光暈與色彩分級,提升商品質感。
虛擬實境 (VR) 影片 RenderPass → SMAAPass → FilmPass 抗鋸齒 + 老電影顆粒感,營造懷舊氛圍。
室內建築可視化 RenderPass → DepthPass → SSAOPass → BloomPass 深度圖產生 SSAO,提升空間感;再加光暈強調光源。
教育互動平台 RenderPass → ShaderPass (反相) → OutlinePass 反相效果突顯重點物件,搭配輪廓線更易於教學說明。

總結

RenderPassShaderPassThree.js 後製特效 的兩大基石。透過 EffectComposer 我們可以把渲染流程抽象成一條條可堆疊的管線,從最基礎的畫面輸出,到自訂的 GLSL 濾鏡、深度效果、抗鋸齒與顆粒感,都能以 模組化可重用 的方式實作。

本文從概念說明、完整程式碼範例、常見陷阱與最佳實踐,以及實務應用場景,提供了 1500+ 字 的全方位指南。只要掌握以下要點,就能在自己的 Three.js 專案中快速加入專業級的後製特效:

  • 先建立 RenderPass,確保影像來源正確。
  • 自訂 ShaderPass 時,務必提供 tDiffuse uniform,並在動畫迴圈中更新任何動態 uniform。
  • 注意 Pass 執行順序視窗尺寸同步,避免效能與畫面異常。
  • 根據平台調整參數,並善用 EffectComposersetSizerenderToScreenneedsSwap 等屬性。

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