WebGL GPU 計算實戰:瀏覽器端並行圖像處理架構

技术架构(更新於 2026年6月11日)

為什麼用 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 加速:

  • 圖片壓縮:顏色空間變換 + 降取樣用 GPU 並行
  • 圖片縮放:雙線性/雙三次插值用 Fragment Shader
  • 圖片變換:旋轉、翻轉用頂點著色器矩陣變換

常見問題

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 渲染,是建構瀏覽器端高效能圖像處理工具的核心能力。

本站提供瀏覽器本地工具,免註冊即可試用 →

#WebGL#GPU计算#着色器#图像处理#并行计算