WebGL で実現するリアルタイム画像エフェクト - シェーダー入門から実践まで
WebGL による画像処理の基本概念 - GPU の並列処理能力を活用する
WebGL は Web ブラウザから GPU (Graphics Processing Unit) にアクセスするための API です。画像処理において WebGL が強力なのは、GPU の大規模並列処理能力を活用できる点にあります。CPU が逐次的にピクセルを処理するのに対し、GPU は数千のコアで同時に複数ピクセルを処理できるため、リアルタイムのエフェクト適用が可能になります。
Canvas 2D API との性能差: 1,920 × 1,080の画像にガウシアンぼかしを適用する場合、Canvas 2D API (CPU) では 50-200ms かかる処理が、WebGL (GPU) では 1-5ms で完了します。この 10-100 倍の速度差が、60fps のリアルタイムエフェクトを実現する鍵です。
WebGL 画像処理のパイプライン:
- 画像をテクスチャとして GPU メモリにアップロードする
- 画面全体を覆う四角形 (フルスクリーンクワッド) を描画する
- フラグメントシェーダーが各ピクセルの色を計算する
- 計算結果が Canvas に描画される
フラグメントシェーダーは GLSL (OpenGL Shading Language) で記述します。各ピクセルに対して独立に実行されるため、隣接ピクセルの情報が必要なぼかし処理でも、テクスチャサンプリングで周囲のピクセル値を参照できます。WebGL 2.0 では GLSL ES 3.0 が使用可能で、整数演算やテクスチャ配列など、より高度な機能が利用できます。
WebGL の初期セットアップ - テクスチャ描画の最小構成
WebGL で画像エフェクトを実装するための最小限のセットアップコードを示します。頂点シェーダーは画面全体を覆うクワッドを描画し、フラグメントシェーダーでエフェクトを適用する構成です。
頂点シェーダー (vertex shader):
attribute vec2 a_position;attribute vec2 a_texCoord;varying vec2 v_texCoord;void main() { gl_Position = vec4(a_position, 0.0, 1.0); v_texCoord = a_texCoord;}
基本フラグメントシェーダー (パススルー):
precision mediump float;uniform sampler2D u_image;varying vec2 v_texCoord;void main() { gl_FragColor = texture2D(u_image, v_texCoord);}
JavaScript 側の初期化: WebGL コンテキストの取得、シェーダーのコンパイル・リンク、テクスチャのアップロード、頂点バッファの設定が必要です。ボイラープレートが多いため、実務では twgl.js (WebGL ユーティリティライブラリ、gzip 後 12KB) や regl を使うと記述量を大幅に削減できます。
const canvas = document.getElementById('canvas');const gl = canvas.getContext('webgl2');// シェーダーコンパイル、プログラムリンク、テクスチャ設定...
テクスチャのアップロードでは、gl.texImage2D に HTMLImageElement を直接渡せます。NPOT (Non-Power-Of-Two) テクスチャも WebGL 2.0 では制限なく使用可能です。画像サイズが大きい場合は、gl.texSubImage2D で部分更新することでメモリ効率を改善できます。
色調補正エフェクト - 明るさ、コントラスト、彩度の調整
最も基本的な画像エフェクトは色調補正です。各ピクセルの色値を数学的に変換するだけなので、テクスチャサンプリングは 1 回で済み、非常に高速に動作します。
明るさ (Brightness) 調整:
uniform float u_brightness; // -1.0 ~ 1.0vec4 color = texture2D(u_image, v_texCoord);gl_FragColor = vec4(color.rgb + u_brightness, color.a);
コントラスト (Contrast) 調整:
uniform float u_contrast; // 0.0 ~ 2.0 (1.0 が元の状態)vec4 color = texture2D(u_image, v_texCoord);vec3 adjusted = (color.rgb - 0.5) * u_contrast + 0.5;gl_FragColor = vec4(adjusted, color.a);
彩度 (Saturation) 調整: RGB をグレースケール値と混合することで彩度を制御します。
uniform float u_saturation; // 0.0 (グレー) ~ 2.0 (過飽和)vec4 color = texture2D(u_image, v_texCoord);float gray = dot(color.rgb, vec3(0.2126, 0.7152, 0.0722));vec3 adjusted = mix(vec3(gray), color.rgb, u_saturation);gl_FragColor = vec4(adjusted, color.a);
これらのエフェクトを組み合わせることで、Instagram のようなフィルタ効果を実現できます。セピア調、ヴィンテージ風、ハイコントラストなど、カラーマトリクス変換で多彩な表現が可能です。uniform 変数をスライダー UI と連動させれば、ユーザーがリアルタイムに調整できるインタラクティブな画像エディタが構築できます。
ぼかしエフェクト - ガウシアンブラーの効率的な実装
ガウシアンぼかしは画像処理で最も頻繁に使われるエフェクトの一つですが、カーネルサイズが大きくなるとテクスチャサンプリング回数が急増するため、効率的な実装が重要です。
ナイーブな実装の問題: 半径 r のガウシアンぼかしは (2r+1)x(2r+1) のカーネルを使用します。半径 10 の場合、1 ピクセルあたり 441 回のテクスチャサンプリングが必要で、1,920 × 1,080の画像では約 9 億回のサンプリングになります。
2 パス分離フィルタ: ガウシアンカーネルは分離可能 (separable) なので、水平方向と垂直方向の 2 パスに分割できます。これにより、サンプリング回数が (2r+1)^2 から 2*(2r+1) に削減されます。半径 10 の場合、441 回が 42 回に減少します。
// 水平パスuniform vec2 u_direction; // vec2(1.0/width, 0.0)vec4 sum = vec4(0.0);for (int i = -RADIUS; i <= RADIUS; i++) { float weight = gaussian(float(i), u_sigma); sum += texture2D(u_image, v_texCoord + u_direction * float(i)) * weight;}gl_FragColor = sum;
リニアサンプリング最適化: GPU のバイリニアフィルタリングを活用し、隣接する 2 つのサンプルを 1 回のテクスチャフェッチで取得するテクニックがあります。これにより、サンプリング回数をさらに半分に削減できます。半径 10 の場合、実質 11 回のフェッチで済みます。
マルチパスダウンサンプリング: 大きなぼかし半径が必要な場合、画像を段階的に縮小してからぼかしを適用し、再度拡大する方法が効率的です。Kawase blur や Dual blur アルゴリズムはこの原理を応用しており、ゲームエンジンで広く使われています。
歪みエフェクト - UV 座標の操作による視覚効果
歪み (Distortion) エフェクトは、テクスチャ座標 (UV 座標) を数学的に変換することで実現します。ピクセルの色自体は変更せず、「どの位置のピクセルを読み取るか」を変えることで、波紋、渦巻き、魚眼レンズなどの効果を生み出します。
波紋 (Ripple) エフェクト:
uniform float u_time;uniform float u_amplitude; // 0.01 ~ 0.05uniform float u_frequency; // 10.0 ~ 30.0vec2 uv = v_texCoord;float dist = distance(uv, vec2(0.5));uv += normalize(uv - vec2(0.5)) * sin(dist * u_frequency - u_time) * u_amplitude;gl_FragColor = texture2D(u_image, uv);
渦巻き (Swirl) エフェクト:
uniform float u_angle; // 回転角度uniform float u_radius; // 効果範囲vec2 center = vec2(0.5);vec2 uv = v_texCoord - center;float dist = length(uv);float factor = smoothstep(u_radius, 0.0, dist);float angle = factor * u_angle;uv = mat2(cos(angle), -sin(angle), sin(angle), cos(angle)) * uv;gl_FragColor = texture2D(u_image, uv + center);
魚眼レンズ (Fisheye) エフェクト: 中心からの距離に応じて座標を非線形に変換します。pow(dist, 2.0) で二次関数的な歪みを加えると、レンズの光学特性に近い効果が得られます。
歪みエフェクトでは、変換後の UV 座標が 0.0-1.0 の範囲外になる場合があります。gl_CLAMP_TO_EDGE テクスチャラッピングモードを設定するか、範囲外を透明にする処理を追加してください。u_time uniform を requestAnimationFrame で更新すれば、アニメーションする歪みエフェクトが実現できます。
パフォーマンス最適化とライブラリ活用 - 実務での WebGL 画像処理
WebGL 画像処理を本番環境で使用する際のパフォーマンス最適化と、開発効率を高めるライブラリの活用方法を紹介します。
パフォーマンス最適化のポイント:
- テクスチャサイズの制限: モバイルデバイスでは最大テクスチャサイズが 4,096 × 4,096に制限される場合があります。
gl.getParameter(gl.MAX_TEXTURE_SIZE)で確認し、必要に応じて縮小してからアップロードしてください - FBO (Framebuffer Object) の再利用: マルチパスエフェクトでは、フレームバッファを毎フレーム作成せず、事前に確保したものを ping-pong 方式で使い回します
- uniform の更新最小化: 変更のない uniform は毎フレーム設定し直さない。WebGL の状態変更はコストが高いため、必要な場合のみ更新します
- 精度指定: モバイルでは
precision mediump floatで十分な場合が多く、highpより高速に動作します
推奨ライブラリ:
- PixiJS: 2D レンダリングライブラリ。フィルタシステムが充実しており、カスタムシェーダーも簡単に追加可能。画像エフェクトアプリに最適
- Three.js (EffectComposer): 3D ライブラリだが、ポストプロセッシングパイプラインが強力。複数エフェクトのチェーンが容易
- gl-react: React コンポーネントとして WebGL シェーダーを記述できる。宣言的な API で開発効率が高い
- gpu.js: JavaScript 関数を GPU カーネルに変換するライブラリ。GLSL を書かずに GPU 並列処理を活用可能
フォールバック戦略: WebGL が利用できない環境 (古いブラウザ、GPU ドライバの問題) では、CSS フィルタや Canvas 2D API にフォールバックする設計が必要です。canvas.getContext('webgl2') || canvas.getContext('webgl') で対応状況を確認し、非対応の場合は filter: blur() brightness() などの CSS フィルタで代替します。