本文 AI 產出,尚未審核

Three.js 進階教材:Uniforms 與 Attributes

簡介

在 Three.js 中,Shader(著色器)是控制模型外觀的核心工具。雖然內建的 MeshBasicMaterialMeshStandardMaterial 已能滿足大多數需求,但要實現自訂的光照、動畫或特效時,就必須直接編寫 GLSL 程式碼。這時 UniformsAttributes 兩個概念就成了與 GPU 溝通的橋樑。

  • Uniforms:在整個渲染呼叫期間保持不變的資料,例如時間、光源位置或貼圖。它們類似於全域變數,所有頂點與片段都能存取同一份值。
  • Attributes:每個頂點獨有的資料,如座標、法線、顏色或自訂的偏移量。它們在頂點著色器執行時會逐一送入,決定每個頂點的最終位置或屬性。

掌握這兩者的使用方式,才能在 Three.js 中寫出 高度客製化、效能優化 的視覺效果。以下將從概念說明、實作範例、常見陷阱與最佳實踐,帶你一步步深入。

核心概念

1. Uniforms 的生命週期與類型

Uniform 變數在 每一次 renderer.render 前由 JavaScript 設定,之後在整個 draw call 中保持不變。Three.js 為常見類型提供了便利的封裝:

GLSL 型別 Three.js 設定方式 範例
float { value: 1.0 } uniforms.time = { value: 0.0 };
vec2 { value: new THREE.Vector2() } uniforms.resolution = { value: new THREE.Vector2(window.innerWidth, window.innerHeight) };
sampler2D { value: texture } uniforms.map = { value: myTexture };
mat4 { value: new THREE.Matrix4() } uniforms.modelMatrix = { value: mesh.matrixWorld };

小技巧:若同時使用多個 uniform,建議一次性建立一個物件 const uniforms = { time:{value:0}, resolution:{value:new THREE.Vector2()}, ... };,再傳給 ShaderMaterial,方便管理與更新。

2. Attributes 的來源與擴充

Three.js 自動為幾何體提供以下預設 attribute:

  • position(必備)
  • normal(若有光照)
  • uv(貼圖座標)

若要在 shader 中使用自訂屬性,需要先在 geometry 中加入 BufferAttribute,再在 ShaderMaterialattributes(已在 r124 起被 bufferGeometry.setAttribute 取代)中聲明。例如:

// 建立一個隨機的顏色屬性
const colors = new Float32Array( verticesCount * 3 );
for ( let i = 0; i < colors.length; i++ ) {
    colors[i] = Math.random(); // 0~1 的隨機顏色值
}
geometry.setAttribute( 'aColor', new THREE.BufferAttribute( colors, 3 ) );

在 GLSL 中即可讀取:

attribute vec3 aColor;
varying vec3 vColor; // 交給 fragment 使用
void main() {
    vColor = aColor;
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

3. Uniform 與 Attribute 的傳遞流程

  1. 建立 Geometry → 設定 positionnormal、自訂 attribute
  2. 建立 Uniform 物件 → 包含時間、解析度、貼圖等。
  3. 撰寫 Shader → 在頂點著色器引用 attribute,在片段著色器引用 uniform
  4. 建立 ShaderMaterial → 把 uniformsvertexShaderfragmentShader 丟進去。
  5. 每一幀更新 Uniform(如 uniforms.time.value = clock.getElapsedTime();),再呼叫 renderer.render

重點Uniform 只要在每幀更新一次即可;Attribute 在建立幾何時一次寫入,除非需要動態改變(如粒子系統),才在每幀更新對應的 BufferAttribute.needsUpdate = true

4. 範例一:時間驅動的波浪變形

// 1. 基本設定
const geometry = new THREE.PlaneBufferGeometry( 10, 10, 128, 128 );
const uniforms = {
    time: { value: 0.0 },
    amplitude: { value: 1.0 }
};

// 2. Vertex Shader
const vertexShader = `
uniform float time;
uniform float amplitude;
varying vec2 vUv;

void main() {
    vUv = uv;
    // 依照正弦波改變 y 軸
    vec3 pos = position;
    pos.z += sin( pos.x * 2.0 + time ) * amplitude;
    gl_Position = projectionMatrix * modelViewMatrix * vec4( pos, 1.0 );
}
`;

// 3. Fragment Shader
const fragmentShader = `
varying vec2 vUv;
void main() {
    gl_FragColor = vec4( vUv, 0.5, 1.0 );
}
`;

const material = new THREE.ShaderMaterial({
    uniforms,
    vertexShader,
    fragmentShader,
    side: THREE.DoubleSide
});

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

// 4. 更新時間
const clock = new THREE.Clock();
function animate(){
    requestAnimationFrame( animate );
    uniforms.time.value = clock.getElapsedTime();
    renderer.render( scene, camera );
}
animate();

說明time 為 uniform,每幀更新後波浪會隨時間流動。amplitude 可在 UI 中調整,控制波幅大小。

5. 範例二:自訂顏色 Attribute + 交錯條紋

// 建立一個長方體
const geometry = new THREE.BoxBufferGeometry( 2, 2, 2 );

// 為每個頂點產生交錯的黑白顏色
const colors = new Float32Array( geometry.attributes.position.count * 3 );
for ( let i = 0; i < geometry.attributes.position.count; i++ ) {
    const c = ( i % 2 === 0 ) ? 0.0 : 1.0; // 交錯 0 / 1
    colors[ i * 3 + 0 ] = c;
    colors[ i * 3 + 1 ] = c;
    colors[ i * 3 + 2 ] = c;
}
geometry.setAttribute( 'aColor', new THREE.BufferAttribute( colors, 3 ) );

const material = new THREE.ShaderMaterial({
    vertexShader: `
        attribute vec3 aColor;
        varying vec3 vColor;
        void main(){
            vColor = aColor;
            gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        }
    `,
    fragmentShader: `
        varying vec3 vColor;
        void main(){
            gl_FragColor = vec4( vColor, 1.0 );
        }
    `
});

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

說明:透過自訂 aColor attribute,直接在 GPU 端決定每個頂點的顏色,省去在 JavaScript 中每幀改變材質的成本。

6. 範例三:使用 Uniform 取樣貼圖並偏移 UV

const texture = new THREE.TextureLoader().load( 'textures/brick_diffuse.jpg' );
texture.wrapS = texture.wrapT = THREE.RepeatWrapping;

const uniforms = {
    map: { value: texture },
    offset: { value: new THREE.Vector2( 0, 0 ) }
};

const material = new THREE.ShaderMaterial({
    uniforms,
    vertexShader: `
        varying vec2 vUv;
        void main(){
            vUv = uv;
            gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        }
    `,
    fragmentShader: `
        uniform sampler2D map;
        uniform vec2 offset;
        varying vec2 vUv;
        void main(){
            vec2 uv = vUv + offset;
            gl_FragColor = texture2D( map, uv );
        }
    `
});

const plane = new THREE.Mesh( new THREE.PlaneBufferGeometry(5,5,1,1), material );
scene.add( plane );

// 每幀平移貼圖
function animate(){
    requestAnimationFrame( animate );
    uniforms.offset.value.x += 0.001; // 向右移動
    renderer.render( scene, camera );
}
animate();

說明offset 為 uniform,控制貼圖的 UV 偏移,可用於製作滾動背景或流動水面等效果。

常見陷阱與最佳實踐

陷阱 原因 解決方案
Uniform 沒有同步更新 animate 迴圈忘記寫 uniforms.xxx.value = ...,導致畫面卡住。 把 uniform 更新寫在 唯一 的渲染函式裡,或使用 THREE.Clock 統一管理時間。
Attribute 綁定錯誤 geometry.setAttribute 的第二個參數長度不符合 itemSize,會拋出錯誤或顯示雜訊。 確認 itemSize(如 3 for vec3)與陣列長度相符,必要時使用 console.assert 檢查。
忘記設定 needsUpdate 動態改變 BufferAttribute 後未標記 needsUpdate = true,GPU 仍使用舊資料。 在每次改變後執行 attribute.needsUpdate = true;
過度使用大量 Uniform 每個 draw call 只能傳遞有限的 uniform,過多會降低效能。 把不需要每幀改變的資料搬到 static uniform,或使用 texture 方式批次傳遞大量資料。
GLSL 版本不匹配 Three.js 內部使用 #version 300 es(WebGL2)或 #version 100(WebGL1),自行寫的 shader 版本不一致會編譯失敗。 依照 renderer.capabilities.isWebGL2 判斷,或使用 THREE.ShaderChunk 中的預設宏。

最佳實踐

  1. 最小化 Uniform 數量:只保留必須每幀改變的變數。
  2. 使用 THREE.InstancedBufferGeometry:對大量相同模型的變形,改用 instance attribute 而非重複建立 geometry。
  3. 把顏色、位移等頻繁變化的資料放在 Attribute,減少 CPU->GPU 的傳輸頻率。
  4. 使用 dat.GUIlil-gui 讓開發期間即時調整 uniform,快速找到最佳參數。

實際應用場景

場景 Uniform 的角色 Attribute 的角色
波浪海面 timewaveAmplitudewaveDirection(全局) aWaveOffset(每個頂點的相位)
粒子系統 texture(粒子貼圖)、cameraPosition(視角) aSizeaVelocityaLife(每粒子屬性)
城市光照 sunDirectionambientColor(全局光源) aBuildingHeight(建築高度)
後期特效 resolutiontimenoiseTexture(全局) aUVOffset(每個畫面格子不同的偏移)
動態貼圖 offsetscale(控制貼圖平移/縮放) aUV(自訂 UV)

例如在 粒子系統 中,aVelocityaLife 會在 CPU 端每幀寫入 InstancedBufferAttribute,而 time 只需要一次 uniform 更新,即可讓所有粒子依同一時間基礎自行計算位置,極大提升效能。

總結

  • Uniform 用於「全局」且「不頻繁」改變的資料,適合時間、光源、貼圖等。
  • Attribute 為「每個頂點」或「每個實例」的獨立資訊,適合位置、顏色、偏移、尺寸等。
  • 正確的 生命週期管理(何時更新 uniform、何時標記 attribute 需要更新)是避免渲染錯誤與效能瓶頸的關鍵。
  • 透過 ShaderMaterial 搭配 BufferAttribute,可以在 Three.js 中實作從簡單波浪到複雜粒子系統的各種特效。

掌握 Uniform 與 Attribute 的概念後,你就能在 Three.js 中自由發揮 GLSL 的威力,打造出 即時互動、視覺驚豔 的 3D 網頁體驗。祝你玩得開心,寫出更多精彩的 Shader!