Engineering

WebGL 入門:在 Canvas 裡做 3D

2026 年 5 月 6 日約 11 分鐘閱讀作者:Hao0321 Studio

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 應用都離不開:

  1. Shader:在 GPU 上跑的程式(vertex + fragment)
  2. Buffer:頂點資料(座標、顏色、UV)
  3. 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);

常見特效(給點靈感)

當 raw WebGL 太累 → 用框架

框架適合
Three.js3D 模型、場景、燈光
PixiJS2D 遊戲(quad batching)
regl函式式 WebGL,shader-heavy
OGL輕量 Three.js 替代(5KB)

Performance 觀察

Debug 困難

WebGL 出錯的訊息很無感。建議工具:

結語

WebGL 學習曲線陡,但理解 shader / buffer / texture 三件套之後,整個 GPU 程式設計世界打開。對視覺向專案是必修,對一般 web 應用是 nice-to-have。建議先寫 Three.js 練視覺感,再回來啃原生 WebGL 理解原理。