DirectXの話 第125回
Tile-based Rendering
今回は(多分)次世代の標準となる(であろう) Tile-based Rendering をやってみました。
通常の Forward Rendering では多数のライトを扱うことは難しいですが、Tile-based 技術を使うことで多数のライトを扱うことが可能になります。
DirectX9世代のハードでは多数のライトを扱うのが難しい状況がありました。
たいていは1回の描画キックに使用されるライトの個数を4個程度に制限し、その4個を何らかの方法で選択して処理していました。
例えば、平行光源1つ、キャラ専用のライト1つ、マズルフラッシュ等のエフェクト用に1つ、配置されている点光源1つといった具合です。
極めて妥当な方法ではありますが、実際に1つのオブジェクトに影響を与えるライトは4つで済まない場合がほとんどです。
ろうそくが大量に置いてある通路、なんていうものを思い浮かべればわかりやすいですよね。
このような状況を解決するために出てきたのが Deferred Rendering です。
Deferred Shading や Deferred Lighting (Light-Pre-Pass) はスクリーンに入るライトのみを描画することで複数ライトの問題を解決しています。
描画の方法はそのライト形状のモデルであったりスクリーン空間でのAABBであったりしますが、基本的にライトが当たっているピクセル(もしくは当たる可能性のあるピクセル)のみの描画で済みます。
とは言っても、その辺はあくまで理想論だったりするので、ライトの数が1000を超えてくると単純な Deferred Rendering では不十分になってきます。
そこで登場したのが Tile-based Deferred Shading です。
Tile-based Deferred Rendering (TBDR) はスクリーンをタイル状に分割し、それぞれのタイルとライトの接触判定を行い、接触しているライトのみでライト計算を行います。
DirectX9世代のGPUではこのような手法は非常に難しいのですが、DirectX11世代ではコンピュートシェーダ1つでこの処理が可能となります。
実際のところ、各ピクセルで発生するライト計算の回数自体は普通の Deferred Rendering と TBDR でそれほど変わらないと思います。
しかし、VS、(GS)、PSを必要とする Deferred Rendering に対して、CS1つで済む TBDR の方が高速になる傾向があるようです。
最終的にはライトの数に左右されるので、どちらも実装しておいてシーンによって最適な方を選ぶ、という方がいいかもしれません。
下図はライトのタイル処理のイメージ図です。
矩形はタイルを、円はライトを、矩形内の数値はそのタイルに接触しているライトの数を示します。
スクリーン空間ではポイントライトは正円にはならないんですが、イメージ図ってことで。
なお、衝突判定はスクリーン空間ではなくビュー空間で行います。
そのためにタイルごとの錐台を求める必要がありますが、"compute_tile.hlsl" ファイルに錐台を求める以下の関数があります。
void GetTileFrustumPlane( out float4 frustumPlanes[6], uint3 groupId )
{
// タイルの最大・最小深度を浮動小数点に変換
float minTileZ = asfloat(sMinZ);
float maxTileZ = asfloat(sMaxZ);
float2 tileScale = screenParam.zw * rcp( float(2 * kTileWidth) );
float2 tileBias = tileScale - float2(groupId.xy);
float4 c1 = float4(mtxProj[0].x * tileScale.x, 0.0, -tileBias.x, 0.0);
float4 c2 = float4(0.0, -mtxProj[1].y * tileScale.y, -tileBias.y, 0.0);
float4 c4 = float4(0.0, 0.0, -1.0, 0.0);
frustumPlanes[0] = c4 - c1; // Right
frustumPlanes[1] = c1; // Left
frustumPlanes[2] = c4 - c2; // Top
frustumPlanes[3] = c2; // Bottom
frustumPlanes[4] = float4(0.0, 0.0, -1.0, -minTileZ);
frustumPlanes[5] = float4(0.0, 0.0, 1.0, maxTileZ);
// 法線が正規化されていない4面についてだけ正規化する
[unroll]
for (uint i = 0; i < 4; ++i)
{
frustumPlanes[i] *= rcp( length( frustumPlanes[i].xyz ) );
}
}
IntelのTBDRサンプルから拝借したものですが、不具合の修正と右手座標系対応を行っています。
Intelのサンプルではタイルに対して少し大きめの錐台となって無駄な計算が発生しているはずです。
TBDR は Deferred Rendering のライト計算を高速化する手法ではありますが、Deferred Rendering、特に Deferred Shading の欠点を解決する方法ではありません。
Deferred Shading の欠点としては大きく4つ挙げられるでしょう。
1つめは G-Buffer が肥大する点です。
1回のジオメトリ描画で多数の情報をバッファに保持する必要があり、これが非常に大きくなりがちです。
現行世代では4枚のバッファに描画を行うことが多いですが、次世代ではより多くの情報を持たせるために多くのバッファを必要とするかもしれません。
2つめはマテリアルの種類をあまり増やせないという点です。
Deferred Lighting であればある程度増やすことは出来るのですが、Deferred Shading はライト計算時に対処できる分のマテリアル数に押さえなければなりません。
マテリアル数を増やす場合、ライト計算時のシェーダが多くの分岐を持つようになりますし、G-Buffer に持たせる情報も増えてくるでしょう。
3つめは半透明オブジェクトの描画には Deferred Shading は使えないという点です。
半透明も使える Deferred Rendering もあるにはあるのですが、制約が全くないわけでもなく、いまいち自由が利きません。
結果、半透明オブジェクトは Forward Rendering にならざるを得ず、半透明オブジェクトを多用する日本のゲームには合わないとされてしまうわけです。
4つめは MSAA が使いづらい点です。
MSAA は使えないと言われることも多いですが、使えないわけではありません。使うと重くなるから使いづらいだけです。
ポストプロセスAA を使う手段もあるのですが、MSAA と一緒に使った方がより効果的です。
これらを一度に解決するには Forward Rendering を行うしかないのですが、大量のライトを使った日には恐ろしいことになること請け合いです。
しかし、ここで再び Tile-based 技術が顔を出してきます。
この技術は Tile-based Forward Rendering (TBFR), Forward+ Rendering, Indexed Deferred Rendering などと呼ばれていますが、ここでは TBFR と呼ぶことにします。
TBDR と同じようにタイルごとにライトの衝突判定を取ります。
TBDR ならそのままピクセルごとのライト計算に進んでしまうのですが、TBFR ではこのライトインデックスを別のバッファに保存します。
このバッファは最大で (タイル数) × (ライト数) × (sizeof(u32)) となります。
この計算の後、普通の Forward Rendering の要領で描画を行うのですが、参照するライトはそのピクセルが所属するタイルと衝突しているライトのみです。
先に保存していたライトインデックスを取得すれば簡単に実装できます。
TBFR は先に挙げた Deferred Rendering の弱点を完璧に補えます。
しかし、欠点がないわけでもありません。
まず、MSAA を使用しない場合はたいてい TBDR より遅いです。
この理由として、TBFR の場合は Z Pre-Pass Rendering がほぼ必須になるからです。
TBDR は黙っていてもオクルージョンカリングされる部分についてのライト計算は行われません。
しかし、TBFR の場合は Z Pre-Pass を行わないとカリングされる部分もライト計算が行われる可能性があります。
これが非常に多くなってしまうと速度面で大きく不利になります。
これを解消するために Z Pre-Pass はほぼ必須なのですが(ちなみに、AMDの人が言ってた)、この場合はジオメトリ描画を2回行うことになります。
つまり、Deferred Lighting の不利な面を受け継ぐことになります。
しかし、4xMSAA を行うのであればほぼ TBDR より高速になりますので、MSAA を使いたいなら TBFR を選ばない手はないでしょう。
バッファサイズについてはライト数にもよるのですが、タイルサイズが 16*16 でライトの数が 1024 であれば1画面分のサイズと同等です。
軽くはないですが、TBDR で大量に G-Buffer を使用するよりはよっぽどマシでしょう。
なんと言っても TBFR は半透明オブジェクトや、やろうと思えばパーティクルに対しても使用できるという点が有利でしょう。
とはいえ、パーティクルに対して大量のライトを処理しようとするとかなり重くなるんじゃないかとは思います。
何らかの工夫は必要になるでしょうね。
TBDR や TBFR は相当数のライトを使うことが出来るとは言え、単純にライトを増やしても重くなるだけです。
重要なのは、それだけのライトを使って何を表現するかという点です。
1024個のライトが使えます、と言われたとして、デザイナさんはフルにライトを使用できるでしょうか?
大半の人は、64個あれば十分だよ、とか言うんじゃないかと思います。64個でも多いかもしれません。
実際、パーティクルにライトをくっつけようと言っても、それで望んだ絵になるのかは不明です。
個人的には Virtual Point Light (VPL) に使用するのがいいんじゃないかとは思うのですが、リアルタイムに VPL を計算できないと効果は薄いかな、とも思います。
そして、ツールについても十分考慮しなければならないでしょう。
1024個のライトを配置するかは別として、大量のライトを配置しやすい環境を作る必要があります。
そういうことを考えるのも次世代では大切になります。
また、ライトが大量に配置された場合にシャドウはどうするかという問題も発生します。
シェーディングは多数のライトに対して行われているのにシャドウイングは1つのライトしか見ていない、というはよろしくありません。
各ライトごとに小さなキューブマップをシャドウマップとして適用する方法もあるようですが、何も考えずに実装すると酷いことになるんじゃないでしょうか。
とりあえずライトを増やしてみる、というだけではダメでしょうね。
サンプルは以下からDLしてください。
今回のサンプルは MSAA なしなので、TBDR の方が高速です。
ライトの数が動的に変更できないのも手抜きのためです。すみません。
ちなみに自宅の環境だと、ライトの数が多かろうが少なかろうが TBDR が最も高速です。
TBFR と Deferred Lighting は、ライト数が64くらいまでは Deferred Lighting が、512個以上は明確に TBFR の方が高速です。
普通の Forward Rendering も選べるようになっていますが、これがアホみたいに重いのは選択してもらえればわかります。
ビルドが必要なので面倒かもしれませんが、ライトの数を増減して遊んでみてください。