WebGPU画像シェーダーでレンズ効果を再現 2025 — ローパワーデバイス最適化ガイド

公開: 2025年9月29日 · 読了目安: 5 · 著者: Unified Image Tools 編集部

2025 年、WebGPU は主要ブラウザで安定版となり、画像演出に大型の WebGL フレームワークを使う必要がなくなりました。Web コーダーが気になるのは、リッチなレンズ効果(レンズフレア、被写界深度、グロウ)を導入した際のパフォーマンスと電力消費です。本稿では、WebGPU の計算シェーダーとテクスチャパイプラインを組み合わせ、ローパワーデバイスでも 60fps を維持する実装パターンと最適化テクニックを紹介します。

TL;DR

  • 2段構成のレンダリング: (1) ライトブロッカー抽出用のコンピュートパス、(2) 合成・ブラー用のレンダーパスで分離。
  • タイルベースのダウンサンプリング: レンズフレアは 1/4 解像度で計算し、合成時に mix で補間。
  • 適応サンプリング: navigator.gpu.getPreferredCanvasFormat() とバッテリー API からサンプル数を調整。
  • GPU-CPU 協調ロギング: performance-guardian に WebGPU のタイムスタンプクエリを送信し、フレームコストをトラッキング。
  • アクセシビリティ対策: prefers-reduced-motion を尊重し、静的画像へフォールバックするプレースメントを用意。

パイプライン全体像

graph LR
    A[Source Texture] --> B[Compute Shader: Bright Spot Detection]
    B --> C[Compute Shader: Flare Tile Accumulation]
    C --> D[Render Pass: Bokeh & Bloom]
    D --> E[Post Processing: Tone Mapping]
    E --> F[Canvas / Texture Output]

WebGPU 設定の初期化

const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'low-power' })
const device = await adapter?.requestDevice({
  requiredFeatures: ['timestamp-query'],
  requiredLimits: { maxTextureDimension2D: 4096 }
})
const context = canvas.getContext('webgpu')!
context.configure({
  device,
  format: navigator.gpu.getPreferredCanvasFormat(),
  alphaMode: 'premultiplied'
})

timestamp-query が無効な場合は CPU 側でフォールバックします。

ライトブロッカー抽出シェーダー

// shaders/bright-spot.wgsl
@group(0) @binding(0) var<storage, read> inputImage: array<vec4<f32>>;
@group(0) @binding(1) var<storage, read_write> brightMask: array<f32>;

@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
  let idx = global_id.y * textureWidth + global_id.x;
  if (idx >= arrayLength(&inputImage)) { return; }
  let color = inputImage[idx];
  let luminance = dot(color.rgb, vec3<f32>(0.299, 0.587, 0.114));
  brightMask[idx] = select(0.0, luminance, luminance > params.threshold);
}

params.threshold はユーザー操作(スライダ)で調整可能にしておくと、演出を柔軟に変えられます。

タイルベースの蓄積

// shaders/flare-accumulate.wgsl
@group(0) @binding(0) var<storage, read> brightMask: array<f32>;
@group(0) @binding(1) var<storage, read_write> flareTiles: array<vec4<f32>>;

@compute @workgroup_size(8, 8)
fn main(@builtin(workgroup_id) group_id: vec3<u32>) {
  let tileIndex = group_id.y * tileCountX + group_id.x;
  var sum = vec4<f32>(0.0);
  for (var y = 0u; y < TILE_SIZE; y = y + 1u) {
    for (var x = 0u; x < TILE_SIZE; x = x + 1u) {
      let idx = (group_id.y * TILE_SIZE + y) * textureWidth + (group_id.x * TILE_SIZE + x);
      sum += vec4<f32>(brightMask[idx]);
    }
  }
  flareTiles[tileIndex] = sum / f32(TILE_SIZE * TILE_SIZE);
}

タイルサイズは 16〜32 を目安にし、低スペック環境では解像度をさらに落とします。

レンダーパスでの合成

// pipeline/render.ts
const flarePipeline = device.createRenderPipeline({
  vertex: { module: device.createShaderModule({ code: quadVert }) },
  fragment: {
    module: device.createShaderModule({ code: flareFrag }),
    targets: [{ format: contextFormat }]
  },
  primitive: { topology: 'triangle-list' }
})

commandEncoder.beginRenderPass({
  colorAttachments: [{
    view: context.getCurrentTexture().createView(),
    loadOp: 'load',
    storeOp: 'store',
    clearValue: { r: 0, g: 0, b: 0, a: 1 }
  }]
})

フラグメントシェーダーではブルームマップとボケテクスチャを mix して合成します。

// shaders/flare.frag.wgsl
@group(0) @binding(0) var flareSampler: sampler;
@group(0) @binding(1) var flareTexture: texture_2d<f32>;
@group(0) @binding(2) var blurTexture: texture_2d<f32>;

@fragment
fn main(@builtin(position) pos: vec4<f32>) -> @location(0) vec4<f32> {
  let uv = pos.xy / vec2<f32>(canvasSize);
  let flare = textureSample(flareTexture, flareSampler, uv);
  let blur = textureSample(blurTexture, flareSampler, uv);
  return mix(flare, blur, params.blurMix) * params.intensity;
}

電力とパフォーマンスの最適化

  • 解像度スケール: device.limits.maxTextureDimension2Dwindow.devicePixelRatio から動的にレンダリング解像度を決定。
  • ワークグループ数制御: バッテリー残量が 20% 以下ならワークグループサイズを半分にし、影響を抑える。
  • ユニフォームバッファの更新頻度: UI 操作時以外はフレームごとの更新を止める。
  • タイムスタンプ計測: WebGPU の writeTimestamp を使い、GPU 占有時間が 5ms 超でアラート。
const querySet = device.createQuerySet({ type: 'timestamp', count: 2 })
passEncoder.writeTimestamp(querySet, 0)
// ... render operations ...
passEncoder.writeTimestamp(querySet, 1)

アクセシビリティとフォールバック

prefers-reduced-motion: reduce を検出した場合、静的な PNG に差し替えます。

.hero-visual {
  background-image: url('/images/hero-static.png');
}

@media (prefers-reduced-motion: no-preference) {
  .hero-visual {
    background-image: none;
    canvas { display: block; }
  }
}

PWA のキャッシュに静的画像を登録し、オフライン時にも視覚が崩れないようにします。

モニタリングと AB テスト

performance-guardian を利用して以下の指標を追跡します。

指標目標アクション
GPU Frame Time< 6msワークグループサイズ調整
Battery Drain (5 分平均)< 2%解像度スケールを下げる
LCP 変動±100ms静的画像への切り替え検討
CTR+5%演出が効果的なら本番採用

チェックリスト

  • [ ] WebGPU 未対応環境にフォールバックが用意されている
  • [ ] 計算シェーダーとレンダーパスが分離されている
  • [ ] timestamp-query で GPU コストが可視化されている
  • [ ] prefers-reduced-motion を尊重している
  • [ ] Battery API で低電力モード時の品質劣化が制御されている
  • [ ] RUM で CTR と LCP が監視されている

まとめ

WebGPU を活用すれば、複雑なレンズ効果も軽量なシェーダーで実現できます。タイルベースの計算や解像度スケールを組み合わせ、ローパワーデバイスでも持続的に 60fps を保つ設計が重要です。演出を視覚的な魅力だけでなく、バッテリーや LCP への影響と合わせて計測することで、プロダクションでも安心して採用できる WebGPU 画像演出を構築しましょう。

関連記事

アニメーション

音声リアクティブなループアニメーション 2025 — サウンドデータで演出を同期

Web/アプリで音声入力に連動するループアニメーションを構築する手法。分析パイプライン、アクセシビリティ、パフォーマンス、QA までを実践的に紹介。

加工/効果

コンテキスト対応アンビエントエフェクト 2025 — 環境センシングとパフォーマンス上限の設計ガイド

環境光・音声・視線データを取り込んでWeb/アプリのアンビエントエフェクトを最適化する最新ワークフロー。ユーザー体験を損なわずに安全なガードレールを敷く方法を解説。

加工/効果

視線応答型ヒーロー画像最適化 2025 — Eye Tracking テレメトリでUIを瞬時に再構成

視線トラッキングデータを収集しながらヒーロー画像を即時最適化するワークフロー。計測基盤、推論モデル、コンプラ対応、A/Bテスト連携を詳細解説。

加工/効果

ホログラフィック環境エフェクト配光 2025 — 店舗Xスペースの没入演出制御

リアル店舗とバーチャル空間で同期するホログラフィック演出のための画像・光源オーケストレーション。センサー制御、演出プリセット、ガバナンスを総合的に設計する。

加工/効果

軽量パララックスとマイクロインタラクション 2025 — GPUフレンドリーな演出設計

Core Web Vitals を犠牲にせずリッチな画像演出を届けるためのパララックス・マイクロインタラクション実装ガイド。CSS/JS パターン、計測フレーム、A/B テスト方法を網羅。

アニメーション

シームレスループの作り方 2025 — GIF/WEBP/APNG の境界を消す実務

ループアニメの継ぎ目を目立たなくするための設計・合成・エンコードの手順。短尺UIとヒーロー演出で破綻を防ぎ、軽量に保つ。