本文 AI 產出,尚未審核

Three.js 教學 – 材質(Materials)

主題:自訂 ShaderMaterial 介紹


簡介

在 Three.js 中,材質(Material) 負責決定物件表面的外觀與光照行為。雖然內建的 MeshStandardMaterialMeshPhongMaterial 等已能滿足大多數需求,但當你想要實現獨特的視覺效果(例如噪波變形、動態光影、程式化紋理)時,內建材質往往捉襟見肘。

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 會自動將 positionnormaluv 等屬性傳入頂點著色器。若需要自訂屬性,必須先在 BufferGeometry 中宣告,再於 ShaderMaterialattributes(已在 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 = ...;

最佳實踐

  1. 模組化 shader:將常用函式(噪波、顏色轉換)抽成獨立字串,使用 THREE.ShaderChunkimport 合併,方便維護。
  2. 使用 onBeforeCompile 針對內建材質擴充:若只需要在 MeshStandardMaterial 上加一點自訂效果,直接覆寫 shader 更省事。
  3. 避免過度使用 varying:每個 varying 都會占用插值單元,過多會導致硬體限制。只傳遞必要資訊。
  4. 利用 THREE.MathUtils 產生隨機或噪波參數:保持程式碼與 Three.js 風格一致。
  5. 測試不同平台:手機 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 的其他功能結合(如 onBeforeCompileInstancedMesh),打造更高效的場景。

只要多加練習、善用範例與社群資源,你就能在 Three.js 中運用 ShaderMaterial,創造出令人驚豔的 3D 互動體驗。祝你玩得開心,寫出好看的自訂材質!