Work
WebGPU Pixel Sort
- Role
- 個人開発
- Period
- 2025/11 — 2025/12
- Stack
- TypeScript
- Babylon.js
- WebGPU
- WGSL
- Vite
- Site
- https://qiita.com/Yohei_Suda/items/7f2356b1d82ee3dd361b
WebGPU Pixel Sort
動画と Webcam の入力に対して、動きのある領域だけに選択的にピクセルソートを掛けるリアルタイムエフェクト。WebGPU の Compute Shader でパイプラインを組み、GPU 上で Bitonic Sort を走らせて毎フレーム処理する。
Overview
- フレーム差分 + モルフォロジー で動き領域のマスクを生成
- 行ごとにマスクを走査してソート対象セグメントを抽出
- 各セグメントを並列で Bitonic Sort → ソート対象だけがピクセルソートされ、動画全体の構図は保たれる
- 全部 Compute Shader、最終 draw だけフルスクリーン quad
Pipeline
graph LR
Cam["Webcam"] --> FrameDiff["FrameDiffPass<br/>(輝度差 + EMA)"]
FrameDiff --> Mask["MaskTexture"]
Mask --> Morph["MorphologyPass<br/>(膨張 → 収縮)"]
Morph --> MaskBuf["MaskBuffer"]
MaskBuf --> SegExt["SegmentExtractPass<br/>(行走査)"]
SegExt --> Segments[/"Segment[]<br/>{row, start, length}"/]
Video["Video texture"] --> CopyFrame["CopyFramePass"]
CopyFrame --> OutTex["OutputTexture"]
Segments --> Sort["PixelSortPass<br/>(Bitonic Sort)"]
OutTex --> Sort
Sort --> Canvas["Canvas"]
Shaders
7 個の WGSL コンピュートシェーダをチェーン:
frameDiff.compute— 現在 / 前フレームの Rec.709 輝度差を取り、EMA で平滑化 + 閾値で二値化してマスク生成morphology.compute— 3×3 近傍の max/min で膨張・収縮。ノイズ除去と領域連結maskTextureToBuffer.compute— マスクをテクスチャ → バッファへ変換(次段はバッファ前提)segmentExtract.compute— 各行を左→右に走査、連続するマスク領域を{row, start, length}として atomic 付きで配列に書き出しcopyFrame.compute— 動画フレームを出力テクスチャへコピー(ソート前のベース)pixelSort.compute— セグメント単位で Bitonic Sort を実行。workgroup 共有メモリで輝度値を比較、閾値以上のみ対象copyStorageTexture.compute— モルフォロジー間で使うテクスチャ swap 用
Key Decisions
- Fragment ではなく Compute: fragment で pixel sort を書くと分岐・書き戻しが非効率。WebGL2 に compute は無いので WebGPU 前提
- 生 WebGPU ではなく Babylon.js 経由: 生 API は儀礼が多くリソース管理が重い。Babylon.js に寄せると実装量が大きく減る
- Bitonic Sort: 並列度が高く workgroup 共有メモリと相性が良い。セグメント長が可変なのでマスクしつつ sort
- アトミックなセグメント追加: 複数スレッドから同時にセグメントを書き込むので
atomic<u32>でインデックスを確保
Constraints
- WebGPU 対応ブラウザ必須(Chrome/Edge 113+、Safari 18+)
- セグメント最大数 262144(超過時は overflow フラグで警告)