本文 AI 產出,尚未審核

Three.js – 材質與 Shader 進階

主題:基礎 Shader 動畫


簡介

在 WebGL 的世界裡,Shader 是掌控像素與頂點最直接的工具**。**雖然 Three.js 已經為我們封裝了大量的內建材質,但當需要「時間」或「互動」的效果時,單純使用 MeshStandardMaterial 已經捉不住細膩的變化。透過自訂 Shader,我們可以在每一幀 (frame) 直接操控顏色、形狀、透明度等屬性,從而實作出波浪、火焰、漸層過渡等動態視覺。

本單元將從最簡單的 Vertex ShaderFragment Shader 入手,示範如何把 時間 (uniform u_time) 注入 Shader,讓幾何體在畫面上「活」起來。文章適合已具備 Three.js 基礎(場景、相機、渲染器)且想深入了解 Shader 動畫的讀者,從概念、程式碼到實務最佳化,提供完整的學習路徑。


核心概念

1. Shader 的組成與運作流程

類型 作用 常見變數
Vertex Shader 處理每個頂點的座標、法線、UV 等資訊,決定最終投影到螢幕的 3D 位置 position, normal, uv, modelViewMatrix, projectionMatrix
Fragment Shader 處理每個像素的顏色與透明度,最終決定畫面上看到的顏色 gl_FragColor, vUv, uniforms

Three.js 會先執行 Vertex Shader,產生 varying(在兩個階段之間傳遞的資料),再由 Fragment Shader 進行每個像素的著色。

重點:若想在畫面上產生動畫,最常見的做法是把 時間 當作 uniform 傳入 Shader,然後在每一幀更新該 uniform 的值。

2. 建立自訂 ShaderMaterial

// 基本的 ShaderMaterial 範例
const material = new THREE.ShaderMaterial({
  vertexShader: /* glsl */`
    varying vec2 vUv;
    void main() {
      vUv = uv;                                   // 把 UV 傳給 fragment
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: /* glsl */`
    uniform float u_time;                         // 時間 uniform
    varying vec2 vUv;
    void main() {
      // 讓顏色隨時間在紅與藍之間變化
      vec3 color = 0.5 + 0.5 * sin(u_time + vUv.xyx * 3.1415);
      gl_FragColor = vec4(color, 1.0);
    }
  `,
  uniforms: {
    u_time: { value: 0.0 }                        // 初始時間為 0
  }
});
  • vertexShaderfragmentShader 必須使用 GLSL 語法。
  • uniforms 以 JavaScript 物件傳入,Three.js 會自動把值同步到 GPU。

3. 把時間注入 Shader

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  material.uniforms.u_time.value = clock.getElapsedTime(); // 更新 uniform
  renderer.render(scene, camera);
}
animate();

THREE.Clock 提供高精度的時間計算,getElapsedTime() 會回傳自 clock.start() 起的秒數。將此值寫入 u_time 後,Shader 內的 sin(u_time)cos(u_time) 等函式即可產生連續變化。

4. 常見的動畫手法

手法 目的 範例片段
波浪變形 (Vertex) 讓平面或曲面隨時間起伏 position.z += sin(position.x * 5.0 + u_time) * 0.2;
顏色漸變 (Fragment) 依 UV 或距離改變顏色 vec3 col = mix(vec3(1.0,0.0,0.0), vec3(0.0,0.0,1.0), vUv.x);
噪聲扭曲 (Fragment) 產生雲朵、火焰等自然感 使用 noise 函式或 SimplexNoise 產生隨機值

下面將提供 4 個完整範例,展示上述手法的實作方式。


程式碼範例

範例 1:簡易時間驅動的顏色漸變

// 建立一個平面幾何體
const geometry = new THREE.PlaneGeometry(4, 4, 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 float u_time;
    varying vec2 vUv;
    void main() {
      // 讓紅、綠、藍三色隨時間交替
      vec3 color = 0.5 + 0.5 * sin(u_time + vUv.xyx * 6.2831);
      gl_FragColor = vec4(color, 1.0);
    }
  `,
  uniforms: { u_time: { value: 0 } }
});

const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

// 動畫迴圈
const clock = new THREE.Clock();
function render() {
  requestAnimationFrame(render);
  material.uniforms.u_time.value = clock.getElapsedTime();
  renderer.render(scene, camera);
}
render();

說明vUv.xyx 把 UV 的 x、y、x 排列成向量,配合 sin 產生三色同步變化。

範例 2:頂點波浪(Vertex)

const geometry = new THREE.PlaneGeometry(6, 6, 128, 128); // 高細分讓波浪更平滑
const material = new THREE.ShaderMaterial({
  vertexShader: `
    uniform float u_time;
    varying vec2 vUv;
    void main() {
      vUv = uv;
      // 把 X、Y 位置乘上頻率,加入時間產生波浪
      vec3 pos = position;
      float freq = 3.0;
      float amp  = 0.3;
      pos.z += sin(pos.x * freq + u_time) * amp;
      pos.z += cos(pos.y * freq + u_time) * amp;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
    }
  `,
  fragmentShader: `
    varying vec2 vUv;
    void main() {
      // 依高度給予藍色調
      float shade = smoothstep(0.0, 0.6, gl_FragCoord.z);
      gl_FragColor = vec4(0.0, 0.4 + shade * 0.4, 0.8, 1.0);
    }
  `,
  uniforms: { u_time: { value: 0 } },
  side: THREE.DoubleSide
});

scene.add(new THREE.Mesh(geometry, material));

const clock = new THREE.Clock();
function animate() {
  requestAnimationFrame(animate);
  material.uniforms.u_time.value = clock.getElapsedTime();
  renderer.render(scene, camera);
}
animate();

技巧:使用較高的細分 (widthSegments, heightSegments) 才能讓波浪不會出現「鋸齒」或「平面」的感覺。

範例 3:噪聲色彩扭曲(Fragment)

// 引入 SimplexNoise (可從 CDN 取得)
import { SimplexNoise } from 'https://cdn.jsdelivr.net/npm/simplex-noise@3.0.0/+esm';

const simplex = new SimplexNoise();

const geometry = new THREE.SphereGeometry(2, 64, 64);
const material = new THREE.ShaderMaterial({
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform float u_time;
    varying vec2 vUv;

    // 內建的噪聲函式(簡化版)
    float noise(vec2 p) {
      return ${simplex.noise2D.toString()}(p);
    }

    void main() {
      // 依時間與座標產生漸變噪聲
      float n = noise(vUv * 5.0 + u_time * 0.5);
      vec3 col = mix(vec3(0.2, 0.0, 0.5), vec3(1.0, 0.8, 0.2), smoothstep(-0.2, 0.2, n));
      gl_FragColor = vec4(col, 1.0);
    }
  `,
  uniforms: { u_time: { value: 0 } }
});

scene.add(new THREE.Mesh(geometry, material));

const clock = new THREE.Clock();
function render() {
  requestAnimationFrame(render);
  material.uniforms.u_time.value = clock.getElapsedTime();
  renderer.render(scene, camera);
}
render();

說明:此範例透過 SimplexNoise 產生雲朵般的顏色變化,適合製作火焰、煙霧等自然效果。

範例 4:UV 方向的條紋掃描(Fragment)

const geometry = new THREE.PlaneGeometry(5, 5);
const material = new THREE.ShaderMaterial({
  vertexShader: `
    varying vec2 vUv;
    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
  `,
  fragmentShader: `
    uniform float u_time;
    varying vec2 vUv;
    void main() {
      // 產生每 0.2 單位的條紋,隨時間向右移動
      float stripe = step(0.5, fract(vUv.x * 5.0 + u_time));
      vec3 color = mix(vec3(0.0,0.0,0.0), vec3(1.0,0.8,0.2), stripe);
      gl_FragColor = vec4(color, 1.0);
    }
  `,
  uniforms: { u_time: { value: 0 } }
});
scene.add(new THREE.Mesh(geometry, material));

const clock = new THREE.Clock();
function loop() {
  requestAnimationFrame(loop);
  material.uniforms.u_time.value = clock.getElapsedTime();
  renderer.render(scene, camera);
}
loop();

技巧fract 取得小數部分,step 用於產生二元(0/1)條紋,簡易實作「掃描線」效果。


常見陷阱與最佳實踐

  1. Uniform 更新頻率

    • 陷阱:在每一幀都重新建立 uniforms 物件會導致 GPU 重新編譯 Shader,嚴重拖慢效能。
    • 做法:只在初始化時建立一次,之後只改變 uniforms.xxx.value
  2. Precision(精度)問題

    • 在 fragment shader 中若未指定 precision mediump float;,在部分行動裝置會出現渲染錯誤。
    • 建議在 shader 開頭加入:
      precision mediump float;
      precision mediump int;
      
  3. 過度細分的幾何體

    • 陷阱:為了波浪而把平面細分到 512x512,會讓 CPU/GPU 記憶體飆升。
    • 最佳實踐:根據需求選擇適當的細分,或改用 GPU Instancing + Vertex Texture Fetch(如果需要更高頻率的變形)。
  4. 時間精度與循環

    • clock.getElapsedTime() 會隨著程式執行時間無限累加,長時間運行會產生浮點精度誤差。
    • 常見做法:使用 mod(u_time, 2.0 * PI) 把時間限制在一個週期內,或自行重設 clock.start()
  5. 避免硬編碼常數

    • 把頻率、振幅等參數寫成 uniform,讓程式在執行時可調整,提升可重用性。

實際應用場景

場景 可能的 Shader 動畫 為何適合使用自訂 Shader
音樂視覺化 依音量或頻譜驅動波形、光柱 時間+音訊資料的結合需要高頻更新,GPU 計算更流暢
互動式 UI 效果 按鈕點擊時的漸變、光環擴散 只改變 uniform 即可完成,不必重建 Mesh
遊戲特效 火焰、煙霧、波浪海面 需要大量像素運算,Shader 能在單帧內完成
資料可視化 時間序列的熱圖、流向圖 用顏色或形狀變化直接映射資料,提升直觀感受
AR/VR 環境 立體空間中的光束、環繞動畫 VR 需要高 FPS,Shader 可減少 CPU 負擔

總結

  • Shader 是 Three.js 中最靈活的渲染工具,透過 uniform 把時間、音訊或使用者輸入傳入 GPU,即可在 VertexFragment 階段產生各式動畫。
  • 建立 ShaderMaterial 時,務必把 precisionuniformvarying 設計好,避免在每幀重新編譯。
  • 本文提供四個實作範例:簡易顏色漸變、頂點波浪、噪聲扭曲與條紋掃描,示範了從最基礎到稍微進階的技巧。
  • 在實務開發中,根據需求選擇適當的細分度、把可調參數抽成 uniform,並注意時間精度與效能瓶頸,即可在 WebGL 上打造流暢且富有表現力的動畫。

掌握了 基礎 Shader 動畫,你就能在 Three.js 專案中加入更具吸引力的視覺效果,從簡單的 UI 交互到複雜的遊戲特效,都能以最小的成本達成。祝你玩得開心,創作出令人驚豔的 3D 動畫!