2D Canvas API 對畫線、畫圖、做粒子動畫夠用。但要做 3D、複雜 shader、上萬個物件,需要 WebGL。這篇用最簡的範例帶你從 2D 升級到 WebGL,搞懂 shader、buffer、texture 三件套。
2D vs WebGL 該選哪個
| 場景 | 建議 |
|---|---|
| UI、圖表、簡單動畫 | 2D Canvas |
| 2D 遊戲(< 1000 個物件) | 2D Canvas |
| 2D 遊戲(> 5000 個物件) | WebGL(PixiJS 包好) |
| 3D 模型、視覺特效 | WebGL(Three.js 包好) |
| 複雜 shader 效果(noise、glitch) | WebGL |
| 學習目的 | 原生 WebGL |
WebGL 三件套
每個 WebGL 應用都離不開:
- Shader:在 GPU 上跑的程式(vertex + fragment)
- Buffer:頂點資料(座標、顏色、UV)
- Texture:圖片貼圖
最小可運作範例:畫個三角形
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl2');
// 1. Shader 原始碼
const vertSrc = `#version 300 es
in vec2 aPos;
void main() {
gl_Position = vec4(aPos, 0, 1);
}`;
const fragSrc = `#version 300 es
precision highp float;
out vec4 fragColor;
void main() {
fragColor = vec4(0.29, 0.48, 0.96, 1); /* #4A7BF5 */
}`;
// 2. 編譯 + 連結
function shader(type, src) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
return s;
}
const program = gl.createProgram();
gl.attachShader(program, shader(gl.VERTEX_SHADER, vertSrc));
gl.attachShader(program, shader(gl.FRAGMENT_SHADER, fragSrc));
gl.linkProgram(program);
gl.useProgram(program);
// 3. Buffer:3 個頂點
const verts = new Float32Array([0, 0.7, -0.7, -0.7, 0.7, -0.7]);
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, verts, gl.STATIC_DRAW);
const aPos = gl.getAttribLocation(program, 'aPos');
gl.enableVertexAttribArray(aPos);
gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
// 4. 畫
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, 3);
這 30 行畫出一個藍色三角形。看起來繁瑣,但每一步都有目的:shader 程式碼、頂點資料、attribute 連接、繪圖呼叫。
Vertex Shader 在做什麼
每個頂點跑一次。主要工作:把模型座標轉到螢幕座標。
uniform mat4 uMVP; // model × view × projection
in vec3 aPos;
in vec2 aUV;
out vec2 vUV;
void main() {
gl_Position = uMVP * vec4(aPos, 1);
vUV = aUV;
}
Fragment Shader 在做什麼
每個 pixel 跑一次。決定顏色:
uniform sampler2D uTex;
in vec2 vUV;
out vec4 fragColor;
void main() {
fragColor = texture(uTex, vUV);
}
畫一張貼圖
// 載入圖片 const img = new Image(); img.src = 'texture.png'; await new Promise(r => img.onload = r); const tex = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, tex); gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
動畫:requestAnimationFrame
function draw(t) {
gl.uniform1f(uTime, t * 0.001); // pass time to shader
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, vertCount);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
常見特效(給點靈感)
- Procedural noise:fragment shader 跑 simplex noise,產生雲、火、水紋
- Glitch effect:用 sin / step / mod 做 RGB shift、scanline
- Glow / bloom:兩 pass 渲染(先 blur 高亮區,再合成)
- Distortion:UV offset 做扭曲、波紋
當 raw WebGL 太累 → 用框架
| 框架 | 適合 |
|---|---|
| Three.js | 3D 模型、場景、燈光 |
| PixiJS | 2D 遊戲(quad batching) |
| regl | 函式式 WebGL,shader-heavy |
| OGL | 輕量 Three.js 替代(5KB) |
Performance 觀察
- WebGL 比 2D Canvas 慢的場景:< 100 個物件時,setup overhead 比畫圖時間長
- WebGL 比 2D Canvas 快的場景:> 1000 個 sprite
- WebGL 殺手級場景:複雜 fragment shader(如 noise field)
Debug 困難
WebGL 出錯的訊息很無感。建議工具:
- Spector.js:捕捉每一個 GL call
- 瀏覽器 DevTools → Performance → 看 GPU 時間
- shader 編譯失敗時印出
gl.getShaderInfoLog
結語
WebGL 學習曲線陡,但理解 shader / buffer / texture 三件套之後,整個 GPU 程式設計世界打開。對視覺向專案是必修,對一般 web 應用是 nice-to-have。建議先寫 Three.js 練視覺感,再回來啃原生 WebGL 理解原理。