ginokent Blog
RSS EN

アニメーション SVG のレンダリング手法比較: CPU ラスタライズ vs GPU ベクターラスタライズ

はじめに

アニメーション 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 のステンシルバッファを使ってパスのフィル領域を判定する。

  1. Stencil パス: アンカー点からパスの各辺への三角形ファンを描画し、ステンシルバッファの値をインクリメント/デクリメント
  2. 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-CoverTessellationCompute Shader
転送量/frame数 MB数十 KB数十 KB数十 KB
CPU 負荷中 (テッセレーション)最低
AA 品質高 (解析的)MSAA 依存MSAA 依存高 (解析的)
実装の複雑性
環境要件なしOpenGLOpenGLWebGPU / 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 の記事 に書いた。