Layered Reflective Shadow Maps for Voxel-based Indirect Illuminationについて

参照論文
Layered Reflective Shadow Maps for Voxel-based Indirect Illumination

IntelのARTのResearcherのMasamichi Sugiharaさんが第一著者のリアルタイムGI手法の論文です。
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には、

  • 直接光が反射された際に生じるRadiance(Reflected Radiance)
  • 光を反射した物体の法線ベクトル(Normal)
  • 光を反射した物体の光源からの距離(Depth)
  • が格納されています。
    通常の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;
    }