オープンソースのメッシュ最適化ライブラリmeshoptimizerとメッシュ最適化の話題

はじめに

AMD Compressonator 3.0のリリースの際に,メッシュ最適化や圧縮機能が加わりました.
Compressonator V3.0 Release Brings Powerful New 3D Model Features
https://gpuopen.com/compressonator-v3-0-release-brings-powerful-new-3d-model-features/
メッシュの圧縮にはGoogleのDracoが入ったのですが,最適化はどういったライブラリをベースにしているのか気になって調べてみました.AMDであればTootleが長らく使われていたのですが,GPUOpenのスクリーンショットでTootleではなさそうで,自分が触ったことがあるライブラリの用語が出てきているので最適化部分のソースを見てみました.
https://github.com/GPUOpen-Tools/Compressonator/tree/master/Compressonator/Source/CMP_MeshOptimizer
どうやらCompressonatorではTootleを採用せずにmeshoptimizerを使用しているようですね.今回はmeshoptimizerを紹介しつつもう一度メッシュの最適化とは何かを簡単に復習したいと思います.
meshoptimizer
https://github.com/zeux/meshoptimizer

meshoptimizerについて

meshoptimizerはGPUで描画する3Dの三角形ポリゴンの最適化のためのライブラリですね.特に頂点とインデックスを処理するものになります.こうしたライブラリはAMDであればTootleなどがあります.使用して見た印象ではtootleに使い勝手が近く,すでにTootleを使用しているところは移行がしやすいと思います.

meshoptimizerの機能

meshoptimizerの最適化に関連する機能としては以下です.

  • Indexing
    • インデックスのない頂点データのインデックス化
  • Vertex cache optimization
    • 頂点キャッシュの最適化
  • Overdraw optimization
    • オーバードローの最適化
  • Vertex fetch optimization
    • 頂点フェッチ最適化
  • Vertex quantization
    • 頂点の量子化
  • (optional) Vertex/index buffer compression
    • 頂点,インデックスバッファの圧縮

このほかに三角形リストをストリップ化する機能もあります.現在は,インデックス付き三角形リストがGPUでレンダリングする上では効率的といわれています.meshoptimizerのページでは,ストリップはリストと比較して50%から60%程度インデックスを多く持ち,リスト(三角形当たり1.5から1.8のインデックスを持つケース)が最適化指標のACMR(後述)において~5%程度不利になるということが書かれています.
上記の最適化機能のうち,AMDのTootleではVertex cache optimization, Overdraw optimization, Vertex prefetch cache optimizationの3つがサポートされています.
さらに頂点について最適化度を評価する分析関数があります.この関数を最適化前と後にかけることで,どれだけ変わったかの比較ができます.なんとなく最適化ツールにかけておけばよいだろと思ったときに評価するものがないと実は元データはそれなりに最適化されていてやった意味がないわけですね.

  • meshopt_analyzeVertexCache
    • 頂点キャッシュの分析
  • meshopt_analyzeVertexFetch
    • 頂点フェッチの分析
  • meshopt_analyzeOverdraw
    • オーバードローの分析

meshoptimizerのサポートする最適化

ここからは各機能について解説をしていきます.

Indexing

meshoptimizerでは基本的にインデックスがあるものを最適化するのですが,元データがインデックスがない場合にはインデックスを生成して重複頂点を削除する関数があります.

Vertex cache optimization

頂点キャッシュの最適化は,インデックスを頂点キャッシュを効果的に使うように並べ替えます.この時,頂点データの方は並べ替えはしません.
GPUは,ベンダーごとに16~64頂点の単位で頂点を処理します.この時に,1つの単位で頂点キャッシュが効くので複数の三角形で共有する頂点がある場合には,キャッシュの恩恵を受けます.この最適化はmeshopt_optimizeVertexCache関数に並び替え前のインデックスとインデックスカウント,頂点を入れると並び替えたインデックスを返してくれます.

Overdraw optimization

オーバードローに対する最適化です.オーバードローは,1つのピクセルがカメラから見て対して三角形が重なりあった場合にピクセルシェーダが複数回呼び出されるオーバードロー状態になります.深深度テストが適切に行われる構造であればオーバードローが抑制できますが,適切に行われないデータの場合,この最適化が有効です.
この最適化はピクセルシェーダの負荷が高いケースで,シェーダの呼び出しを抑制することで負荷を軽くすることを目的に行います.複雑なシェーダを適用するものに効果が高いと思います.
オーバードローの最適化を行うにはインデックスのほかに頂点座標を渡す必要があります.この最適化アルゴリズムは頂点キャッシュの効率とのバランスをとるための係数を与えることができます.

Vertex fetch optimization

頂点フェッチの最適化は,頂点バッファを並び替えてメモリアクセスの効率化を行います.GPUは頂点シェーダを実行する前に頂点バッファから頂点属性をフェッチしますが,その際のメモリアクセスの最適化をします.
最適な頂点データの配置はインデックスをもとに決めるためある最適化されたインデックスバッファにて行うとよいようです.
この最適化の注意は,頂点データの方の並び替えが発生する点にあります.たとえば,BlendShapeなどを使う場合,頂点データの並びが変わるとモーフターゲットとベースメッシュの頂点のインデックスがマッチしなくなるかもしれません.

Vertex quantization

頂点の量子化は頂点データのメモリの帯域幅を削減する最適化です.頂点データに必要なメモリを削減する効果もあります.
この最適化は,頂点データの中の座標や法線などを小さいサイズにします.例えば,頂点法線は一般的にXYZの3Dのベクトルで持ちますが,XYZの各要素はfloatで持つことがあると思います.量子化ではこれを10bitにエンコードしてしまうことで10:10:10:2の32bitをに入れてしまいます.こうすることで96bit(float x 3)だったものが32bitになります.
法線の精度が下がるとシェーディングの落ちるケースがありますが,モバイルでは精度よりパフォーマンスを取りたいというケースがありますので有効なことがあるかと思います.
他に量子化が可能な頂点要素だと頂点座標ですが,これは各要素を32bit floatから16bit shortにしてしまうということもありうるかと思います.
量子化はモバイルで有利と書きましたが,通信でデータを取得してブラウザでレンダリングをするWebGLなどでもありがたいかもしれませんね.

(optional) Vertex/index buffer compression

この最適化は,頂点バッファとインデックスバッファ自体を圧縮することでデータを小さくします.これはGPU上での最適化というよりはストレージサイズやローディングなどのケースで有効なものかと思います.
この圧縮はoptionになっていますが,使用するにはいくつか条件があるようです.頂点に関しては,頂点バッファが頂点フェッチ最適化がされ,頂点データが量子化されていることが条件のようです.インデックスに関しては,頂点/インデックスバッファが頂点キャッシュおよび頂点フェッチの最適化がされている必要があり,そうでないケースは圧縮率が下がるようです.
なお,この圧縮に加えてzstdなどでさらに圧縮ということも有効なようです.冗長性を考慮している分,圧縮前のデータからかけるよりも有効に機能するようです.
デコード機能は,1~2GB/s程度で動作し,16bitインデックスバッファであれば5~6倍程度は圧縮できるそうです.これにさらに汎用的な圧縮アルゴリズムを適用することで圧縮率を高められるようです.
圧縮といえばGoogleのDracoがありますが,このことについても触れれています.Dracoでは自分たち独自の量子化などを行った頂点データの圧縮などは不向きなようで,Dracoで圧縮したものを展開してから量子化では処理時間がかかるので,meshoptimizerでの量子化を行うケースではこの圧縮が相性がいいようです.

最適化の分析関数

つづいて最適化の分析関数について見ていきます.最適化の分析関数は,最適化前と後のインデックスにかけてみることでmeshoptimizerの最適化が効果あったか比較するのに使えます.

meshopt_analyzeVertexCache

頂点キャッシュの最適化具合を測ります.キャッシュの最適化はACMRとATVR2つの指標があります.
ACMRは,Average cache miss ratioということで平均キャッシュミス率ということでワーストケースでは3という数字になり,0.5に近いほど良いケースということだそうです.インデックスバッファから三角形を呼び出す際に,各頂点がキャッシュからどれだけ呼び出せるかが,3頂点ともキャッシュにないケースが非効率なケースですね.だいたい,0.5 – 1.5の範囲に収まるのが一般的なケースのようですね.
ATVRCは,Average transformed vertex rationになり総頂点に対してどれだけ頂点シェーダが呼び出されるかの比率になります.1.0が最良のケースだそうです.1つの頂点が頂点シェーダから複数呼ばれるケースというのはインデックスで同じ頂点が何度もあるケースですね.

meshopt_analyzeVertexFetch

頂点フェッチの統計を返す関数です.最善のケースだと1.0を返し,大きくなるほど効率が悪いといえます.この統計は,頂点バッファから読み込んだバイト数と頂点バッファの合計サイズの比ということになります.

meshopt_analyzeOverdraw

オーバードローの統計を取る関数です.この分析は正射影でいくつかの視点で描画します.その際に実際にピクセルシェーダが呼び出される数の比率です.1.0が最善のケースで数値が大きいほどオーバードローが多いということになります.

おわりに

meshoptimizerの機能を見ていくことで頂点データを最適化するアプローチというものが見えてきたかと思います.
今回取り扱った話題の多くは,Xbox 360やPlayStation 3など本格的にGPUを使った開発をする時代にはよく知られていた最適化の基本であったのですが,最近はこうした初歩的な最適化の知識は開発者にとって当たり前のものということなのか読める資料が少なくなってきました.一方で,若い開発者はそうした知識を学ぶための資料がないことで学ぶのが難しくなりました.そういう点では今回は頂点データの最適化の基礎を復習するにはちょうど良い題材でした.
 
実践に関しては,meshoptimizerは比較的組み込みやすいライブラリではありますが,AMDのCompressonatorは分析して表示する機能もありますので,そこから使用してみるとわかりやすいと思います.