Three.js 進階教材:Uniforms 與 Attributes
簡介
在 Three.js 中,Shader(著色器)是控制模型外觀的核心工具。雖然內建的 MeshBasicMaterial、MeshStandardMaterial 已能滿足大多數需求,但要實現自訂的光照、動畫或特效時,就必須直接編寫 GLSL 程式碼。這時 Uniforms 與 Attributes 兩個概念就成了與 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,再在 ShaderMaterial 的 attributes(已在 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 的傳遞流程
- 建立 Geometry → 設定
position、normal、自訂attribute。 - 建立 Uniform 物件 → 包含時間、解析度、貼圖等。
- 撰寫 Shader → 在頂點著色器引用
attribute,在片段著色器引用uniform。 - 建立 ShaderMaterial → 把
uniforms、vertexShader、fragmentShader丟進去。 - 每一幀更新 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 中的預設宏。 |
最佳實踐
- 最小化 Uniform 數量:只保留必須每幀改變的變數。
- 使用
THREE.InstancedBufferGeometry:對大量相同模型的變形,改用 instance attribute 而非重複建立 geometry。 - 把顏色、位移等頻繁變化的資料放在 Attribute,減少 CPU->GPU 的傳輸頻率。
- 使用
dat.GUI或lil-gui讓開發期間即時調整 uniform,快速找到最佳參數。
實際應用場景
| 場景 | Uniform 的角色 | Attribute 的角色 |
|---|---|---|
| 波浪海面 | time、waveAmplitude、waveDirection(全局) |
aWaveOffset(每個頂點的相位) |
| 粒子系統 | texture(粒子貼圖)、cameraPosition(視角) |
aSize、aVelocity、aLife(每粒子屬性) |
| 城市光照 | sunDirection、ambientColor(全局光源) |
aBuildingHeight(建築高度) |
| 後期特效 | resolution、time、noiseTexture(全局) |
aUVOffset(每個畫面格子不同的偏移) |
| 動態貼圖 | offset、scale(控制貼圖平移/縮放) |
aUV(自訂 UV) |
例如在 粒子系統 中,aVelocity 與 aLife 會在 CPU 端每幀寫入 InstancedBufferAttribute,而 time 只需要一次 uniform 更新,即可讓所有粒子依同一時間基礎自行計算位置,極大提升效能。
總結
- Uniform 用於「全局」且「不頻繁」改變的資料,適合時間、光源、貼圖等。
- Attribute 為「每個頂點」或「每個實例」的獨立資訊,適合位置、顏色、偏移、尺寸等。
- 正確的 生命週期管理(何時更新 uniform、何時標記 attribute 需要更新)是避免渲染錯誤與效能瓶頸的關鍵。
- 透過 ShaderMaterial 搭配
BufferAttribute,可以在 Three.js 中實作從簡單波浪到複雜粒子系統的各種特效。
掌握 Uniform 與 Attribute 的概念後,你就能在 Three.js 中自由發揮 GLSL 的威力,打造出 即時互動、視覺驚豔 的 3D 網頁體驗。祝你玩得開心,寫出更多精彩的 Shader!