はじめに
アニメーション SVG をレンダリングする方法は複数ある。別記事で書いた svg-rs では SMIL アニメーション評価の話をしたが、評価の結果得られたパスデータを実際にピクセルに変換する部分はまた別の話になる。
この記事では「評価済みのパスデータをどう画面に描画するか」に焦点を当てて、CPU ラスタライズと GPU ベクターラスタライズの手法を比較する。
SVG アニメーションで変化する属性
アニメーションで何が変化するかによって、レンダリングのコストが大きく変わる。
- パスジオメトリ (
d属性のモーフィング) — 再ラスタライズが必須。最もコストが高い - トランスフォーム (
translate,rotate,scale) — 行列の変更だけで済む場合がある - 色・不透明度 (
fill,opacity) — ジオメトリは変わらないのでフラグメント処理だけ
パスが毎フレーム変化するケースがいちばん厄介で、ここをどう捌くかが手法選択の分かれ目になる。
CPU ラスタライズ方式
もっとも素直なアプローチ。パスを CPU 側でピクセルに変換して、結果をテクスチャとして GPU にアップロードする。
SVG パース → フラット化 → CPU ラスタライズ → ピクセルバッファ → GPU テクスチャアップロード → 画面描画
代表的な実装:
- Skia (ソフトウェアバックエンド)
- Cairo
- resvg / tiny-skia
問題: 毎フレームのテクスチャアップロード
CPU ラスタライズ方式の最大の問題は、毎フレーム巨大なピクセルバッファを CPU → GPU に転送しないといけないこと。
- 1920x1080 RGBA = 約 8.3 MB/frame
- 60fps だと約 500 MB/s の帯域を消費する
- モバイルではメモリバス帯域の制約がさらに厳しい
加えて CPU-GPU 間の同期によるパイプラインストールも発生する。CPU がラスタライズを終えるまで GPU は待たないといけないし、GPU がテクスチャを使い終わるまで CPU は次のフレームのバッファを書けない。
静的な SVG やアイコン程度なら問題にならないが、フルスクリーンのアニメーション SVG を 60fps で回すとなるとこれがボトルネックになる。
GPU ベクターラスタライズという選択肢
ここで発想を変える。ピクセルバッファ (数 MB) を送る代わりに、パスの制御点データ (数 KB〜数十 KB) だけを GPU に送って、ラスタライズ自体を GPU 側でやればいい。
転送量が桁違いに減る。パスデータは制御点の座標列なので、フルスクリーンでも数十 KB 程度に収まることが多い。
GPU ベクターラスタライズの 3 つのアプローチ
Stencil-then-Cover
2 パスのアルゴリズムで、GPU のステンシルバッファを使ってパスのフィル領域を判定する。
- Stencil パス: アンカー点からパスの各辺への三角形ファンを描画し、ステンシルバッファの値をインクリメント/デクリメント
- Cover パス: パスのバウンディングボックスを描画し、ステンシルテストで実際のフィル領域だけ塗る
アンカー点
*
/|\
/ | \
/ | \
/ S | S \ S = ステンシル更新される三角形
/ T | T \
/ E | E \
/ N | N \
*-------+-------*
パスの辺
winding rule (even-odd / non-zero) がステンシル操作で自然に表現できるのが嬉しい。even-odd ならステンシル値の LSB をチェック、non-zero ならステンシル値が 0 でないかチェックするだけ。
代表的な実装:
NV_path_rendering(NVIDIA の OpenGL 拡張)- Pathfinder (Rust)
Pathfinder は浮動小数点テクスチャで台形カバレッジ計算をやっていて、256xAA 相当のアンチエイリアシングを実現している。
Tessellation ベース
パスをフラット化 (ベジェ曲線を直線列に近似) してから三角形メッシュに分割し、通常の GPU ラスタライザで描画する方式。
SVG パース → フラット化 → テッセレーション (三角形分割) → GPU 頂点バッファ → GPU ラスタライズ → 画面描画
代表的な実装:
- Lyon (Rust)
既存の GPU パイプラインにそのまま乗るので実装は比較的楽。ただし、パスが毎フレーム変わるアニメーションだと、毎フレーム CPU 上でテッセレーションをやり直す必要があるのがボトルネックになる。テッセレーション自体はそこそこ重い処理なので。
Compute Shader ベース
フラット化、ビニング (タイル分割)、ラスタライズの全工程を GPU の compute shader で実行する方式。CPU の仕事がほぼなくなる。
代表的な実装:
- Vello / piet-gpu (Rust, linebender プロジェクト)
Vello は Sparse Strips というアルゴリズムを使っていて、4x4 タイル単位で並列ラスタライズする。中間表現の保持 (retain) も可能で、変化がない部分の再計算をスキップできる。
解析的なアンチエイリアシングも GPU 上でやるので AA 品質も高い。
ただし WebGPU / Vulkan / Metal が必要で、WebGL 2 では動かない。環境要件がいちばん厳しいアプローチになる。
比較
| 観点 | CPU ラスタライズ | Stencil-then-Cover | Tessellation | Compute Shader |
|---|---|---|---|---|
| 転送量/frame | 数 MB | 数十 KB | 数十 KB | 数十 KB |
| CPU 負荷 | 高 | 低 | 中 (テッセレーション) | 最低 |
| AA 品質 | 高 (解析的) | MSAA 依存 | MSAA 依存 | 高 (解析的) |
| 実装の複雑性 | 低 | 中 | 中 | 高 |
| 環境要件 | なし | OpenGL | OpenGL | WebGPU / Vulkan / Metal |
選択指針
用途と環境によって最適な手法が変わる。
- 小さいアイコン・静的 SVG → CPU ラスタライズで十分。resvg / tiny-skia が手軽
- フルスクリーン・高フレームレートのアニメーション SVG → GPU ベクターラスタライズを検討する
- WebGPU 対応環境 → Vello (Compute Shader) が最もパフォーマンスが良い
- OpenGL 環境 → Pathfinder (Stencil-then-Cover)
- モバイルで互換性重視 → Lyon (Tessellation) + GPU 描画
個人的には Vello の Sparse Strips アプローチが将来的にいちばん筋が良いと思っている。ただ WebGPU の普及次第なところもあって、今すぐどこでも使えるわけではない。
まとめ
CPU ラスタライズはシンプルだけどテクスチャアップロードの帯域がボトルネックになる。GPU ベクターラスタライズならパスの制御点データだけ送ればいいので帯域効率が桁違いに良い。
Compute Shader ベースの Vello が最も将来性があるが、WebGPU / Vulkan / Metal が必要という環境制約がある。環境に応じて Stencil-then-Cover や Tessellation を選ぶのが現実的な判断になる。
SVG パーサーや SMIL 評価の話は svg-rs の記事 に書いた。