まぁ、前者に決まってるんですけどね(爆)
(公開当初、「擬似インスタンシングいいよオススメ」的な印象が強い記事になってしまっていたので、誤解を生まないよう、最後のまとめの論調を変えました。本物のジオメトリインスタンシングが使えないGL環境では試す価値はありますが、無条件でお薦めする技法ではないことをあらかじめ注意書きしておきます)
はじめに
皆さんこんにちは。
最初からオチ言って早くも話の展開に行き詰まった、エマ・デュランダルさんですよぉ(挨拶)
さて、皆さん。WebGL使ってますか? 中級者な方は、ANGLE_instanced_arrays拡張なんて使っちゃったりなんかして、けっこうブイブイ言わせちゃってるんじゃないのぉ? コノコノ!←
さて、ANGLE_instanced_array拡張によって、WebGLにおいても一般的になってきたジオメトリインスタンシングですが、
(ジオメトリインスタンシングなにそれおいしいの? という方はこちらのページをごらんください)
OpenGL系が今日のインスタンシング機能の獲得に至るまでには、いろいろな紆余曲折があったらしいですよ。
最初にジオメトリインスタンシングを実現したのはDirectX
最初にジオメトリインスタンシングをサポートしたのはDirectXです。Direct3D9の頃だったかな?
ワールド行列など、インスタンス固有の情報を前もってバッファとしてGPUに送っておき、CPU側ではドローコール一発で大量のインスタンスを描画できるというこの画期的な機能は、ゲームプログラマ達に熱烈に歓迎されました。
XBox360世代のゲーム機でやたら大量のオブジェクトが描画されるシーンがあったりしますが、これの恩恵は大きいと思います。
(とはいえ、ゲーム機の3DAPIの場合、そもそも描画コールが多くてもPCほど問題にならないケースが多かったりしますが)
で、一方のOpenGL。こっちの陣営がDirectXと同等のジオメトリインスタンシングをサポートするのには、少し時間がかかりました。だいぶ後になってからのことです。つまりそれまでの間、OpenGLがゲーム向け用途としては不利とみなされやすい時期が生まれました(まぁこのことだけが要因じゃないけど)。
まぁ、誤解を恐れず当時のDirectX陣営くんとOpenGL陣営くんにしゃべってもらうとですね。
DirectX陣営「ね、君、表示位置と色とかそれぞれ違うオブジェクト10万個出せる? 60FPSで。余裕だよね? これくらいできなきゃ3D APIとは言えないよねwww」
OpenGL陣営「ぐ、ぐぬぬ・・・」
おやおや、OpenGL陣営、いきなり窮地に立たされてしまいましたよ。ん、しかし様子が・・・。
OpenGL陣営「ふ、ふーんだ。悔しくなんかないもーん。OpenGLだって、頑張れば似たようなことができるんだもーん!」
つ、強がりを言い始めた! か、可愛いぞ、OpenGL陣営!(違)
DirectX陣営「頑張るってどう頑張るんだよ。我らが主、MSさんと違って、君らOpenGL.org1は規格の刷新がノロノロじゃないか。新しいAPIを作らずして、実現は難しいよねぇ?(ニヤニヤ」
……言われっぱなしやな。OpenGL陣営。
OpenGL陣営「制約からは創造が生まれる……」
お?
OpenGL陣営「DirectX陣営、君たちは忘れちゃいないか。各GPUメーカーも実はOpenGLにしっかりコミットしているということを……」
DirectX陣営「む…?」
OpenGL陣営「時は満ちた。今日は特別ゲストを紹介しよう……NVIDIAさん、どうぞ!」
NVIDIAさん、召喚!? 展開が早いな、なんか辺りが緑色になってきた気もするし。
NVIDIA「面白いアイデア考えターヨ。『擬似インスタンシング』、ッテユーヨ」
なんだ、この謎なキャラ付けは・・・。
本題に戻りまして...
いちいちh1相当の文字で茶番続けてもしょうがないんでここら辺で切り上げますが(爆)
こんな感じで、OpenGLがなかなかちゃんとしたインスタンシングAPIを提供できなかった間のつなぎとして、NVIDIAが考案したOpenGL向けのインスタンシング手法が「pseudo instancing(擬似インスタンシング)」です。
まだNVIDIAさんのHPからPDF資料がダウンロードできるようです。GL系に明るい方は、読めば仕組みがすぐにわかります。そんな難しくはありません。
これ、NVIDIAが自ら「pseudo(擬似)」と言っているだけあって、本当の意味でのジオメトリインスタンシングとは言いがたいところがあります。とはいえ、かなりの高速化をもたらしてくれました。
仕組みを説明しましょう。
まず、前提として
まず、インスタンス(インスタンシング描画で、描画する際のベースとなるメッシュがまずあるわけですが、これを複数描画した際のそれぞれの描画物のことを指します)を大量に描画する場合、まず最低でも位置はそれぞれ変化をつけたいですよね。だって全部同じ位置に重ねて表示したって面白くないですから。
なので、位置をインスタンスごとに変化させるためには、インスタンス分の変換行列が必要になります。
World行列、View行列、Projection行列。これらをCPU側で掛け合わせたWorldViewProjection行列です。
インスタンシング技法を一切使わない普通のやり方だと、この行列の2回の掛け算、そして計算後の行列のGPUへの設定コスト。これがモロにかかってきてしまいます。
工夫、擬似インスタンシングでもう一工夫
それを低減する工夫として、人によってはこんなことをやったりします。
World行列、View行列、Projection行列。この3つのうち、View行列とProjection行列はほとんどのケースにおいて、全てのインスタンスで不変なので、共通に使うことができます。
(View行列は、カメラが動いたりすればもちろん変化しますが、その場合でも変更の頻度は、多くのケースでは描画フレームごとになると思います。つまり、言い換えれば1フレームの中では不変というわけです)
であれば、View行列とProjection行列は、事前にGPUに送ってしまい、インスタンス毎に設定するのはやめる(インスタンスごとに設定するのはWorld行列のみにする)ことができます。
これで、CPU側の行列の掛け算のコストは無くなりました。(その代わり、GPU側で同様のことをすることになるんですが、まぁGPUの方が処理が高速ですから)。
で、NVIDIAの擬似インスタンシング技法では、さらにもう一工夫加えます。インスタンスごとのWorld行列の設定に、通常使うglUniformMatrix系の関数ではなく、固定的な頂点属性(glMultiTexcoord)としてWorld行列を設定しよう、というものです。
これは私もあまり知らなかったのですが、Uniform系の設定よりも、固定的な頂点属性(各頂点に指定する通常の頂点属性データとは意味が違います。これについては後述します)でのデータ設定の方が設定コストが低いってことらしいですね。
こうすることで、インスタンス毎のWorld行列の設定のコストも「低減」された。ここまでがNVIDIAさんの擬似ジオメトリインスタンシング技法によるボトルネック削減です。
でも、あれ? 描画命令の数は??
良い質問です。PDFをご覧になった方は気がつかれたかと思いますが、描画命令の数は実はこれっぽっちも減ってません!(爆) まぁ、この部分がNVIDIAが「擬似(pseudo)」と名付けた所以だと思うのですが…。
ただ、NVIDIAの資料が言うには、
「glDraw*系の描画関数はOpenGLの中でも非常に高速な部類だから(エマ注:ぉ、ぉぅ(´・ω・`))、ほとんど問題はないよ」
とのことです。
「見せてもらおうか、擬似インスタンシングの性能とやらを…」本当に速いんですかね? 擬似インスタンシングって
と、思う人もいますでしょ? だって、削減できたのはCPUでの行列の2回の掛け算と、World行列の設定コストの二つだけですよ? いかに「速い」とはいえ、描画コールはインスタンス毎に呼び出すんですよ?
当時、NVIDIA SDK 9.52とかにこの擬似インスタンシングの実働デモがありましてね。それを試した限りでは、確かにノーマル描画時よりも、結構パフォーマンスが改善されてました。
でもなー、自分で試してみないことには納得できんぞなもし。ってわけで、
長い前フリでしたー!
ここまで無駄に引っ張ってきてすみませんorz。ここから、エマさんが 大量のインスタンスの描画を
「通常描画版」「擬似インスタンシング版」「ANGLE_instanced_arrayによる本物インスタンシング版」
の3バージョンのデモをWebGLで実装して、パフォーマンス比較してみよう っていう企画が始まるわけですよ。ええ。
……すでにここまで読む前に途中で帰っちゃった人もいるよね。きっと。すまそw
で、実際の比較の前に、NVIDIAのPDF資料にある説明やサンプルコードは、かなり昔のOpenGL(glColorとかglVertexとかglTexcoordなどがあった時代)なので、API体系がWebGLと結構違います。その違いをどう乗り越えるのについて、ちょっとこれからエマさんが説明しますね。
古の時代の「擬似インスタンシング」を現在のWebGLで実装するには
別に実装方法が大きく変わるわけではありません。旧OpenGLでの関数を、現在のWebGLでそれらに機能的に対応するものに置き換えるだけです。
まず、NVIDIAのPDFのサンプルコードで使われていた、旧OpenGLのglMultiTexCoord4fvとかglColor4fvはどういう働きがあるのかというと、例えばglColor4fvなら、この関数を読んで色を指定した場合、それ以後ずっとその色でオブジェクトが描画されます。
glMultiTexCoord4fvは、マルチテクスチャのテクスチャ座標を設定するんですが、これも、一度呼んで設定したら、それ以後ずっとそのテクスチャ座標が有効になります。
今のWebGL時代では、通常、シェーダーでattribute変数を使うためには、まずVBOを用意して、そのattribute変数のレジスタ変数を指定してgl.enableVertexAttribArrayを読んで有効化し、そしてgl.vertexAttribPointerでVBOをアタッチします。これでようやくattribute変数にVBOの値が入ってくるわけですが…。
一方のこれら旧OpenGLのglColor4fvやglMultiTexCoord4fvは、いわばこのWebGLのシェーダのattribute変数に、(先ほどの通常のVBOと紐付ける手続きを完全にすっ飛ばして)直接固定値を割り当てるようなものです。
では、今のWebGLにはこうした旧OpenGLの関数と同様のものはないのでしょうか?
実はあります。あまり、使ったことのある方は少ないかもしれませんが。
それはgl.vertexAttrib1f
、gl.vertexAttrib2f
、gl.vertexAttrib3f
、gl.vertexAttrib4f
です(以後、これらをまとめてgl.vertexAttrib*
と呼ぶことにします)。
これらも、シェーダーのattribute変数に(VBOなどを使わずに)直接固定値を割り当てる関数です。
使い方は簡単。固定値を設定したい頂点attibute変数のレジスタインデックス値を、関数の第一引数に指定して、あとは残りの引数に固定値を設定するだけです。
なんと、gl.enableVertexAttribArrayで事前にattribute変数インデックスを有効化する必要すらありません(というか、むしろしてはいけません。有効化してあった場合はgl.disableVertexAttribArrayで無効化してください)。
これを使えば、glMultiTexCoord4fvやglColor4fvと同じことができますね。
(それにしても、昔のOpenGLはカラーとかテクスチャ座標とか、そういう代表的な属性用にいちいち専用の関数が用意されていたってわけですねぇ。今はより汎用志向になっているわけです。)
旧OpenGLからWebGLへの置き換えが必要なのは、これだけです。では早速実装してみましょう。
WebGLによる擬似インスタンシング・サンプルコード
サンプルコードを以下に示します。
いよいよベンチマーク!
さて、実装も完了したことですし。早速比較してみましょう。比較対象は3つ。通常レンダリング、擬似インスタンシング、本物インスタンシング(ANGLE_instanced_arrays)です。
ちなみに本物インスタンシングについては、この記事のアプローチ1の実装方法を使いました。
結果考察
立方体(Cube)を50000個または100万個描画した際のパフォーマンス比較になります。
バリエーションデータ更新あり、というのは、具体的にいうと各Cubeごとに毎フレーム変換行列を更新しています(Cubeを常時回転させるため)。
測定環境は:MacBook Pro (Retina, 15-inch, Mid 2015) (AMD Radeon R9 M370X 2048 MB)、Chrome 50.0.2661.94 (64-bit)
FPS | ノーマル | 擬似インスタンシング | ANGLE_instanced_arrays |
---|---|---|---|
バリエーションデータ更新あり。インスタンス数50000個 | 6.12 FPS | 25.48 FPS | 45.08 FPS |
バリエーションデータ更新なし。インスタンス数100万個 | 1.02 FPS | 1.57 FPS | 30.51 FPS |
うおお、こうしてみるとやっぱり速いぞ、本物インスタンシング(ANGLE_instanced_arrays)!
そして擬似インスタンシング、こちらも意外と健闘しているではありませんか!
どノーマルのに比べて1.5〜4倍程度は速く、工夫の効果はちゃんと出ているようです。
そして、本物インスタンシングはバリエーションデータを更新しない時は本当にべらぼうに速く、バリエーションデータを更新する時は、ノーマルや擬似インスタンシングとの差が(相対的に)縮まる傾向にあることもわかりました。
まぁ、CPUで頑張ってデータ更新作業しますからね。仕方ないでしょう。
ここを、うまくトランスフォームフィードバックやら何やら使って、GPGPUっぽくバリエーションデータを更新できれば、また違ってくるかもしれません。
いずれにしろ、実際の3Dアプリケーションにおいては、バリエーションデータを随時更新するケースの方が多いと思います。そのようなケースでは本物インスタンシングも(GPUでうまく更新しない限り)CPUによるバリエーションデータ更新が辛く、相対的に見ると、擬似インスタンシングでも実用になるケースは出てくるかもしれません。
追記
Win版のブラウザ(Chrome、Firefox。GPUはQuadro K2000D)でも試したところ、擬似インスタンシングの高速化効果が見られませんでした。
実は、Win版の各ブラウザのWebGL実装はANGLEという、WebGLの命令をDirectXに翻訳してDirectXで実際の3D処理を実行させるという、中間層が入っています。もしかしたら、このANGLEが介在していることで、(OpenGLドライバを駆動しているわけではないため)擬似インスタンシングの効果が出てこないのかもしれません。
→と思ったら、 @cx20 さんの報告では、Winブラウザでも擬似インスタンシングの効果出てるとの報告もあり、なんだかよくわかりませんねw
ちなみに、この擬似インスタンシングはNVIDIA社考案のテクニックではありますが、実際に試したところ(非Win環境において)、RADEONやiPhoneなどでも効果が出ています。一般的なOpenGL系環境であれば、多くのケースで使えるテクニックだと思って良さそうです。
まとめ:意外と健闘した擬似インスタンシング! だがしかし…。
はい、そんな結果になりました。
実際の描画コールや変換行列の設定の数が減っているわけではないにもかかわらず、この結果。
glDraw*系はNVIDIAの人が言う通り、意外と高速なのですね(もちろん、数は削減した方が良いことには変わりませんが)。
ノーマルの1.5〜4倍速いっていうなら、ケースによっては十分アリなのかもしれません。
と、は、い、え。現在はほぼすべてのブラウザでANGLE_instanced_arraysはサポートされているので、結局は擬似インスタンシング要らない子なんですけどね(*゚∀゚)ヴァハハノヽノヽノヽノ \ / \/ \
じゃあなんで試したのかというと、まぁWebGL(OpenGL)のいろんな関数自体のパフォーマンスをちょっと測ってみたかった、というのが動機です。あと、もし古い機種でジオメトリインスタンシング非対応のものがあったら、この擬似インスタンシングどうかなーと思ったのもあり…。
それにしても通常レンダリング時の数倍。古いGL環境なら十分試す価値ありでしょう。非常に得るものがあった検証でした。
あ、でもね。一応断っておきますが、上の追記でも書いた通り、
- この擬似インスタンシングは「擬似」というだけあり、描画コールや行列設定の数自体は全く削減できていない。
- World行列のみ渡すようにする & OpenGLの行列設定時に使う命令を軽い命令に換える、という部分のみで高速化している(OpenGLの内部仕様的・実装的な部分を利用したバッドノウハウに近い)
- Windows環境の多くのブラウザ(ANGLEに依存している)では、効果が出ないケースもある。
ということから、有効なノウハウではあるものの、「WebGLにおける正式な高速化手法として推奨されるべきものではない」かつ「高速化手法としては、本物のインスタンシング(ANGLE_instanced_arrays)に素直に道を譲って消えていくべき技法」であるともと言えると思います。
え? じゃあなぜ取り上げたのか? そりゃ奥さん。 勢いとネタ心に決まってるじゃないですか!!
ではみなさん、またお会いしましょう〜〜!(脱兎)
-
当時はまだ、Khronosが発足する前だったのです…。 ↩