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.maxTextureDimension2D
とwindow.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とヒーロー演出で破綻を防ぎ、軽量に保つ。