■ 1回ドローコールで複数のモデルをインスタンシング表示
今回のマージドメッシュインスタンシングの例

今回は複数のメッシュモデルをインスタンシングで同時に表示する つまり1ドローコールで何種類ものモデルを描画する方法ですね。
マージドメッシュ(MergedMesh)という呼び方があります。 インスタンシングによるバッチ処理も似たような効果ですがそちらはモデル自体をそのまま結合する方法なのである程度のデータの塊に限定されます。 一つのモデルが65536ポリゴンまで表示可能なこの方法とは少し異なります。
簡単にインスタンシング処理を説明するとCPUとGPUのメモリは分かれているのでメッシュデータはバスを通してGPUに毎回転送することになります。 転送済のデータのシャドーコピーをインスタンスとして複製表示することで描画処理を向上させるのが、メッシュのインスタンシングです。
複数のメッシュモデルはスタティックバッチやダイナミックバッチのような実行時にある程度の塊に結合して転送する方法があります GPUに一度に転送できるサイズは上限があるため複数回に分けて転送が行われますが。その回数分データ転送の処理時間がロスするため実行速度が低下することになります。
今回の方法ではメッシュ自体に形状をもたないトライアングルの塊を転送してGPUの側で復元するためどのような形状でもインスタンシング可能でメモリが許す限り何種類のメッシュでも1ドローコールの表示が可能です。
ちなみに画像の右上のstatではドローコールは2となっていますがこれは間違っていると思います 。表示物がないシーンでも
標準のライトパス:1 + シャドーパス:1 + カスケード:3 =5パス
のドローコールが必要なのでインスタンス表示自体のどローコールは1ですが 実際にはStatのドローコールは6程度になるはずです。がそれでも大分少ない描画負荷ですね。 カスケードというのはカメラの距離に応じて手前から奥に向かって3回シャドーのLODなどのレンダリングパスです
■マージドメッシュインスタンシングのあらまし
■fig1
fig1は実装例の画像です。同じような解説を繰り返しますが 図のように背景のレベルに同様のモデルが複数存在するような場合に インスタンシングが有効ですが、 インスタンシング描画とは一度GPU側に送ったメッシュデータをコピーして使いまわすことで描画速度を向上させる技術です、 メッシュモデルはそのままだとモデルオブジェクト一つにつき1ドローコールが発生しますが、それを軽減する技術がご存知のようにバッチ処理と呼ばれる複数モデルを結合転送する方法です。GPUに一度に送れる上限が65536ポリゴンまでなので、それ以上のメッシュサイズの場合はある程度何度かに分けて転送されます 分割された結合メッシュの個数分はドローコールが発生してしまうということになります。
そこで何度も描画される形状メッシュモデルをインスタンシング描画することで描画処理の速度を向上することができます。 ただし通常であればインスタンシングはセットされたメッシュモデル単体をコピーするのみなので、メッシュモデルの群衆を描画した場合でもすべて同じモデルが表示されてしまい、複数のモデル種類を表示する場合はモデルデータごと何度かインスタンシングのドローコールが発生してしまうためある程度までのモデル種類までは表示できますが 表現的には若干単調になってしまうかもしれません。 もちろんデスクトップPCなどのパワーの有るハードでであれば多少のドローコールは問題ないですが。ゲームの場合必ずしも高性能ハードで実行されるわけではないので少しでも余力を残したいところです。
■実装例:SIGRAPH2012より
■fig2
■fig3
fig3図のように メッシュインデックスはパックしたモデルデータにパックした順番にIDを割り付けます。 それらIDが示すのは頂点データ配列のインデックス <頂点座標、ノーマル、UV>などが格納されたデータの先頭インデックスを呼び出します。
それにより元のメッシュの頂点データが配列内の頂点データに置き換わり 見かけ上別のメッシュモデルを表示することができるという仕組みになります。
■マージドメッシュのポイントスプライト実装例
■fig4
マージドメッシュを使用した基本コード例としてパーティクルのポイントスプライトが挙げられます。 GL系ではPointSprite命令をPontSizeというポイントに大きさを与える命令で実装しているコードを見かけますが。 HLSLシェーダの場合はマージドメッシュを使用してビルボードポリゴンを描画するという スニペットと呼んでよいのかな このような手法で実装されることが多いようです。
fig4のコードを簡単に解説します
頂点バッファにはこの場合Quad(四角形メッシュ)単位でデータが入力されるのでなので連続した4頂点が四角形ポリゴンの1セットとなります。 入力の id:SV_VERTEXID は頂点シェーダに入力される連続した頂点列に0から順番に頂点数分の割り付けられたIDで
uint particleIndex = id / 4; パーティクルのインデクスは四角形メッシュの先頭を4で割り算して求め。 uint VertexInQuad =id % 4; 4で割ったあまりが各四角形メッシュのサブIDで0,1,2,3 頂点順の番号を取得できます。 position.x = (vertexInQuad % 2 ) ? 1.0 : ?1.0; 四角形メッシュの4スミの座標を決定します。 position.y = (vertexInQuad % 2 ) ? -1.0 : 1.0; position.z = 0.0;
このように入力頂点データから 四角形メッシュを復元します。 入力頂点座標データはどんな値が入ってきてもよいのですが、とりあえず 0で初期化されて入ってくると仮定します。 Position = mul(Position , (float3x3)g_mInvView ) + g_bufPosColor[ ParticleIndex ].Pos.xyz;
g_bufPosColor[ ParticleIndex ].Pos.xyz はパーティクルIDごとに座標データが帰るので、 Positionに加算することでパーティクルがバッファーの値の座標に移動します。バッファーはパーティクルのPosition座標が計算されて入力された配列です。 position.xy *= PARTICLE_RADIUS; パーティクル大きさにサイズを成分を掛けて 最後に頂点にMVP(プロジェクション行列)を掛けてワールド上のカメラからの見かけ位置に配置するとパーティクルビルボードが完成します いつもの演算ですね。
|
■シェーダスクリプトの実装
頂点IDを取得する場合 HLSLでは標準では以下のようになります
v2f vert (uint id : SV_VertexID, uint inst : SV_InstanceID){} uint id : SV_VertexID : 頂点IDは入力されたメッシュモデルの頂点番号順に割り付けられるID uint inst : SV_InstanceID: インスタンシングされたメッシュモデルごとに割り付けられるID idデータを別のシェーダ内で使いまわしたい場合はPOSITIONデータなど同じく構造体に含める必要があります。 |
Unityヘルパー関数があるので以下のように
UNITY_VERTEX_INPUT_INSTANCE_ID :頂点IDの取得 UNITY_SETUP_INSTANCE_ID(v); :インスタンスID の取得
UNITY_TRANSFER_INSTANCE_ID(v, i);
unityマクロでTRANSFERの名称がついている場合は構造体に含める作業を省略できます。 |
書き方はどちらでもかまわないです。Unity以外の既存のシェーダを書き換える場合などに参考にはなります。
fig2で vertex頂点の処理をfor命令で表現していますが、シェーダを入力されたデータ数だけ処理を実行するFOREACHループ
と捉えると、理解しやすいかもしれません。
vertexシェーダは入力頂点数分のループで、fragmentシェーダはスクリーン上のピクセル数分のループと考えてください。
トライアングルメッシュデータを今回は Graphics.DrawProcedural( ) 命令で転送しますこれはMeshTopology.Trianglesか MeshTopology.Quadsのようにストリップの方法を指定してインスタンス描画する命令です。
Quads(四角形ポリゴン)は内部的にはTriangles(三角形ポリゴン)が2回記述するのと同義なので、Triangles(三角形ポリゴン)だけを対応します。
シークエンスは以下のようになります
- 複数のメッシュモデルからVertexデータを取得してストラクチャバッファないしテクスチャバッファにセットする。データはすべてを連続して配列にまとめて、それぞれのメッシュモデルのトライアングル数をシェーダに配列で渡す。インデックスIDはトライアングル数と対応させておく。
- インスタンシングには初期化された大きさのないトライアングルメッシュをセットする トライアングルの全体数は描画するメッシュモデルのポリゴン数をカバーできる大きさに設定する。つまりGPUに一度に転送できる最大ポリゴン数65536であればロスはあるが再設定の必要なくすべてのモデルデータのポリゴンサイズがカバーできることになる。
- 頂点シェーダで描画する際にSV_VertexIDをもとにしてバッファ(ストラクチャバッファ)から対応する頂点データを呼び出し頂点データを置き換える。 これによりポリゴントライアングルが大きさやノーマルなど属性を与えられ描画される。今回は頂点データをトライアングルに1体1対応させているので、データサイズは多少大きくなるがポイントスプライト例のようなVertexIDを割り算するような対応は必要ない。
- インスタンシング命令の描画時に設定したモデルのポリゴン数を上回った余分なポリゴンメッシュを描画スキップさせる。 頂点シェーダにはフラグメントシェーダのようにスキップ命令がないので、ポリゴンにスケール0を代入するか頂点座標にfloat3(0、0、0)を代入するまたは頂点アルファで非表示にするなどの方法が考えられる。
欠点というか注意点は
- ドローコールを抑えるにはマテリアルは基本的に1種類なので同じようなマテリアルを使用するモデル同士で結合する必要がある。
- メッシュ結合時に無駄なメッシュ部分をなるべく減らすために大体同じようなサイズのメッシュでまとめる必要がある。
■配列の転送
気をつけるポイントは 繰り返しますが
- なるべく同じポリゴンサイズのメッシュでまとめて表示することでデータのロスを減らすこと
- 偶数倍のサイズを指定すること :固定サイズの倍数を指定するだけでデータの先頭インデックスを計算できます。 条件分岐を発生させないため演算処理は向上します。
下のように先頭インデックスの計算は必要となりますが、データの終了判定が必要なくなる分演算が軽くなります。ハードのスペックにもよりますけど・フレームレートで比較してもらうと条件分岐の処理がありなしで結果がだいぶ変わることがわかるかと思います。
id_offset = _SegmentsID[inst]* _MaxLength UNITY_BRANCH if (id < id_offset + _SubDataSize[_SegmentsID[inst]])
? id_offset = _SegmentsID[inst]* _MaxLength パディング(詰め物)でサイズを調整することでIF分岐の記述が必要なくなり処理速度の最適化につながります。 |
ちなみに if分岐で 比較時に(uint)型id と int型のindexで型エラーが生じると思いますので、速度は uint<int< float ですのでこの場合はuintをint型にキャストしてください
あと掲載するまでもないと思うのですが。 List<> 操作ですね STRUCTを使用する場合とCLASSを使用する場合がありますが
配列の大きさが不定の場合使用すると操作が楽になりますね 一応CLASS多用はメモリ分断が起きやすいのガベージコレクション上の問題があるという最適化の点から構造体でLIST操作をするのが良いかもしれません。 LIST型と配列は相互に変換できますのでケースに応じてします。
とりあえず初心者向けな解説
using System.Collections.Generic; ...
または List string[] stringArray = stringList.ToArray(); |
■分岐命令のマクロ
Unityはif分岐で UNITY_BRANCH UNITY_FLATTEN のマクロ命令が定義されていますが これは HLSLのBRANCHとFLATTENを置き換えるだけのマクロ命令なので
- GLには作用しません
- #if はUnityのマルチコンパイルのマクロですのでこれにも作用しません
BRANCH オプションは分岐の片方だけを実行して評価するオプションでマルチコンパイルみたいなものでHLSLの場合にのみ有効。
FLATTENは分岐の両側を両サイドを実行して一方の値を選択します。 旧式のGPUで採用されている分岐がFLATTEN型なので処理が重かった
ということですが、 FLATTENの用途としてはif分岐でどちらか一方のみで定義された値がもう一方で参照されてもエラーが起こらない ということですが あまり使い所が思いつかないですね。
BRANCHは本家のフォーラムだとあまり期待するなというアドバイスもありましたが。害がないのでとりあえず書いておけばいいんじゃないかなと
■シェーダーの配列サイズ
モデルの種類にIDを指定する配列はコンスタントバッファで指定しています。IDをは入れるインデックスを指定すためにint型で指定しますがUnityでSetIntArrayはサポートされていない様です。しかし SetFloatArrayで代用できるようです コンスタントバッファにInt型で配列を確保して スクリプトから渡すデータはフロート型でシェーダ内はInt型でデータが渡るようなのでこれでいきます。
以前も書きましたが配列はシェーダ内で特に指定をしない場合はコンスタントバッファ扱いのようです コンスタントバッファは1024バイトのストライド長なので 。モデルの種類サイズが大きくなるようであればストラクチャバッファなど大きなサイズの配列を確保してください。もっともそんなに何種類も同時に表示する場面はなさそうなのですが。
シェーダでは配列の動的サイズは確保できませんので、マルチコンパイルオプションでキーワードをセットして
スクリプト側: Shader.EnableKeyword("MYDEFINE")
#endif |
今回の場合例えば次のように#defineで配列を初期化するようにしておけばスクリプト側から配列サイズを制御する。
#define ARRAY_SIZE128 #define ARRAY_SIZE256 #define ARRAY_SIZE512 #define ARRAY_SIZE1024 ※シェーダ内部からならKeywordを使用して#defineで書きますか
////////////////////////////////////////////////////////////////////////// #if defined(ARRAY_SIZE128 ) float _Array[128]; #endif #if defined(ARRAY_SIZE256 ) float _Array[256]; #endif #if defined(ARRAY_SIZE512 ) float _Array[512]; #endif #if defined(ARRAY_SIZE1024) float _Array[1024]; #endif この書き方で多分大丈夫だと思いますが SSAOシェーダを参考にすると良いかも。 |
■構造体を定義する場合のTips的なもの
一つの構造体にパックする変数のサイズにも制限があって DirectXは128ビットを一単位として扱うそうで、例えばfloat4型などが転送の最小単位となるようです。 struct構造体単位が128ビットの倍数でパックされるようにしないと 足りないビット分は構造体の次の要素の一部からをセットで転送しようとするため転送ロスが発生するということだそうです。 場合によっては30%程度の速度低下が生じることがあるそうで、公開されてるコードで確認するとだいたいそうならないように最適化されていると思います。
|
構造体を設計する場合に128ビットの倍数に足りない分は必要がなくてもfloat4型に定義するか、またはダミーのパディングデータを含めておくことで転送の速度ロスが軽減できるということです。 もちろん実行速度が十分なら気を使う必要はないですよ 既存コードを参考にしたときにデータを軽くしようとしてうっかり最適化部分を削ってしまうことはありそうなので 知識として持っておく程度でかまいません。
|
■テクスチャのアトラス化
モデル単位で複数のテクスチャ指定があると条件分岐が必要になるため 処理負荷軽減のためにもアトラス化が必要になります。 他の理由としてテクスチャ選択に条件分岐があると描画が崩れる場合があったのですが原因はまだわかりません。
スクリプトでメッシュを配列にパックする段階でアトラス化されたUVが指定されるようにすればどのようにUVがアトラス化されていても構いません。あらかじめUVがスクリプトのUV座標の計算部分はスルーして元のモデルからコピーするだけです。
■LOD対応
メッシュLODはインスタンシングでオプションパラメータがサポートされていますが 今回の方法の場合はLODモデルを一緒にメッシュ配列にパックしカメラからの距離を参照してLODメッシュに割り付けたIDで選択するようにすれば良いと思います。
通常のインスタンシングの場合pragmaを記述するとlodに自動で対応できます。
#pragma instancing_options lodfade
■カリング対応
テセレーションシェーダからの抜粋ですが unity_CameraWorldClipPlanes[planeIndex] とトライアングル頂点の内積(Dot)をとることで トライアングルメッシュがスクリーンの内側にあるかの判定を行い 3頂点とも外部にあれば描画をスキップするようにすることでポリゴン単位でのカリングができます。 トライアングルがスクリーンを覆うように大きく表示される場合もあるので、重心で判定などの手抜きは突然欠けたりで危険ですね。
bool TriangleIsBelowClipPlane ( float3 p0, float3 p1, float3 p2, int planeIndex ) {
float4 plane = unity_CameraWorldClipPlanes[planeIndex]; return dot(float4(p0, 1), plane) < 0 && dot(float4(p1, 1), plane) < 0 && dot(float4(p2, 1), plane) < 0; } |
モデル単位であれば、バウンディングボックスにモデルごとにトランスフォーム行列を掛けて 同様にスクリーンの内部にあるか判定をすれば良いと思います。 できる限りシェーダーでできることはスクリプトで書くのを避けたほうが良いと思います。
■コリジョンチェック
基本的にはシェーダ側の処理でできることはシェーダ側で済ませないと行けません CPU側からデータを触るとデータの移動が発生するためせっかく描画処理が向上してもそこで処理速度の低下を招きますので、セットするデータはなるべく小さく、できれば数フレームに一回で済むようにしたほうが良いです。シェーダ側のほうがCPUに比べて数十倍実行速度が速いので頻繁に値が書き換わるとデータ取得がコケる。調整すれば使えるかな。という範囲です。
プレイヤーの座標データをシェーダ側にセットすればインスタンスとlength()関数を使用して距離が計測できるので、最短距離または距離を近い順に並べた配列のインスタンスIDをシェーダ側の変数にセットする。プレイヤー座標とインスタンスの座標の方向が取得したい場合はベクトルもセットしておくことで、スクリプト側でGetを使用して変数の値を呼び出せばプレイヤーのヒット情報と状態が取得できます。 描画とはシェーダを分けて書いても良いし 描画数が多ければコンピュートシェーダが有効です。 AIのからみになると背景とのヒットチェックはDepthテクスチャを使用して計算するか または 上方からコリジョンを2Dレンダリングした背景画像を使用するなどで対応はできます。 スワップバッファを用いたスワップチェーン(シェーダで計算結果をレンダテクスチャで連続で入れ替える方法 パーティクル描画などで使いますね)かコンピュートシェーダを使用する方法になると思いますが NVIDIAには無いサンプルでもAMDかINTELのサンプルデモにはあったかと思います。
■コード・インスペクタなど サンプル
- インスペクタの表示
■SAMPLE CODE A: メッシュ配列にパディングを追加しているバージョン
"ProceduralMeshIstancing.cs”
using UnityEngine;
|
"DX11 MergedInstancing.shader”
Shader "DX11 MergedInstancing" {
|
■SAMPLE CODE B: メッシュデータを配列にパディング無しで詰めたバージョン
"ProceduralMeshIstancing2.cs”
|
"DX11 MergedInstancing.shader2”
|
■その他 今後の展開など
メッシュモデルを何種類表示しても 基本ドローコールは1となりますが 描画処理の負荷はかかるのでFPSは安定して高いというわけにはいきません。 樹木や、建物、プロップ類、ガレキ、キャラクタ あるいはエフェクト類などでまとめられるものを一度に表示することで、描画の負担は軽減できますのでアイデア次第では使えるテクニックです。
今回のサンプルはMeshFilterのみで作成しました。スクリプト部分は難しいところはないので 自分が使いやすいメッシュ結合の スクリプトを書き換えれば良いです。メッシュ結合はあまり処理が早いとはいえないので ゲームに実装する場合はプリプロセッサでメッシュにかためてプレファブ化するかasset化するかして 各メッシュのトライアングル数だけテーブルでシェーダに送れるようにします。
コードを読めばわかりますがトライアングルのデータに合わせて直接メッシュの頂点データを埋め込んでいます。 頂点データは重複しますが余分な計算が入らないので、スタティックメッシュなら多少データ量が増えた場合でも速度面で恩恵があると思います。頂点アニメーションを考慮するならば、頂点データを頂点へのインデックスIDで間接的に指定するほうが、一手間かかりますがアニメーション部分で演算量は減ります。
今回のサンプルコードで構造体の頂点カラーは未使用なのですが、頂点カラー部分に頂点へのインデックスIDを入れられますので インスタンスメッシュに更にサブメッシュを制御したい場合。 例えばキャラクターのメッシュに装備がついている場合や背景にオプション物がある場合などに表示非表示の制御 あるいは部分的に動きを与えるという際にはフラグとして活用することができますので予約として記述してあります。 頂点カラーでなくてもUV2など余っているところに自由にセットしてももちろんかまいません。
全体が65536ポリゴン以下で収まってしまう場合はバッチ結合とどうように、原点で複数メッシュモデルをマージして なんらかの方法(たとえば頂点カラー)でIDを指定して表示をコントロールすれば、同様に見かけ上の複数モデルの同時表示は可能です。 パーティクルと組み合わせる場合はこうした方法でも良いかもしれません。
最近はテクスチャ配列を使用した頂点アニメーションがクローズアップ されていますがわりと昔からある手法で確か2009年のGPUGEMsのインスタンシング記事には登場していました。 以前テクスチャ配列の記事を書いたときは、そこら辺につなげようとしていたのですが、まだUnityのインスタンシング周りが未完成だったため断念した経緯がありました ちょっと手を出すのが早すぎましたね 大分時間が経ってから記事のアクセスが伸び始めましたし。 テクスチャ配列のアニメーションはシンプルなため処理は軽いですが、頂点数が増加するとテクスチャサイズが肥大化するので、いまのところあまり大きなサイズのメッシュには適用できないという弱点もあります。65536頂点なら256*256テクスチャがアニメーションフレーム数分 ノーマルなどの要素もベイクするとさらにその数倍のテクスチャメモリが必要となります。キーフレームを削減するうまい方法ができれば解決できそうですが。 ボーンマトリクスだけベイクしてスキンウェイトシェーダを自前で書く skinweight対応したシェーダは公開されていますかね。あるいはローポリゴンのプロキシからハイメッシュデータに移し替えるような方法であればメモリの少ない低スペックハードにも恩恵がありそうです。 もっともロースペックマシンでそこまで無理する必要があるのかは、わかりません。
時間が許せばその辺も解説するかもしれませんが
■リファレンスリンク
- https://www.slideshare.net/DevCentralAMD/vertex-shader-tricks-bill-bilodeau
- https://forum.unity.com/threads/correct-use-of-unity_branch.476804/
- http://catlikecoding.com/unity/tutorials/rendering/part-19/
- https://forum.unity.com/threads/graphics-drawprocedural-meshtopology-quads-working-as-triangles.491007/
- https://forum.unity.com/threads/shaders-and-the-mystery-of-multitude-of-different-conditional-define-checks.319112/
■後記
更新間隔がだいぶ空いてしまっているので、更新のついでに感想を
Unityの記事は何故かゲームメーカに限らずメーカーさんのアクセスが多いのですが、Unityの記事が比較的初心者ユーザが読むことを前提に書かれているため コードを読めばデータの流れから意味が理解できるが、数式が苦手。というゲーム開発者さんが理解しやすく Unityエンジン以外でも参考になると そういう理由だと聞きました。 なんだか責任重大ですね
数学苦手なプログラマさんとか意外と多いんですけどね(笑
最近は特許関係でいろいろ話もありました アレとかアレなど。 関連した話をしますと 海外では新し目の技術にはなるべく特許を取らないようにして 業界の発展を促そうという思想があるようで、ネット検索すると例えばシェーダーやAIや物理演算やらはソースがほとんどオープンになっていたりするのがわかるかと思います。 そこで特許の代わりになるものが、サイト記事やコードの中に貼られているリファレンスリンクというもので、製作時に参考にしたもの PDFやスライド。サイト、ブログ記事。書籍など できる限り記載することで学習効果をお互いに高めていきましょうという狙いがあるそうです。 国内の現状を見ればお互い権利主張した結果 開発は全体にブレーキがかかって衰退ムードになっていったというのがわかるのでは。 もちろん海外でもすべての新しめのコードやアイデアに特許がないわけではないので侵害してしまう場合もあります 例えば個人ライセンスを記載ミスでオープンにしたりといったような事故も起きることがあるそうなので、問題が起きないため 使用する側も権利侵害に抵触しないか調べる手間が省けるメリットもあるので、なるべくリファレンスの記載をするように心がけるのはマナーとして正しいような気がします。 罰則があるわけではないのであくまでマナーの範疇ですが。 ほとんどリファレンスリンクがないサイトもあるので判断に迷うこともあるかもしれませんが、初心者率の高めなUnityユーザーは後から続く開発者のためにも迷ったらとりあえずリファレンスリンクは貼ってくれるとうれしいかな と思います。
と言うことで今回は、ここまでです。 毎度長文で申し訳ないですが読んでいただいて有難うございました。
※git.gistのHighlighterを前回使用してみたのですが、表示のたびにサイトを呼び出すため重たいそうで、検索したところ評判があまり良くないようです。 今回は見送りましたが次回からシンタックスハイライトをgoogle code prettifyなど JSコードをサイトに埋め込むほうが表示が軽くなるようなので、そちらで対応しようかと考えています。ブログがフリー版の時代はアップロード機能が使用できなかったためうまい方法が見つからずハイライトなしでしたので、見づらいという指摘を受けてましたけど ほんと コード部分見づらくて申し訳ないです。