タグ別アーカイブ: GlobalIllumination

大域照明,間接照明,Indirect Illumination,Global Illumination関連の話題

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;
    }
    

    ConeTracingについて

    Cone Tracingについて少し考えてみたいと思います。VoxelのOctreeの処理については考慮しません。

    参照

    [Implementing Voxel Cone Tracing. Simon Yeung]
    [Non-interleaved Deferred Shading of Interleaved Sample Patterns, Segovia06]
    The Technology Behind the 3D Graphics and Games Course “Unreal Engine 4 Elemental demo”

    Coneの角度の決め方

    参照に挙げた、SimonさんのBlogの解説では、ジオメトリの法線方向を中心に6方向のConeTracingを行う方法を用いています。Coneの角度は60度だそうです。60度のConeは、3つ並べると、丁度180度となるため、法線方向に一つのConeを配置して、それを囲むように5方向のConeを配置することで、半球上の光をTracingするということのようです。

    Coneの角度を決める際には、もっと汎用的な考え方もあると思います。
    半球の表面積は2πなので、6回のConeTracingでこれを購うとすると、2π/6が一つのConeの受け持つべき面積です。言い換えると、半径の1/6の長さで球を切り取った部分(球冠)が、それに該当すると思います。これより求まるConeの角度は、acos(5/6)*2なので、67.1度となります。
    これは、Coneの数で半球上の面積を割り、その面積から求めた角度なので、実際にこの角度のConeを半球状に、重複することなく配置することは不可能です。しかし、ConeTracingのConeは、概念的な存在で、Voxel空間を正確にConeの形状で切り取るわけではありません。したがって、このような考え方も十分通用すると思います。

    Voxel内のSampling位置の決め方

    次に、ConeTracingする際の、Voxel内のSampling位置の決め方を考えます。

    まず、例として、Coneの角度が67.1度の場合を考えます。
    Coneの角度が67.1度の場合は、0.5/tan(67.1/2)の位置で、Coneの直径が、ちょうど1になります。値は約0.75です。従って、Coneの直径が2になる場所は、1.5で、直径が4になるのは、3.0です。これ等の位置で、VoxelのそれぞれのLODをサンプリングすれば、Coneの形状に沿ったサンプリングになると思います。
    この計算から明らかなように、Voxelの絶対的な大きさは影響せず、Coneの角度によってのみサンプリングの位置が決定します。この例のように、Cornの開度が大きい場合は、サンプリング回数は各LODにつき1回で、連続した空間をサンプリングすることができます。

    次に、極端に細いConeの場合を考えます。
    例として、角度が10度のConeを考えた場合、0.5/tan(10.0/2)の値は5.7になります。従ってceil(5.7)=6なので、LOD:0で6回サンプリングすれば良いわけです。5.7/6は0.95なので、LOD:0を0.95間隔で6回(0.95, 1.9, 2.85, 3.8, 4.75, 5.7)サンプリングします。
    LOD:1は、距離5.7~11.4までをサンプリングします。ceil(5.7/2)=3、つまり3回サンプリングすれば良いわけです。5.7/3は1.9なので、LOD:1は,間隔1.9で3回(7.6, 9.5, 11.4)サンプリングを行います。以降の処理は同様で、目的の距離までサンプリングを行います。

    結局のところ、想定したConeに則って、サンプリング位置とLODを決めれば良いだけで、上記も考え方の一つに過ぎません。Quad-Linear filteringを用いれば、各LODの中間の値もサンプリングすることができます。

    Coneの角度と、サンプリング回数、サンプル距離、LODの関係

    上記の通り、ConeTracingで周辺の情報をVoxelから集める場合、Coneの角度と、サンプリング回数、サンプル距離には密接な関係があります。迅速に遠くまでの情報を集めようとするときは、VoxelのLODを下げて、大きな間隔でサンプリングする必要があります。そのためには、Coneの開度は必然的に大きくなります。一方、精度の高い情報が必要な場合は、角度の小さなConeを用いないと、LODの急激な低下で、情報の精度が落ちてしまいます。
    つまり、Coneの角度を決めてしまえば、情報の精度と、同一サンプリング回数で到達する距離は決定します。また、Coneの角度と必要到達距離を決めると、必要とするVoxelのLODのレベルが決定します。

    Non-interleaved Deferred Shading of Interleaved Sample Patternsについて

    隣接する場所で、同じ向きで、ConeTracingを行った場合、その結果は、おそらく似た値になることが予想されます。この隣接度合いが、Voxelのサイズに対して十分小さい場合は、その結果は、おそらく殆ど同じ値になると思います。これを、DiffuseのConeTracingに大胆に活用した例が、UE4のGIをはじめとした技法になります。
    ここで使われる手法の元になるのが、Non-interleaved Deferred Shading of Interleaved Sample Patternsの論文で、簡単に言えば、Shadingの計算をインターリーブで行い、後でそれを合成するという方法です。これを応用し、ConeTracingのConeの向きをインターリーブして、結果を後で合成するようにします。
    UE4の例では、G-Bufferを3×3ピクセルブロックに分割し、1-9のインデックスを割りあて、各インデックスで、それぞれ1方向にのみConeTracingを行います。Traceする9方向は、全球上にうまく分布するようにあらかじめ設定しておきます。ConeTracingが終わったら、各自の周辺ピクセルから、ConeTracingの結果を取得します。(UE4の例では周辺5×5ピクセルから情報を集めているそうです。)これによって、各ピクセルで、9方向のConeTracingの結果が手に入るわけです。
    当然ながら、周辺のピクセルの深度値を見て、ピクセルが近いかどうかをチェックして使用する必要があります。また、周辺ピクセルの法線が未知なので、ジオメトリの法線を基準としたTracingは行えず、全球上をConeTracingする必要があります。加えて、この手法を用いる場合、Tracingの回数の自由度が少ないです。3×3を用いれば、Tracingの回数は9の倍数となり、4×4を用いれば16の倍数となります。しかし、全体でのConeTracingの回数は1/9もしくは1/16になります。
    この手法では、Traceする方向とConeの角度が固定されるので、最適化も行いやすいと思います。たとえば、各ピクセルで、Voxelの各LODに逐次アクセスするのではなく、LOD:0に対して行うTracing処理をまとめて行い、その後、LOD:1以降のTracingへといった処理にすることも可能だと思われます。こうした場合の、Voxel(3Dテクスチャ)へのアクセスは局所化され、キャッシュのヒット率向上が望める場合もあると思われます。
    ちなみに、ConeTracingのグリッドは必ずしも正方形である必要は無く、4×3のグリッドが用いられているケースもあるようです。このケースでは、12方向のTracingとなり、正20面体の頂点方向をそのままTracingの方向として使える利点がありそうです。

    ところで(実はここからが本題)

    ところで、全球上を9分割したConeTracingでは、具体的にどの方向にConeTracingをすればよいのでしょうか。
    残念ながら、頂点数が9の正多面体は存在しないので、適当に求めることにします。具体的な求め方は、ベクトルの向きを反復的に修正して、各ベクトルがなるべく離れるように分布させるだけです。これをWebGLで作ってみました。Chromeを使って作りましたので、Chromeならたぶん動作すると思います。
    wgtest
    使い方は、ベクトルの本数を6~32の範囲で入力し、Magnitudeのスライダを適当に動かして、ベクトルの分布が適切になるのを待ちます。あとはShow Axis Valueのボタンを押せば、ベクトルが表示されます。マウスで適当にドラッグすれば回転させることができるので、好みの向きに合わせることができます。表示される角度の値は、最も隣接するベクトルとの成す角度の平均で、ConeTracingの際の角度の指標になると思います。