WebGL GPU計算実践:ブラウザ側並列画像処理アーキテクチャ

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

なぜWebGLで画像処理をするのか?

ブラウザ側画像処理には3つのパスがあり、パフォーマンス差は顕著です:

手法 計算ユニット 並列度 典型スループット 適用場面
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を取得してからテクスチャにアップロードすると、HTMLデコードをスキップでき2-3倍高速です。


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倍のパフォーマンス優位性を持ちます。


マルチパスレンダリング:チェーンフィルタ

複雑な効果にはマルチパスが必要です(例:ぼかし→エッジ検出→合成):

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) で中間結果をテクスチャに保存し、パイプラインをチェーンします。


実践的なユースケース

ToolsKuの以下のツールが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マルチパスレンダリングの習得が、ブラウザ側の高性能画像処理ツール構築の核心です。

ブラウザローカルツールを無料で試す →

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