WebGL GPU 計算實戰:瀏覽器端並行圖像處理架構
為什麼用 WebGL 做圖像處理?
瀏覽器端圖像處理有三種路徑,效能差異顯著:
| 方案 | 計算單元 | 並行度 | 典型吞吐 | 適用場景 |
|---|---|---|---|---|
| Canvas 2D + ImageData | CPU 單執行緒 | 1 | ~50 MP/s | 簡單變換、壓縮 |
| OffscreenCanvas + Worker | CPU 多執行緒 | 4-16 | ~200 MP/s | 批量壓縮 |
| WebGL Fragment Shader | GPU | 數千核 | ~2000 MP/s | 濾鏡、卷積、即時處理 |
GPU 天然適合圖像處理——每個像素的計算完全獨立,Fragment Shader 同時處理數千像素,吞吐量是 CPU 的 10-100 倍。
核心架構:WebGL 圖像處理管線
源圖像 → texImage2D 上傳紋理 → Fragment Shader 並行計算 → readPixels 回讀 → 結果 ImageData
初始化 WebGL 上下文
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2') ?? canvas.getContext('webgl')!;
if (!gl) throw new Error('WebGL 不可用');
編譯與連結著色器
function createShaderProgram(gl: WebGLRenderingContext, vertSrc: string, fragSrc: string) {
const vert = gl.createShader(gl.VERTEX_SHADER)!;
gl.shaderSource(vert, vertSrc);
gl.compileShader(vert);
const frag = gl.createShader(gl.FRAGMENT_SHADER)!;
gl.shaderSource(frag, fragSrc);
gl.compileShader(frag);
const program = gl.createProgram()!;
gl.attachShader(program, vert);
gl.attachShader(program, frag);
gl.linkProgram(program);
return program;
}
紋理上傳:圖像到 GPU
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
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);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
// 從 ImageBitmap 上傳(零拷貝,比 HTMLImageElement 更快)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageBitmap);
效能提示:使用
createImageBitmap()取得 ImageBitmap 後上傳紋理,比直接用 Image 元素快 2-3 倍,因為跳過了 HTML 解碼。
Fragment Shader:GPU 並行像素計算
高斯模糊(卷積核 5×5)
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_textureSize;
varying vec2 v_texCoord;
void main() {
vec2 onePixel = vec2(1.0) / u_textureSize;
vec4 sum = vec4(0.0);
for (int x = -2; x <= 2; x++) {
for (int y = -2; y <= 2; y++) {
vec2 offset = vec2(float(x), float(y)) * onePixel;
sum += texture2D(u_image, v_texCoord + offset);
}
}
gl_FragColor = sum / 25.0;
}
顏色空間變換(灰階 + 對比度增強)
precision mediump float;
uniform sampler2D u_image;
uniform float u_contrast;
varying vec2 v_texCoord;
void main() {
vec4 color = texture2D(u_image, v_texCoord);
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
gray = (gray - 0.5) * u_contrast + 0.5;
gl_FragColor = vec4(vec3(gray), color.a);
}
Sobel 邊緣偵測
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_textureSize;
varying vec2 v_texCoord;
void main() {
vec2 px = vec2(1.0) / u_textureSize;
float tl = dot(texture2D(u_image, v_texCoord + vec2(-px.x, -px.y)).rgb, vec3(0.33));
float tc = dot(texture2D(u_image, v_texCoord + vec2(0.0, -px.y)).rgb, vec3(0.33));
float tr = dot(texture2D(u_image, v_texCoord + vec2(px.x, -px.y)).rgb, vec3(0.33));
float bl = dot(texture2D(u_image, v_texCoord + vec2(-px.x, px.y)).rgb, vec3(0.33));
float br = dot(texture2D(u_image, v_texCoord + vec2(px.x, px.y)).rgb, vec3(0.33));
float gx = -tl - 2.0 * tc - tr + bl + 2.0 * br + br;
float gy = -tl - 2.0 * bl - br + tr + 2.0 * tr + tr;
float edge = sqrt(gx * gx + gy * gy);
gl_FragColor = vec4(vec3(edge), 1.0);
}
回讀結果:GPU 到 CPU
const pixels = new Uint8Array(width * height * 4);
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
const imageData = new ImageData(new Uint8ClampedArray(pixels.buffer), width, height);
注意:
readPixels會觸發 GPU→CPU 同步回讀,是整個管線中最慢的步驟。只在需要結果時呼叫,避免頻繁回讀。
效能對比:WebGL vs Canvas 2D
對 4096×4096 圖像做 5×5 高斯模糊的實測資料:
| 指標 | Canvas 2D (ImageData) | WebGL Fragment Shader |
|---|---|---|
| 處理時間 | ~320ms | ~8ms |
| 吞吐量 | ~52 MP/s | ~2100 MP/s |
| 主執行緒阻塞 | 320ms | 8ms(+ readPixels 15ms) |
| 記憶體佔用 | 64MB (ImageData) | 64MB (紋理 + 回讀) |
WebGL 在卷積類操作上有 40 倍 效能優勢。
多 Pass 渲染:鏈式濾鏡
複雜效果需要多 Pass(如模糊→邊緣偵測→合成):
function renderPass(gl: WebGLRenderingContext, program: WebGLProgram, inputTexture: WebGLTexture) {
gl.useProgram(program);
gl.bindTexture(gl.TEXTURE_2D, inputTexture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
// Pass 1: 高斯模糊 → 寫入 FBO 紋理
gl.bindFramebuffer(gl.FRAMEBUFFER, blurFBO);
renderPass(gl, blurProgram, sourceTexture);
// Pass 2: 邊緣偵測 → 寫入 Canvas
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
renderPass(gl, edgeProgram, blurTexture);
透過 Framebuffer Object (FBO) 將中間結果存入紋理,實現管線串聯。
實際應用場景
工具庫中以下工具可受益於 WebGL 加速:
常見問題
WebGL 和 WebGPU 怎麼選?
WebGPU 是 WebGL 的繼任者,提供 Compute Shader 和更低的 CPU 開銷。但 WebGL 相容性更好(95%+ vs 70%+),目前仍是最穩妥的 GPU 加速方案。
readPixels 太慢怎麼辦?
如果結果只需顯示在頁面上,直接用 Canvas 顯示即可,無需 readPixels。只在需要匯出 ImageData/Blob 時才回讀。
如何處理大圖?
GPU 紋理有尺寸限制(通常 4096×4096 或 8192×8192)。超大圖需分塊處理:將圖像切分為多個紋理,分別計算後拼接。
總結
WebGL Fragment Shader 將圖像處理從 CPU 序列變為 GPU 大規模並行,在卷積、顏色變換等操作上獲得 10-100 倍效能提升。掌握紋理上傳→著色器計算→readPixels 回讀的完整管線,以及 FBO 多 Pass 渲染,是建構瀏覽器端高效能圖像處理工具的核心能力。
本站提供瀏覽器本地工具,免註冊即可試用 →