IntelのARTのResearcherのMasamichi Sugiharaさんが第一著者のリアルタイムGI手法の論文です。
Layered Reflective Shadow Maps for Voxel-based Indirect Illumination
Voxel Cone TracingによるGI手法の一種といって差し支え無いと思います。
既存の手法(SVOGI)では、VoxelにOcclusionの情報とLightの情報を書き込みますが、本手法では、Voxel内に記述するのはOcclusionの情報のみで、Lightの情報はLayered Reflective Shadow Maps(LRSM)に格納するのが、既存手法との大きな違いです。
アルゴリズム概要
参照論文(Fig.3)に、アルゴリズムの概要が図で記載されています。
GIに関するパスは大きく分けて2つです。
Binary Voxelization
シーン内の遮蔽物(直接光を反射する物)のVoxel化を行います。Voxel情報は文字通りBinaryで、
VoxelがFullもしくはEmptyを記述します。つまり、1Voxelあたり1Bitの情報となります。加えて、効率的なCone TracingのためにMip Mapの生成を行います。
LRSM Construction
LRSMは各光源ごとに生成します。LRSMのソースとなるのは、RSMです。RSMは光源から見た、直接光の反射状況が記述されます。
具体的に、RSMには、
が格納されています。
通常のRSMを、そのままCone Tracingに用いると、Cornのサンプル半径が大きくなったときに、計算量(サンプル数)が大きくなるので、事前にフィルターを適用してこれを少ないコストで行えるようにします。簡単に言ってしまえば、MipMapのようなものを生成するのですが、生成の過程で問題になる、深度情報もしくは法線情報が大きく異なる隣接Pixelのフィルタリングを適切に行うための対策として、Layerを生成します。
4.2 Reflective Shadow Map Pre-filtering
RSMの各要素に対してpre-filterの適用、すなわちMipMapの生成を行います。
その際に用いるfilterですが、Reflected RadianceとNormalに関しては、Hardware Texture Samplerを用いたBox Filterと記述されているので、線形のBox Filterを用いるようです。(実際には後述の理由により、そのままSamplerの補間機能を用いることはしていないと思います。)
Depth Pre-filtering
Depthは、RSMとLRSMで表現形式が異なります。
まず、RSMを特定のCorn半径でサンプリングした場合を考えると、そのCornの深度の範囲内のDepthに分布しているRSMのピクセルのみが、Cornのサンプリングに寄与するべきです。しかしこれを大きなCorn半径で行うと、サンプリング数と計算コストが高くなります。
LRSMでは、この部分を確率密度関数に置き換えています。具体的にはガウス分布を用いています。ガウス分布を計算するために、サンプル範囲の深度の平均値と分散値が必要になるので、深度と深度の2乗の値を計算し格納します。これをMipMap化しておくことで、Corn半径に応じたMipMapをサンプルすることで、深度分布の確率関数を少ないサンプル数で入手可能となります。
4.3 Reflective Shadow Map Partitioning
LRSMでは、文字通りLayerを生成するのですが、Layerのパーティションのキーとなるのが、RSMの法線と深度です。
Pre-Filterを適用する前に、あらかじめ近しい法線と深度でLayer分けした後、Filterを適用することで、大きな品質の低下を防ぎます。
法線に関しては、3要素のベクトルなので、dot(n, l)の値を用いて区分けをします。
たとえば、深度方向でa個、dot(n, l)の値でb個パーティションを設定すれば、Layerの数は、(a+1)(b+1)となります。深度方向のパーティションの境界では、オーバーラップ区間と遷移のための関数を用い、スムースな遷移が行われるようにしています。
5 Implementation
Voxelの構築
Voxelには、1Bitの情報しか記述しませんが、テクスチャフォーマットの制約上、1Byte/Voxelとなっています。
LRSMの構築
LRSMの格納には2DTexture Arrayを用います。LRSMの構築は大きく分けて2-passとなります。
まずはじめにRSMを描画します。これには、Reflected Radiance, Depth, Normal, dot(n,l)を記述します。
次にCompute Shaderで、各RSMをサンプリングし、Depthとdot(n,l)の値から、振り分けるLayerを決定し、同時に、Depthの値を、各Layerの深度範囲で正規化し、深度の2乗の値を計算します。
LRSMの解像度は、RSMの1/2なので、RSM 4Pixelの平均値が、LRSMのLOD:0に格納されます。Compute Shaderの生成スレッドは、LRSMの解像度にあわせ、1スレッドあたりRSMの2x2pixelを処理します。LRSMの全てのLayerのLOD:0の生成が終われば、あとはMipMapを生成します。
ライティングの計算
ライティングの計算では、各G-bufferのPixelで、Final Gatheringを行います。基本的な手法はCorn Tracingとなりますが、Corn TracingでVoxelにHitした場合は、まず、その時のHit位置とCorn半径をRSMの座標系に投影し、サンプルするLRSMの位置とMipMapのレベルを決定します。MipMapのレベルは、log(dcircle)で計算されます。dcircleはRSMに投影されたCornの直径(texel)です。
次に、Hitした場所の深度から、該当深度のLayerのLRSMをチェックします。まずはじめに、LRSMのNormalをサンプリングし、dot(N,Vc) (Vc:Cone Tracingの方向)を計算し、これが負になる場合は、Corn Tracingの向きに対して、裏面であると考え棄却します。
次に、LRSMのDepthとDepth^2をサンプリングし、深度分布の関数を算出し、該当深度が分布関数で0以上かを調べ、0の場合は該当のLRSMが深度範囲外とし棄却します。
次に、LRSMのReflected Radianceをサンプリングし、深度Layerの遷移関数、深度分布の確率とdot(N,Vc)を乗算し、Tracingの結果とし、該当するLayer全ての結果を加算します。
6 Results
参照論文に、Voxel Corn Tracingとの実行時間と、品質の比較が載っています。
考察
本手法の一番のポイントは、なんといってもVoxelに光源情報を書き出さないことだと思います。SVOGIではOctree構築とLightのInjectionの部分が明らかに複雑でしたが、本手法では、Voxelizingは最も明快な部分といえると思います。LRSMの構築も、不明瞭な点がなく、SVOGIと比較し素早く実装が可能だと思います。
Diffuse Cornのサンプリングにかかる時間がSVOGIに比べて5倍以上の時間がかかりますが、Specular Cornのサンプリングは、20%程度高速となっています。Specularに関してはOctreeの追跡コストから来るものと思われ、基本的に本手法ではCorn TracingにかかるコストはSVOGIよりも高いものと思われます。これは、どちらが優れているということではなく、SVOGIではCorn TracingのためにVoxelにRSMの情報を埋め込むのに対し、本手法ではそれを行わない代わりに、LRSMを構築し、Corn Tracingを行うときにコストをかけるようにしているという違いです。近年では、Corn Tracingをフレーム間で分割して行い、再構成する手法が開発されており、Corn Tracingにかかる時間(サンプル数)は削減可能なため、本手法のほうが高速化に関しては可能性が大きいと思います。
もうひとつの大きな違いは、Voxelが必要とするメモリ領域です。本手法では、必要とするVoxel情報が1Bit/Voxelなので、Octreeの構築を行わなくても1024^3のVoxelを構築可能です。本論文では512^3まで実装していますが、これは、1Byte/Voxelのメモリ割り当てで実装したためで、1Bit/Voxelで実装すれば1024^3解像度のVoxel空間を、現実的な速度で実装可能だと思われます。これにより、1024^3解像度までのVoxel空間ならば、SVOGIで行っていたOctree構築と追跡の手間が省略可能となります。
対して、LRSMの構築と評価は、光源ごとに必要となります。多数の光源を配置したGIでは、多数のLRSMの構築と評価が必要となるため、光源の数は多くても数個までだと思われます。また、SVOGI手法では比較的簡単なEmissive Objectの反映も本手法では難しいです。逆に、太陽などの支配的な単一光源があるシーンでは、本手法が適していると思われます。
余談: 1Bit/Voxelを使う
DX11のテクスチャフォーマットには、1Bit/Texelのフォーマットはありません。したがって、R8_UNROMのテクスチャの8Bitをうまく使う必要があります。幸いにして、8Bitは丁度2x2x2のVoxelに見立てることが出来るので、1Bit/VoxelのVoxel空間は、解像度が半分の8Byte/Voxelで表すことが出来ます。
Bitの書き込みには、Atomic命令のAndもしくはOrを使用する必要があります。Atomic命令は4Byteアラインのアドレスに対して適用可能なので、BitMaskをうまく調整して実行することで、1Bitずつ書き込むことが出来ます。読み出しは、対応するBitMaskが生成できれば、読み出したTexelとAndを取るだけで、こちらはAtomicの必要がありません。
ちなみにこれは、GeForce GTX680で動作が確認されていますが、正しく動作することが永続的に保障されるものではありません。あくまで実験的な実装となります。(とはいえVoxelを扱うときに1Bit/Voxelの格納領域は、ほぼ必須な気もしますが。)
// The following codes are for the 3Dtexture which is 8bit format. void MarkExFlag(RWTexture3D tExVoxel, const uint3 vPos) { uint3 exVoxPos = uint3(vPos.x/2, vPos.y/2, vPos.z/2); uint byteOfs = exVoxPos.x & 0x03; uint exFlag = 0x01 << ((byteOfs * 8) + (vPos.x&0x01) + (vPos.y&0x01) * 2 + (vPos.z&0x01) * 4); InterlockedOr(tExVoxel[uint3(exVoxPos.x&0xFFFFFFFC, exVoxPos.y, exVoxPos.z)], exFlag); } uint GetExFlag(Texture3D tExVoxel, const uint3 voxPos) { uint3 voxExPos = voxPos/2; uint exVoxValue = tExVoxel.Load(uint4(voxExPos, 0)); uint flag = 0x01 << ((voxPos.x&0x01) + (voxPos.y&0x01) * 2 + (voxPos.z&0x01) * 4); return flag & exVoxValue; }