WebGL GPU計算実践:ブラウザ側並列画像処理アーキテクチャ
なぜ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加速の恩恵を受けます:
よくある質問
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マルチパスレンダリングの習得が、ブラウザ側の高性能画像処理ツール構築の核心です。
ブラウザローカルツールを無料で試す →