カテゴリー別アーカイブ: DirectX

DirectXに関連する話題です

DK2のHMDにおける、レンダリング結果のPixel密度の分布について

Oculus Rift DK2のHMDスクリーンにおける、シーンのレンダリング結果のPixelの分布に関する考察です

DK2の視野の中心のPixel密度について

 Oculus Riftに代表される、近年リリースされているHMDの中には、広範囲の視野を確保するため、光学的に収差を補正するのではなく、収差をマッピングしたレンダリングを行うことで、レンダリングを成立させているものがあります。
 DK2では、レンズが強い糸巻きの歪収差を持つため、シーンが樽型の配置になるようにレンダリング結果を再マッピングしています。そのため、歪収差補正前のレンダリング結果が、HMDのスクリーン上では、視野中心付近では拡大傾向になり、視野周辺付近では縮小傾向となります。加えて、歪収差補正の前段で使用する通常の透視変換も、視野の中心付近のPixel分布は疎になり、視野周辺が密になる傾向があります。
つまり、単一の透視変換のレンダリング結果に、さらにDK2の歪収差補正のレンダリングを掛けると、視野中心が疎、周辺が密というPixel分布の傾向が強調されることになります。
 以前このブログのPostで、これに関する考察を行い、CubeMapの角周辺の密なPixel分布になっている箇所が、HMDの視野の中心に来るようなレンダリングを提案しました。こうすることで、DK2の歪収差補正と、CubeMapのPixel密度の分布が相殺する形で、良好なPixel分布が得られるのではないかと考えたわけですが、今回これをテストしてみました。

CubeMapRenderingの結果

以下のスクリーンショットは、通常の単一の透視変換でレンダリングしたものと、CubeMapを用いた場合になります。Gridが無い状態だと区別が付きにくいので、Gridを表示したものを用意しました。
懸念された、CubeMapからのDistortionRenderingにおけるCubeMapの境界ですが、Seamless Cubemap Samplingが可能なGPUでは、境界は殆ど判別できない結果となりました。
ovr_01
ovr_02
ovr_04
ovr_05

中間レンダリング結果のHMD上におけるPixe分布の検証

次に、中間レンダリング結果のPixelがHMD上における最終レンダリング結果で、どのようなPixel密度になるのかをレンダリングしてみました。
下記のグラフが、ピクセル密度をあらわします。範囲は0.3~8.3 でclampしています。単位は(中間RT pixel)/(HMD pixel)となっています。
color_bar

DK2の使用レンズはAレンズで、Eye Reliefなどは、デフォルトの状態で検証しました。
まずは、単一の透視変換を用いた通常のレンダリング時のPixel分布です。片眼の中間RTの解像度は、1182x1461pixel, FoVは(90H, 100V)で計算しています。これらの数値は、OVRSDKがデフォルトの値として使用しているものとほぼ同一です。

ovr_planner
この図からわかるとおり、OVRSDKは、視野の中心が、おおよそ1sample/pixelになるように、中間RTの解像度を決定しているようです。
中間RTのPixel分布は、視野周辺に向かって上昇し、レンズ視野範囲外と思われる部分で、5.0 sample/pixを超えるという状況になっています。

次にCubeMapを用いた場合のPixel分布です。CubMapのRT解像度は, 1071x1071x3 となっています。Pixel数は上記の約2倍になるように計算しました。

ovr_cube_758_2
図の通り、視野の中心では、高いPixel密度を確保できたものの、視野周辺に向けて急速にPixel密度が落ちています。上記の通り、約2倍のPixel数を用いてレンダリングしたにも関わらず、視野中心を除いては、良好なPixel分布とはいえません。

まとめ

今回は、CubeMapレンダリングによって、DK2のHMDにおける中間RTのPixel分布を改善できるかどうかを試してみましたが、結果的には、従来通りのレンダリングの方が良好な結果が得られることが解りました。
ただし、今回の実験を通じて解ったことがあります。

視野中心のみレンダリング品質を上げても余り意味が無い

まず、テストを通じて感じたのは、視野の中心部分と周辺部分にレンダリング品質に差があると、それが気になり、全体的なHMD体験の向上にはつながらないということが解りました。つまり、視線追跡の類の技術と組み合わせて使わない限りは、このようなレンダリングは、あまりり大きな意味が無いことがわかりました。

通常のレンダリングパスで発生している、中間RTのPixelの無駄

単一の透視変換を用いた通常のレンダリング時のPixel分布で解るとおり、HMDのレンズを通じて殆ど見えないPixelに、中間RTのPixelが密に配置されています。これは仕方が無い事ですが、このPixelは基本的に描画する必要のないPixelとなります。
従って、中間RTをレンダリングする際に、一番初めに視野の形状(円形)に穴の開いたObjectを、視点付近で描画し、不要なPxielを早期棄却できるようにすることで、実質的なレンダリングのPxiel数を減らすことが出来るはずです。
実際には、TimeWarpによる補正を使用する場合は、レンズ視野角以上のPixelを使用する可能性があるので、確保する補正幅を残して行う必要がありますが、HMD上での見た目以上に、中間RTでは多くのPixelがそこに分布していることが解ったので、試してみる価値はあるかもしれません。

GPUViewで確認できるEventを簡単に追加する方法

GPUViewは、GPUとCPUのスレッド実行のタイミングを確認する際に特に有用なのですが、処理が複雑になると、一体どのDMAパケットが、何の処理をしているのかが分かりにくくなります。特に、他人の書いたプログラムをチェックしている際などは顕著です。こんなときに、Custom EventをマーカーとしてGPUViewのLogに埋め込むことが出来れば、CPU側が一体何をしているタイミングなのかを簡単に知ることが出来ます。
続きを読む

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

    A Reconstruction Filter for Plausible Motion Blur について

    テストを兼ねて実装してみました。

    [参照]
    A Reconstruction Filter for Plausible Motion Blur

    ScatterとGatherについて

    Blurを適用する際に考えられる方法として、自身のPixelのVelocityに従って、自身のPixelの色を他のPixelに拡散させる方法(Scatter)と、周辺のPixelの色とVelocityに従って、自身のPixelに周辺のPixelを重畳させる方法(Gather)の二つが考えられます。
    Scatterの演算は、GPUの処理に置き換えれば、多数のPixelのBlending処理に置き換えられます。メモリの書き込み負荷が高いことが容易に想像がつきます。
    一方で、Gatherの演算は、Blurの適用半径を大きくすれば、周辺の多数のPixelをSamplingしなければならず、メモリの読み込み負荷が高いです。また、Gather演算の場合は、SampleしたPixelのVelocityが小さかったり、向きが関係ない方向の場合は、対象PixelにBlurが届かず、Sampling処理自体が無駄になります。

    本手法は、Gather手法ですが、周辺のPixelに対して一様にSamplingを行う代わりに、周辺に存在する、一番大きいVelocityを代表として、そのVelocityに基くSamplingを行うことで、Gather手法を用いつつも少ないSampling数で、大きなBlurの適用半径を実現するものです。

    Tile Max と Neighbor Maxについて

    上記で説明した、周辺に存在する、一番大きいVelocityを検出するために、この2つのバッファを作成します。まず、screen spaceにおけるVelocity Mapが存在するか、算出可能なことが前提です。
    Tile Maxは、Blurの最大半径に相当する大きさを単位としてTileを作成し、それぞれのTile内で、Velocityが最大のVectorを保存します。従って、Tile Maxバッファの解像度は、(w,h)/size_of_a_tileとなります。
    Neighbor Maxは、自身のTileと隣接する周辺8つのTileの中で、Velocityが最大のVectorを保存します。
    後に、Gather演算を行う際に、該当するNeighbor Maxに格納されているVectorを参照すると、”周辺に存在する最も大きなVelocity”を取得することが出来ます。
    本手法では、Neighbor Maxに格納されるVectorを”周辺で支配的に作用するVelocity”と見做し、BlurのReconstruction処理を、このベクトルに基いて行います。

    Reconstruction処理について

    Reconstruction処理では、Neighbor Maxに格納されているVectorに沿って、[-1,1]の範囲で直線上をサンプリングします。(ただし、Ghostingを避けるためにJitteringしています)
    SamplingしたPixelと、基準となるPixelのZ値を考慮しつつ、互いにBlurで滲出する量を計算して最終結果としています。(詳細は論文参照のこと。pseudo code付きで解説されています。)

    Samplingの際のアクセスパターンは、周辺のTileを共有するPixelで一様なので、Samplerのキャッシュヒットは大いに期待できます。
    Blurの半径を大きくすれば、キャッシュのヒット率は低下すると思われますが、Sampleの数を増やせばヒット率が上昇すると思われます。各GPUの特性やそれぞれの状況に応じて調整できると思います。

    考察

    まず、Tile Max の算出には、Compute Shaderを用いるのが最も適切と思われます。Vectorの長さは常に正なので、uintキャストしてAtomicMaxのパターンです。
    一方で、CSの使えない環境下では、PSでMipMapへの畳み込みを行う必要があると思います。
    Neighbor Maxも同様の方法で求めることが出来ますが、周辺3×3のSamplingなので、1Threadで3×3ブロックをサンプリングして、比較したほうが速いです。
    ここでは、PSを使うかCSを使うか迷うところなので、確かめるために実装してみました。しかし、Neighbor Maxを求めるための同機能のシェーダーコードをPS,CSに実装し、実行時間を比較をGTX680で行ってみましたが、有意な差は見出せませんでした。
    したがって、どちらを使っても問題なさそうです。(使用したCSのnumthreadは[4x8x1]です。)

    Blur

    ちなみにこの論文の手法を元に改良した手法が既に発表されています。
    [参照]
    A Fast and Stable Feature-Aware Motion Blur Filter