ジオメトリシェーダでカリング

Share

Frostbiteの”Optimizing the Graphics Pipeline with Compute”を今まで見ていなかったので,資料見たら感銘を受けました。ただ,実際に実装するの怠いな~と思っていたのですが,ふと思い出して,ジオメトリシェーダでカリングを実装してみました。
下記のような感じ。

///////////////////////////////////////////////////////////////////////////////
// CbCulling constant buffer.
///////////////////////////////////////////////////////////////////////////////
cbuffer CbCulling : register(b0)
{
    float4 Viewport : packoffset(c0);  // xy:(width, height), zw:(topLeftX, topLeftY).
};

//-----------------------------------------------------------------------------
//      メインエントリーポイントです.
//------------------------------------------------------------------
[maxvertexcount(3)]
void main
(
    triangle VSOutput                   input[3], 
    inout  TriangleStream< VSOutput >   output
)
{
    // Orientation Culling.
    // ※ラスタライザーステートで時計回りを正とするため,マイナス倍になっていることに注意!
    float det = -determinant(float3x3(
        input[0].Position.xyw,
        input[1].Position.xyw,
        input[2].Position.xyw));
    if (det <= 0.0f)
    { return; }

    // 正規化デバイス座標系(NDC)に変換.
    float3 pos0 = input[0].Position.xyz / input[0].Position.w;
    float3 pos1 = input[1].Position.xyz / input[1].Position.w;
    float3 pos2 = input[2].Position.xyz / input[2].Position.w;

    // Frustum Culling.
    float3 mini = min(pos0, min(pos1, pos2));
    float3 maxi = max(pos0, max(pos1, pos2));
    if (any(maxi.xy < -1.0f) || any(mini.xy > 1.0f) || maxi.z < 0.0f || mini.z > 1.0f)
    { return; }

    // Small Primitive Culling.
    float2 vmin = mini.xy * Viewport.xy + Viewport.zw;
    float2 vmax = maxi.xy * Viewport.xy + Viewport.zw;
    if (any(round(vmin) == round(vmax)))
    { return; }

    // カリングを通過したものだけラスタライザーに流す.
    [unroll]
    for (uint i=0; i<3; i++)
    { output.Append(input[i]); }
}

RenderDocでキャプチャして,ジオメトリシェーダの出力をチェックしてみました。
正面から見た図。

回り込んで横から見た図。

一応ちゃんとカリングされているようです。
コンピュートシェーダで実装するのが怠い人は,プラットホームによっては全然違いますが,ジオメトリシェーダでパフォーマンス向上するモードがなにかしらあると思うので,そちらで動作させると幸せになれるかもしれません。

※ちなみに手元の環境で計測もしてみたんですが,あまり高速化は見られず処理が格段に重くなりました。環境やシーンにもよると思いますが、暴力的な数のポリゴン数が投入されるシーンでは使わない方が良さそうだなと実感しました。手元のシーンでやってみた感じだと,GSカリングを入れた処理が+3msほど重くなっていて,シーン全体で+12ms程度負荷が増えました。

ビジビリティカリング メモ(3)

Share

こんちわ、Pocolです。
前回までで,ビジビリティカリングの説明が終わりました。
こんなに素晴らしいカリング手法があるのなら,積極的に使いたいと思いました。
で,ふと思いついたのが,ビジビリティカリングをシャドウキャスターのカリングに使おうという単純なアイデアです。

参考文献

[3] Oliver Mattausch, Jiri Bittner, Ari Silvennoinen, Daniel Scherzer, and Micheal Wimmer, “Efficient Online Visibility for Shadow Maps”, GPU Pro 3, pp.233-242, CRC Express, 2012.
[4] Jiri Bittner, Oliver Mattausch, Airi Silvennoinen, Micheal Wimmer, “Shadow Caster Culling for Efficient Shadow Mapping”, I3D’11: Symposium on Interactive 3D Graphics and Games, February 2011, pp.81-88.

実装アイデア

実装アイデアはいたって単純です。
[3]の文献で使用されているハードウェアオクルージョンカリングの代わりにカリング手法をビジビリティカリングに置き換えるだけのものです。

まず,以下を前提とします。
・カメラビューからみた深度バッファがある。
・シャドウマップ行列がすでに求まっている。

[3]の手法をかなり雑に説明すると,
欲しい情報は「カメラビューから見た時のピクセルにシャドウが落ちるかどうか?」であるので,カメラビューのピクセルにシャドウを落とさないものは,そもそも寄与しないからカリングしてよくね?というアイデアです。

実装のためのメモ

ここからは自分なりの実装前の実装するためのメモです。
[3]の手法を実装するために,マスクが必要なります。

まずは,Pre-Zなりで作成されたカメラビューから見た深度バッファを用いて,現在フレームのワールド空間位置座標に変換し点群を求めます。
次に,この求まったワールド空間位置座標にシャドウマップ行列を適用し,ライトビュー空間位置座標に変換します。
これでライトビュー空間からみたときのラスタライズ位置が求まるはずです。
あとはUAV等に,このラスタイズ位置に「カメラから見える」としてフラグを書き込みます。
これでマスクが完成です。

次に,False Negative Passと同じ要領でライトビューからOBBを描画し,マスクバッファ上のフラグが立っている場所にシャドウをキャストするものだけをビジビリティバッファ上に「見える」としてマークします。
これで「カメラビューに寄与するシャドウキャスターのみ」のビジビリティバッファが完成します。

あとはインダイレクトドロー情報を生成して,ExecuteIndirect()で描画すればOKのはず…。

オプションとして,Mainパスに当たる部分のように前フレームのシャドウマップバッファを用いて,粗くカリングしておくという手も考えられます。…あんまり高速化期待できないかもしれませんが。

ビジビリティカリング メモ(2)

Share

前回の続きです。
今回はFalse Negative Passの説明。

False Negative Pass

Main Passでは前フレームの深度バッファをダウンサンプルして,現在フレームの深度バッファをでっちあげカリング処理を行いました。
当然ながら,適当にでっちあげた深度バッファだと誤判定される可能性があります。
そこで,False Negative Passで誤判定されたものを取りこぼしが無いようにちゃんと判定しようというわけです。

このパスでは,フル解像度の深度バッファを用いて描画を行います。
文献[1]には,理論的にはコンサバティブラスタライゼーションを用いれば,この第2オクルージョンパスでも1/4解像度の深度バッファが使えると書いてあるのですが,いくつかの問題があったためフル解像度を使っているようです。

ビジビリティバッファのクリア

False Negative Passの最初の処理はビジビリティバッファのクリアです。
これはメインパスと同じようにゼロクリアすれば良いようです。

ビジビリティバッファを埋める

メインオクル―ジョンパスのように,Early-DepthStencilテストを通過したピクセルをビジビリティバッファ中にVisibleとしてピクセルシェーダで各インスタンスをマークします。問題のあるオブジェクトすべては最初のパスでどっちみち描画されるので,カメラのニア平面背後にあるバウンディングボックスに対してクランプするコードの実行は不要になります。

インダイレクトドロー情報を生成する

インダイレクトドロー情報はメインパス中で生成されます。
このときには可視できるインスタンスドロー情報のみが生成されているので,遮蔽されたインスタンスに対する処理は不要になります。

インダイレクトにFlase Negativeを描画する

メインパスと同じように描画します。

ここまでは,文献[2]でも説明されているので,特に新しいことはありません。
ここから先はアイデア段階のものです。
…それは次回に説明します。

ビジビリティカリング メモ(1)

Share

新年明けましておめでとうございます。
本年もProjectAsuraをよろしくお願い致します。

さて,今日はビジビリティバッファを用いたカリングを行うための自分用の実装メモを残しておこうと思います。
あくまでも自分用なので間違っているかもしれません。また実装結果ではなく,これから実装するためのメモなので全然当てにならない可能性もあるので注意してください。

参考文献

[1] Hawar Doghramachi and Jean-Normand Bucchi, “Deferred+: Next-Gen Culling and Rendering for Dawn Engine”, GPU Pro, pp.77-104, Black Cat Publishing 2017.
[2] 三嶋 仁, “最新タイトルのグラフィックス最適化事例”, https://cedil.cesa.or.jp/cedil_sessions/view/1897, CEDEC 2017.

概要

ここでは,[1]の文献をベースに纏めて置きます。
基本的に2パスで処理を行います。まず1パス目がMain Passと呼ばれるものです。
前フレームの深度バッファを1/4解像度にダウンサンプリングして,現フレームのビュー射影行列を用いて,現在フレームにおおまかに一致する深度バッファを作成しインダイレクトドローを用いて可視と判定されたメッシュのみを描画します。
2パス目はFalse Negative Passと呼ばれるもので,フル解像度でMain PassでOccludeと判定されたOBBを描画し,インダイレクトドローを用いて,可視と判定されたメッシュのみを描画します。

Main Passの処理概要は次です。

  • ① 深度バッファのダウンサンプリングとリプロジェクション
  • ② ビジビリティバッファのクリア
  • ③ ビジビリティバッファを埋める
  • ④ インダイレクトドロー情報を生成する
  • ⑤ 可視メッシュをインダイレクトに描画する

False Negative Passの処理概要は次になります。

  • ① ビジビリティバッファのクリア
  • ② ビジビリティバッファを埋める
  • ③ インダイレクトドロー情報を生成する
  • ④ 可視メッシュをインダイレクトに描画する

以下のデータが多分必要。

  • 前フレームの深度バッファ
  • 雑な現フレームの深度バッファ(Computeから深度書き込みできないらしいので,Color → Depthの変換が必要らしい…)
  • ビジビリティバッファ(uint32_tのUAV) 
  • インスタンス変換用のバッファ(たぶん,ワールド行列。これをつかってGPU上でOBBにする)
  • メッシュ情報
  • インダイレクトドロー引数用のバッファ(可視メッシュ描画用)
  • 可視インスタンスのインデックスバッファ
  • Occludeされたインスタンスのインデックスバッファ

上記のうち,[1]によると…
<CPU更新>

  • メッシュ情報
  • インスタンス変換用のバッファ

<GPU更新>

  • ビジビリティバッファ
  • 可視インスタンスのインデックスバッファ
  • Occludeされたインスタンスのインデックスバッファ
  • インダイレクトドロー引数用のバッファの

とのこと。

実装詳細

Main Pass

深度バッファのダウンサンプリングとリプロジェクション

前フレームの深度バッファを用いて4×4ピクセルの最大値をコンサバティブにとることにより1/4解像度にダウンサンプリングし,前のフレームのワールド位置座標を復元します。
復元した,ワールド位置座標に現フレームのビュー射影行列を掛けて,w除算することにより現フレームの雑な深度情報を生成しておきます。大きな深度値を避けるためにカメラよりも後ろの深度値を出力することを抑制します。
疑似コードは次のようになります。

// 4要素のうちの最大値を求める.
float Max4(float4 value)
{ return max( max( max(value.x, value.y), value.z ), value.w ); }

[numthreads(REPROJECT_THREAD_GROUP_SIZE, REPROJECT_THREAD_GROUP_SIZEk, 1)]
void main(uint3 dispatchThreadID : SV_DispatchThreadID)
{
     if ((dispatchThreadID.x < (uint(SCREEN_WIDTH)/4)) 
      && (dispatchThreadID.y < (uint(SCREEN_HEIGHT)/4)))
    {
        const float2 screenSize = float2(SCREEN_WIDTH/4.0f, SCREEN_HEIGHT/4.0f);
        float2 texCoords = (float2(dispatchThreadID.xy) + float2(0.5f, 0.5f)) / screenSize;

        const float offsetX = 1.0f/SCREEN_WIDTH;
        const float offsetY = 1.0f/SCREEN_HEIGHT;

        // 深度のダウンサンプル
        float4 depthValues00 = depthMap.GatherRed(depthMapSampler, texCoords + float2(-offsetX, -offsetY));
        float depth = Max4(depthValues00);

        float4 depthValues10 = depthMap.GatherRed(depthMapSampler, texCoords + float2(offsetX, -offsetY));
        depth = max( Max4(depthValues10), depth);

        float4 depthValues01 = depthMap.GatherRed(depthMapSampler, texCoords + float2(-offsetX, offsetY));
        depth = max( Max4(depthValues01), depth);

        float4 depthvalues11 = depthMap.GatherRed(depthMapSampler, texCoords + flaot2(offsetX, offsetY));
        depth = max( Max4(depthValues11), depth);

        // ダウンサンプルした深度から前フレームのワールド空間位置座標を再構築する.
        float4 lastProjPosition = float4(texCoord, depth, 1.0f);
        lastProjPosition.xy = (lastProjPosition.xy * 2.0f) - 1.0f;
        lastProjPosition.y = -lastProjPosition.y;
        float4 position = mul(cameraCB.lastInvViewProjMatrix, lastProjPosition);
        position /= position.w;

        // 現フレームの射影空間位置座標を計算する.
        float4 projPosition = mul(cameraCB.viewProjMatrix, position);
        projPosition.xyz /= projPosition.w;
        projPosition.y = -projPosition.y;
        projPosition.xy = (porjPosition.xy * 0.5f) + 0.5f;
        int2 outputPos = int2(saturate(projPosition.xy) * screenSize);

        // カメラ背後の大きな深度値を避ける.
        float depthF = (projPosition.w < 0.0f) ? depth : projPosition.z;

        // atomic max操作のための深度変換.
        // バインドされたカラーバッファはゼロで初期化されるため,最初に深度を反転し,atomic max操作を行い,
        // 最終的な深度バッファにコピーする際に深度を戻す.
        uint invDepth = asuint(saturate(1.0f - depthF));

        // 新しい位置にリプロジェクションされた深度を書き込む.
        InterlockedMax(depthTexture[outputPos], invDepth);

        // リプロジェクションによる穴あきを処理するために現在位置にリプロジェクションされた深度を書き込む.
        InterlockedMax(depthTexture[dispatchThreaID.xy], invDepth);
    }
}

ビジビリティバッファのクリア

ゼロクリアする。
DirectX12のUAVクリアAPIか,コンピュートシェーダでゼロクリアする。

ビジビリティバッファを埋める

前フレームの深度バッファから作成された現フレームの深度バッファをバインドして,ピクセルシェーダで[earlydepthstencil]付きでOBBを描画し,可視メッシュのフラグを立てる。
疑似コードは次のようになります。

// 頂点シェーダ
VS_Output main(uint vertexId : SV_VertexID, uint instanceID : SV_InstanceID)
{
    VS_Output output;

    output.occludedID = instanceID;

    // 単位キューブの位置を生成する.
    float3 position = float3(((vertexID && 0x4) == 0) ? -1.0f : 1.0f,
                             ((vertexID && 0x2) == 0) ? -1.0f : 1.0f,
                             ((vertexID && 0x1) == 0) ? -1.0f : 1.0f);

    matrix instanceMatrix = instanceBuffer[output.occludedID];
    float4 positionWS = mul(instanceMatrix, float4(position, 1.0f));
    output.position   = mul(cameraCB.viewProjMatrix, positionWS);

    // カメラがバウンディングボックスの中の場合,オブジェクト自身が可視であっても完全に遮蔽される.
    // そのため,バウンディングボックスの頂点がニア平面の後ろであるものは,そのようなオブジェクトカリングを避けるために
    // ニア平面の前にクランプされる.
    output.position = (output.position.w < 0.0f) 
                      ? float4(clamp(output.position.xy, float(-0.999f).xx, float(0.999f).xx), 0.0001f, 1.0f)
                      : output.position;

    return output;
}

// ピクセルシェーダ
[earlydepthstencil]
void main(VS_Output input)
{
    visBuffer[input.occludedID] = 1;
}

インダイレクトドロー引数の生成

コンピュートシェーダ上でインダイレクトドロー引数を生成する。
疑似コードは次のようになります。

#define THREAD_GROUP_SIZE 64

groupshared uint visibileInstanceIndexCounter;

[numthread(THREAD_GROUP_SIZE, 1, 1)]
void main
(
    uint3 groupID          : SV_GroupID,
    uint  groupIndex       : SV_GroupIndex,
    uint3 dispatchThreadID : SV_DispatchThreadID
)
{
    if (groupIndex == 0)
    {
        visibleInstanceIndexCounter = 0;
    }
    GroupMemoryBarrierWithGroupSync();

    MeshInfo meshInfo = meshInfoBuffer[groupID.x];
    for(uint i=0; i<meshInfo.numInstances; i+=THREAD_GROUP_SIZE)
    {
        uint elementIndex = groupIndex + i;
        if (elementIndex < meshInfo.numInstances)
        {
            uint instanceIndex = mesh.instanceOffset + elementIndex;

            // 可視の場合.
            if (visBuffer[instanceIndex] > 0)
            {
                uint index;
                InterlockedAdd(visibleInstanceIndexCounter, 1, index);
                visibleInstanceIndexBuffer[meshInfo.instanceOffset + indexs + NUM_FILL_PASS_TYPE] = instanceIndex;
            }
            else
            {
                uint index;
                InterlockedAdd(drawIndirectBuffer[0].instanceCount, 1, index);
                occludedInstanceIndexBuffer[index] = instanceIndex;
            }
        }
    }
    GroupMemoryBarrierWithGroupSync();

    if (groupIndex == 0)
    {
        if (visibleInstanceIndexCount > 0)
        {
            // 可視メッシュのカウンターをインクリメントする.
            uint cmdIndex;
            InterlockedAdd(visibleInstanceIndexBuffer[meshInfo.meshType], 1, cmdIndex);
            cmdIndex += meshInfo.meshTypeOffset + 1;

            // G-Bufferに可視メッシュを描画する.
            DrawIndirectCmd cmd;
            cmd.instanceOffset        = meshInfo.instanceOffset;
            cmd.MaterialId            = meshInfo.materialId;
            cmd.indexCountPerInstance = meshInfo.numIndices;
            cmd.instanceCount         = visibleMeshInstanceIndexCounter;
            cmd.startIndexLocation    = meshInfo.firstIndex;
            cmd.baseVertexLocation    = 0;
            cmd.startInstanceLocation = 0;
            drawIndirectBuffer[cmdIndex] = cmd;
        }
    }
}

可視メッシュを描画する

メッシュタイプごとにExecuteIndirectコマンドを発行して,描画します。
疑似コードはつぎのような感じ。

VS_Output main(VS_Input input, uint instanceID : SV_InstanceID)
{
    VS_Output output;
    uint instanceIndex = instanceInfoCB.instanceOffset + instanceID+

    instanceIndex = visibleInstanceIndexBuffer[instanceIndex + NUM_MESH_TYPES];

    matrix transformMatrix = instanceBuffer[instanceIndex].transformMatrix;

    ...
}

長くなったので,続きのFalse Negative Passは次回書きます。

Pre-Integrated SkinのLUTを作ってみた。

Share

こんにちわ。
Pocolです。

会社でキャラの肌が調整しずらいからどうにかしてくれ!
…と,言われてしまったので,とりあえずPre-Integrated Skinを調べてみようと思いました。

LUTテーブル作るところまでは実装してみました。
出来上がったLUTのテクスチャはこちら。

で,これを作るソースコードは以下のような感じ。


#define STBI_MSC_SECURE_CRT
#define STB_IMAGE_WRITE_IMPLEMENTATION

//-----------------------------------------------------------------------------
// Includes
//-----------------------------------------------------------------------------
#include <cstdio>
#include <stb_image_write.h>
#include <vector>
#include <asdxMath.h>

//-----------------------------------------------------------------------------
//      リニアからSRGBに変換.
//-----------------------------------------------------------------------------
inline float ToSRGB(float value)
{
    return (value <= 0.0031308) 
        ? 12.92f * value 
        : (1.0f + 0.055f) * pow(abs(value), 1.0f / 2.4f) - 0.055f;
}

//-----------------------------------------------------------------------------
//      ガウス分布.
//-----------------------------------------------------------------------------
inline float Gaussian(float v, float r)
{ return 1.0f / sqrtf(2.0f * asdx::F_PI * v) * exp(-(r * r) / (2.0f * v)); }

//-----------------------------------------------------------------------------
//      散乱計算
//-----------------------------------------------------------------------------
inline asdx::Vector3 Scatter(float r)
{
    // GPU Pro 360 Guide to Rendering, "5. Pre-Integrated Skin Shading", Appendix A.
    return 
          Gaussian(0.0064f * 1.414f, r) * asdx::Vector3(0.233f, 0.455f, 0.649f)
        + Gaussian(0.0484f * 1.414f, r) * asdx::Vector3(0.100f, 0.336f, 0.344f)
        + Gaussian(0.1870f * 1.414f, r) * asdx::Vector3(0.118f, 0.198f, 0.000f)
        + Gaussian(0.5670f * 1.414f, r) * asdx::Vector3(0.113f, 0.007f, 0.007f)
        + Gaussian(1.9900f * 1.414f, r) * asdx::Vector3(0.358f, 0.004f, 0.00001f)
        + Gaussian(7.4100f * 1.414f, r) * asdx::Vector3(0.078f, 0.00001f, 0.00001f);
}

//-------------------------------------------------------------------------------
//      Diffusionプロファイルを求める.
//-------------------------------------------------------------------------------
asdx::Vector3 IntegrateDiffuseScatterOnRing(float cosTheta, float skinRadius, int sampleCount)
{
    auto theta = acosf(cosTheta);
    asdx::Vector3 totalWeight(0.0f, 0.0f, 0.0f);
    asdx::Vector3 totalLight (0.0f, 0.0f, 0.0f);

    const auto inc = asdx::F_PI / float(sampleCount);
    auto a = -asdx::F_PIDIV2;

    while(a <= asdx::F_PIDIV2)
    {
        auto sampleAngle = theta + a;
        auto diffuse     = asdx::Saturate(cosf(sampleAngle));
        auto sampleDist  = abs(2.0f * skinRadius * sinf(a * 0.5f));
        auto weights     = Scatter(sampleDist);

        totalWeight += weights;
        totalLight  += weights * diffuse;
        a += inc;
    }

    return asdx::Vector3(
        totalLight.x / totalWeight.x,
        totalLight.y / totalWeight.y,
        totalLight.z / totalWeight.z);
}

//-----------------------------------------------------------------------------
//      ルックアップテーブル書き出し.
//-----------------------------------------------------------------------------
bool WriteLUT(const char* path, int w, int h, int s)
{
    std::vector<uint8_t> pixels;
    pixels.resize(w * h * 3);

    float stepR = 1.0f / float(h);
    float stepT = 2.0f / float(w);

    for(auto j=0; j<h; ++j)
    {
        for(auto i=0; i<w; ++i)
        {
            auto radius    = float(j + 0.5f) * stepR;
            auto curvature = 1.0f / radius;
            auto cosTheta  = -1.0f + float(i) * stepT;
            auto val       = IntegrateDiffuseScatterOnRing(cosTheta, curvature, s);

            // 書き出しを考慮して, 上下逆にして正しく出力されるようにする.
            auto idx = ((h - 1 - j) * w * 3) + (i * 3);

            pixels[idx + 0] = static_cast<uint8_t>(ToSRGB(val.x) * 255.0f + 0.5f);
            pixels[idx + 1] = static_cast<uint8_t>(ToSRGB(val.y) * 255.0f + 0.5f);
            pixels[idx + 2] = static_cast<uint8_t>(ToSRGB(val.z) * 255.0f + 0.5f);
        }
    }

    auto ret = stbi_write_tga(path, w, h, 3, pixels.data()) != 0;
    pixels.clear();

    return ret;
}

//-----------------------------------------------------------------------------
//      メインエントリーポイントです.
//-----------------------------------------------------------------------------
int main(int argc, char** argv)
{
    std::string path = "preintegrated_skin_lut.tga";
    int w = 256;
    int h = 256;
    int s = 4096;

    for(auto i=0; i<argc; ++i)
    {
        if (_stricmp(argv[i], "-w") == 0)
        {
            i++;
            auto iw = atoi(argv[i]);
            w = asdx::Clamp(iw, 1, 8192);
        }
        else if (_stricmp(argv[i], "-h") == 0)
        {
            i++;
            auto ih = atoi(argv[i]);
            h = asdx::Clamp(ih, 1, 8192);
        }
        else if (_stricmp(argv[i], "-s") == 0)
        {
            i++;
            auto is = atoi(argv[i]);
            s = asdx::Max(is, 1);
        }
    }

    return WriteLUT(path.c_str(), w, h, s) ? 0 : -1;
}

ちょっとググって調べた感じだと,https://j1jeong.wordpress.com/2018/06/20/pre-intergrated-skin-shading-in-colorspace/というBlog記事をみて,検証もせずにとりあえず鵜呑みで,sRGBで書き出してみました。
シェーダで使う場合は補正入れてリニアになるようにしてから使ってください。

NaNをなくす

Share

良くライティング計算結果がNaNになってポストエフェクトでバグるというのが頻発していて困っていたのですが,
ようやく最近回避方法を知りました。
StackOverflowとかみていたら,

step(value, value) * value;

みたいにすると,NaNである場合には0になり,NaN以外の場合にはvalueで返すことが出来るらしい…。
と書いてあったのですが,よくよく考えてみるとstep(value, value)のところは確かにゼロとなるので問題ないなさそうに思えるのですが,NaNとの四則演算結果はNaNになるような気がします。実際に試してみたのですが,やっぱり駄目でした。

あとは,Googleのfilamentに

#define MEDIUMP_FLT_MAX    65504.0
#define saturateMediump(x) min(x, MEDIUMP_FLT_MAX)

みたいなのが書いてあったので,下記のような実装を試してみました。

static const float HALF_MAX = 65504.0;
clamp(value, 0.0f, HALF_MAX);

結果として,NaNが発生しなくなりバグが解消されるようになりました。

特に物理ベースレンダリングに移行する際に,GGXモデルなどを使うことが多いと思うのですが,定式化されているものが除算を含む形で定義されているので,実装する際に除算をしなくてはならないということが発生します。
GGXの計算する際に,分母がゼロになることにより,ゼロ除算が発生し,計算結果がNaNになる可能性があります。
また,pow()関数を使用している場合は,MSDNのドキュメント にも書いてあるように第1引数が0未満の場合はNaNになり,第1引数・第2引数がともにゼロの場合はハードウェア依存によりNaNが発生する可能性があります。

昔からよくある方法としてゼロ除算を発生させないために,例えば1e-7fなどのような小さな値を分母側に足しておき,ゼロにならないようにオフセットを掛けるという回避方法もあるのですが,この方法は除算には適用できるのだけれども,pow()関数には適用できません。

GGXを用いたライティング計算の場合は,ゼロ除算が発生する可能性があるため最初はこのオフセットを足し込むというやり方を取っていたのですが,これをやってしまうと計算結果が変わってしまい,ハイライトが鈍くなるなど見た目上に影響を及ぼすことが分かりました。
PBRやっているはずなのに,何故かなまった感じの画が出る場合には,下駄をはかせてハイライトを潰してしまっていないかどうか確認するのを強くお勧めします。
下駄をはかせている箇所を下駄を取って普通に除算し,最後に上記のようなNaNをはじく処理をいれることで,ハイライトがなまったり,ライティング以降のレンダリングパスでバグるという問題を解決することが出来ます。また,pow()関数を用いる場合にも適用でき,NaNをなくすることができるので,NaNで困っている方はぜひ試してみてください。

※もっとより良い方法ご存知であれば,是非教えてください!

超雑訳 CHC++ : Coherent Hierarchical Culling Revisited

Share

こんばんわ。
Pocolです。
とあるサイトを見て,温めているネタの温度が一気に下がりそうな予感がしたので,温度が下がる前に放出します。

会社でシャドウマップが重い!重い!と文句をつけられるので,カリングに効きそうな手法を探してみました。昔実装した,オクルージョンカリング関係でググったら,CHCが出てきたので,読んでみました。
いつもながら,適当に訳しているので,誤字・誤訳が多々あると思います。ご指摘いただける場合は,正しい翻訳例と共に指摘していただけると幸いです。
(さらに…)

Adaptive Depth Bias For Shadow Mapについて。

Share

こんにちわ,Pocolです。
仕事で実装しているやつのアクネが酷く,Adaptive Depth Bias For Shadow Mapを読んでみたので,まとめておきます。

論文和訳

2. Adaptive Depth Bias
 出発点として,従来のシャドウマップが持つ問題の解決をします。その他のシャドウマッピングアルゴリズムへの我々の手法の拡張は簡単で,以下のセクションで詳しく説明します。
 従来のシャドウマップにおけるシャドウアクネは主にシャドウマップ中におけるカメラビューからのシェーディング点のサンプルのミスマッチによって発生します。誤ったセルフシャドウイングを排除するために必要な深度バイアス量は,フラグメントによって異なります。図2に示すように,\(F_1\)と\(F_2\)が同じ平面にあると仮定します。\(F_1\)はシャドウマップ上でサンプルされた\(F_2\)によってシャドウになります。\(F_2\)は\(F_1\)のオクルーダーまたは,対応するテクセルのオクルーダーと呼ばれます。

※図は,[Dou 2014]より引用

\(F_1\)に対する間違ったシャドウイングを取り除くために必要とされる最小の深度バイアスあるいは最適な深度バイアスは\(L\)です。最適なバイアス以外に,オクルーダー\(F_2\)のぴったり真上に\(F_1\)を移動させるためには小さなイプシロン値が必要とされます。適応的深度バイアスを計算するための定式は単純に次のようになります。

\begin{eqnarray}
adaptiveDepthBias = optimalDepthBias + adaptiveEpsilon \tag{1}
\end{eqnarray}

2.1 Optimal Depth Bias
与えられたフラグメントに対して最適な深度バイアスを計算するために,最初に潜在的なオクルーダーを特定します。
平面的なオクルーダーであると仮定すると,フラグメント\(F_1\)が与えられると,その潜在的なオクルーダーは\(F_2\)は\(\vec{R}\)と\(P\)の交点として計算することができます。
ここで,ベクトル\(\vec{R}\)はテクセル中心Cを通して光源からレイトレースされるもので,\(P\)は\(F_1\)と法線\(N\)によって定義される接線平面です。
最適なバイアスは\(F_1\)と\(F_2\)の間の深度の差となります。実際には,optimalDepthBiasを明示的に計算するよりも,可視性をチェックするために接線平面のフラグメントとシャドウマップのテクセル中心を通る光線の交差点の深度を使用します。
平坦な平面上にフラグメントがあると仮定すると,多くの実際のシーンにおいて共通の状況に近い近似を得ることができます。

2.2 Adaptive Epsilon
 潜在的なオクルーダー上でフラグメントをシフトさせるためには,最適な深度バイアスの代わりに適切なイプシロンの値が必要となります。しかしながら,定数イプシロンは深度値が大抵非線形に圧縮されるのでうまく動作しません。
従って,定数イプシロンを直接利用する代わりに,定数イプシロンを深度圧縮関数に基づいて適応的に変換します。
 適応的イプシロンは次の深度圧縮関数と定数イプシロンに基づいて計算されます。

\begin{eqnarray}
\epsilon &=& f’ (x) \Delta x \tag{2} \\
\Delta x &=& sceneScale \times K \tag{3}
\end{eqnarray}

 ここで,\(\epsilon\)はバイアスに使用するための適応的なイプシロンを示し,\(x\)はシェードされたフラグメントの正規化されていない深度値,\(\Delta x\)はワールド空間座標系における正規化されていないイプシロン値,\(f(x)\)はニア平面からファー平面への[0, 1]の深度値をマップする任意の深度圧縮関数,\(sceneScale\)は真のバウンディングボックスの対角線の長さを示し,\(K\)は定数となります。実際には標準的なOpenGLの深度圧縮関数

\begin{eqnarray}
a &=& – \frac{ lf + ln }{ lf – ln} \tag{4} \\
b &=& – \frac{ 2 \times lf \times ln }{ lf – ln } \tag{5} \\
f(x) &=& \frac{ -a \times x + b }{ 2 \times x } + \frac{ 1 }{ 2 } \tag{6}
\end{eqnarray}

ただし,\(ln\)と\(lf\)はライトのニア平面とファー平面の距離を表し,\(x\)は実数深度値\((x \in [-lf, -ln] )\)で,\(f(x)\)は圧縮された深度値\((f(x) \in [0, 1])\)を示します。
式(6)から,次を得ます:

\begin{eqnarray}
f(x)’ &=& \frac{ -b }{ 2 \times x^2 } \tag{7} \\
x &=& \frac{ b }{ 2f(x) + a – 1 } \tag{8}
\end{eqnarray}

式(2), 式(7), 式(8)をまとめ,適応的イプシロンのための定式を得ます:

\begin{eqnarray}
\epsilon = \frac{ ( 2f(x) + a – 1 )^2 }{ -2 \times b } \times \Delta x \tag{9}
\end{eqnarray}

\(a\)と\(b\)を式(4)と式(5)で置き換え,我々が提案する適応的イプシロンは以下のようになります:

\begin{eqnarray}
\epsilon = \frac{ (lf – depth \times (lf – ln) )^2 }{ lf \times ln \times (lf – ln) } \times sceneScale \times K \tag{10}
\end{eqnarray}

ここで,\(depth\)は与えられたフラグメントに対する正規化された深度値を表します。我々のすべての実験では,\(K = 0.0001\)を設定します。

3. Implementation
それでは,従来のシャドウマップ,放物面シャドウマップ,ボクセライズドシャドウボリュームについてのGLSL上での我々の手法の実装を説明します。

3.1. Adaptive Depth Bias for Traditional Shadow Map
従来のシャドウマップにおける各フラグメントに対して,潜在的なオクルーダーを特定し,適応的イプシロンを計算し,可視性チェックの前に潜在的オクルーダーのすぐ上にフラグメントをシフトします。
アルゴリズム1における疑似コードとリスト1に明確なGLSLの実装を詳細に示します。入力パラメータはライト空間におけるフラグメントの法線,ワールド空間におけるフラグメントの位置座標,そしてシャドウマップの解像度です。

※図は,[Dou 2014]より引用

※リストは,[Dou 2014]より引用

3.2 Adaptive Depth Bias for Parabloid Shadow Map
 半球上,および無指向性光源の場合は,全視野の光をマッピングする必要があります; キューブマップと放物面マップの2つは一般的なマッピングです。半球あるいは全方向位照明に対してキューブマップを使用するとき,セクション3.1で説明した適応的深度は直接使用することが可能です。放物面シャドウマップを用いた適用的バイアスについての可視性の疑似コードは従来のシャドウマップについての疑似コードと同じです。潜在的なオクルーダーの特定方法においてのみ違いがあります。
 図3(右)に示すように,\(F_1\)は放物面シャドウマップ上の\(F_2\)によって間違ってシャドウになります。

※図は,[Dou 2014]より引用

\(F_1\)について,\(L\)は最適な深度バイアスです。従来のシャドウマップの場合と同様に,各フラグメントについて,放物面シャドウマップにおいて対応するテクセルを特定します。次に,フラグメントのローカル接線平面とそのテクセルのシャドウサンプルレイを交差させることによって,テクセル内の潜在的なオクルーダーを得ます。
 下図に示すように,フラグメント\(F_1\)が与えられると,放物面シャドウマップへとそれを射影し,対応するテクセル中心\(C(x_c, y_c, 0)\)を特定します。\(H(x_h, y_h, z_h)\)は放物面上の衝突点を表します。\(N\)は\(H\)におけるサーフェイス法線を表します。次のようにサンプルシャドウレイ\(\vec{R}\)を得ることができます。

\begin{eqnarray}
\vec{ R } &=& 2 \times ( \vec{ d_0 } \cdot \vec{ N } ) \times \vec{ d_0 } – \vec{ d_0 } \\
\vec{ d_0 } &=& \lbrack 0, 0, 1 \rbrack ^{T} \\
\vec{ N } &=& \left\lbrack \frac{ x_c }{ z_h }, \frac{ y_c }{ z_h }, \frac{ 1 }{ z_h } \right\rbrack ^{T} \\
z_h &=& \frac{ 1 }{ 2 } – \frac{ 1 }{ 2 } \times ( x_{c}^{2} + y_{c}^{2} )
\end{eqnarray}

 テクセル\(T\)内の潜在的なオクルーダーは\(F_2\)で,\(\vec{ R }\)と\(F_1\)のローカル接線平面\(P\)の交差点です。誤ったセルフシャドウイングを取り除くために,セクション2.2で説明した適応的イプシロンで\(F_2\)のぴったり真上に\(F_1\)を移動させます。以下はシェーダコードを示しています。入力パラメータは,フラグ面の位置,ライトの位置,シャドウマップの解像度です。従来のシャドウマップと同じように,放物面シャドウマップにおけるテクセル中心を横断するライトレイを事前計算することも可能です。

※リストは,[Dou 2014]より引用

3.3 Adaptive Depth Bias for Voxelized Shadow Volume
ボクセライズドシャドウボリューム(VSV)は,関与媒質中におけるシャドウとサーフェイスのシャドウの両方を計算可能にします。従来のシャドウマッピングと同様に,VSVでサーフェイスのシャドウを計算するとジオメトリの離散化によって視覚的なアーティファクトが発生します。従来のシャドウマップとは異なりVSVはバイナリ情報を持つシャドウを表現します。ボクセルは,フラグメントが遮蔽されるか,ほかの遮蔽オブジェクトの影中に存在する場合のみ遮蔽されます。

 VSVは,バイナリのエピポラーボクセルグリッドを持つシャドウを表現します。図4に示すように,エピポラー空間は眼と光を結ぶ線であるエピポールに対して定義されます。

※図は,[Dou 2014]より引用

3つの角度がエピポーラ点を定義します。したがって,エピポーラ空間における点は:\( (\alpha\ \phi, \theta) \in ( \lbrack 0, \pi \rbrack, \lbrack 0, 2 \pi ), \lbrack 0, \pi \rbrack ) \)となります。角度\(\theta\)はあるベクトル(慣例的に,カメラのアップベクトル)に対するエピポーラ平面を定義します。エピポーラ平面上の点は視点とライト点に対して定義されます。角度\(\alpha\)はビューレイに平行な軸を決定し,角度\(\phi\)はライトレイに平行な軸を定義します。図4に示すように,フラグメント\(F_1\)が与えられると,与えられたフラグメントを対応するボクセル\(V_1(\alpha, \phi, \theta)\)に射影し,[Wyman 2011]で説明されるような\(V_1\)のボクセル中心\(C_{V_1} (\alpha_c, \phi_c, \theta_c) \)を得ます。シャドウサンプル例を生成するために\(C_{V1}\)をデカルト座標に変換する代わりに,シャドウサンプルレイを生成するために\(\theta_c\)によって定義される平面上でlightからeyeへのベクトルを変換します:

\begin{eqnarray}
\vec{ V } &=& Eye – Light \\
\vec{ R } &=& Rotate( \vec{ V }, \phi_c )
\end{eqnarray}

このとき,\(F_1\)の潜在的なオクルーダー\(F_2\)を得るために\(F_1\)のローカル接線平面\(P\)と\(\vec{ R }\)を交差させます。誤ったセルフシャドウイングを取り除くために,潜在的なシャドウをキャストするボクセルの真上に\(F_1\)をシフトする必要があります。ですが,潜在的なシャドウキャストとして\(F_2\)の対応するボクセル\(V_2\)を直接使用することは,図5に示すように偽陽性誤差を引き起こします。

※図は,[Dou 2014]より引用

疑似コードとGLSLシェーダは以下のようになります。入力パラメータはフラグメントの位置,フラグメントの法線,ライト位置そしてテクスチャの解像度で,それらはVSVが保持しています。すべてのパラメータはカメラ空間となります。

※図は,[Dou 2014]より引用

光源に近い\(V_2\)に隣接するボクセルが\(V_3\)であると仮定します。\(F_2\)がボクセル中心あるいは後ろにある場合に,可視性チェックのために\(V_2\)を使用します。\(F_2\)がボクセル中心の場合は,可視性チェックのために\(V_3\)を使用します。上記は明確にするための疑似コードを表しています。

※リストは,[Dou 2014]より引用

4. Results and Discussion
我々はOpenGL/GLSLとC++で手法を実装しました。すべてのテストシーンはIntel(R) Cores(TM) i7 CPU at 2.93 GHzとNVIDIA GTX580のグラフィックスカードを装備するマシン上で描画しました。すべての画像の出力は1024×1024の解像度です。テストシーンにおいて,VSVはWymanらの研究のように[2011],シャドウマップのリサンプリングとプレフィックススキャンを適用することによって生成されます。
 図6と図7は複雑なシーンにおいて従来のシャドウマップと法物面シャドウマップに対して定数バイアスとスロープスケール深度バイアス[King 2004]と我々の手法の比較となります。

※図は,[Dou 2014]より引用

※図は,[Dou 2014]より引用

我々は参考画像の生成に2層深度レイヤー手法[Weiskopf and Ertl 2003]を使用しました。定数バイアスと傾斜スケール深度バイアスでは,ライトに近いオブジェクトはシャドウアクネを持ちますが,ライトから遠いオブジェクトは強いシャドウの分離に悩まされます。テストシーンでは,本手法は2層深度シャドウマップに近い結果が得られますが,パフォーマンスは大幅に向上しています。

 図9(右)に図示するように,遮蔽されたフラグメントのローカル接線平面がライトレイとほぼ平行である場合に,過剰なバイアスを被り適応的バイアスは予期しないノイズとなります。しかし,これはローカル接線平面がライトレイにほぼ平行である場合のみ発生し,LambertianやPhongのような一般的なマテリアルにおいて観測者にはほとんど輝度を与えません。ゆえに,この問題はシェーディングが適用された後に消えてしまいます。

※図は,[Dou 2014]より引用

 表1は図6に示したシーンに対応するパフォーマンスを示しています。

※表は,[Dou 2014]より引用

定数深度バイアスと傾斜スケール深度バイアスの両方はわずかなオーバーヘッドを追加します。2層深度バイアスは2つのレンダリングパスを持ち,シャドウマップの生成に2倍の時間がかかります。さらに,余分なテクスチャルックアップは0.6ms近くで,定数バイアスと比較するとレンダリング時間が18%も長くなります。シェーディングステージでは,本手法における適応的深度バイアスを計算するためのコストはデュアルレイヤーベースの手法における余分のテクスチャルックアップコストに近くなります。しかしながら,追加のレンダリングパス持つので2層ベースの手法は我々の手法と比べて50%近くレンダリング時間が長くなります。図10は図7におけるシーン(19Mポリゴン)のパフォーマンスチャートを示しています。シャドウマップの解像度が上がるにつれて,傾斜スケール深度のコストは定数バイアスと比較して約5%のレンダリング時間を要します。我々の手法は定数バイアスと比較すると20%近くレンダリング時間を要し,誤ったシャドウイングが大幅に少ないです。我々の手法と比較すると,2層深度マップは同等の画像品質を与えますが,シャドウマップ解像度が4096×4096未満の場合はレンダリング時間が50%長くなり,シャドウマップ解像度が増加し続ける場合はさらに50%以上のコストがかかります。

※図は,[Dou 2014]より引用

 図8は,VSVに適応的バイアスを適用した結果を示しています。VSVは書くボクセル内にバイナリ値のみしか持たないので,スロープスケール深度バイアスや2層化のような他の深度バイアスアルゴリズムの拡張するという率直な方法はVSVに対してはうまく機能しません。そのため,適応的バイアスと定数バイアスの結果のみを比較します。VSVの非均一エピポラーボクセルグリッドと視点依存の性質は,定数バイアスをうまく働かせづらいものにしています。定数バイアスでは,アーチ上に間違ったセルフシャドウイングが残っていますが,離れた青いカーテンではシャドウの乖離を既に被っています。適応的深度バイアスは,間違ったセルシャドウイングを低減し,固定の定数バイアスよりも詳細なシャドウを保ちます。

※図は,[Dou 2014]より引用

この技術の主な制限は,各フラグメントがその潜在的なオクルーダーと一緒に同じ平面上に存在するという仮定に由来します。図11は,単純なコーナーにおける場合を示しています。左側の画像では,フラグメントは過剰にシフトされていますが,誤ったセルフシャドウイングを削除することができます。しかし,右側の画像では,適応的深度バイアスは間違ったシャドウイングの削除に失敗します。

※図は,[Dou 2014]より引用

平面的なオクルーダーの仮定は,シャドウマップの解像度が低いあるいはオブジェクトが光源から非常に遠くに離れた大きなシーンスケールでは破綻する可能性があります。図12では,光源はオブジェクトから遠いです。1024×1024のシャドウマップでは,適応的バイアスは間違ったセルフシャドウの多くを排除することができます。512×512のシャドウマップでは,壁の隅やモデルの凹んだ領域において間違ったシャドウが現れ,ドラゴン上にシャドウの乖離が表示されます。

※図は,[Dou 2014]より引用

VSVの場合,エピポラーサンプリングの性質は,ライトが観測者の後ろにある場合に平面的なオクルーダーの仮定を維持することを困難にします。図13に示すように,1024×1024×512のバイナリボリュームを持つ適応的深度バイアスは多くの誤った背フルシャドウを排除することができます。ボリュームの解像度を512×512×512へと減らした際に,シャドウアクネが現れ始めます。

※図は,[Dou 2014]より引用

感想

読んだ感じだと,結構よさげに思いました。
やっていることとしては,上の和訳に書いた通り,きちんとしたピクセル中心をピクセルシェーダ内で演算して求めておき,そこからレイトレして正しいバイアス値を求めるという手法です。
図11にあるとおり,\(F_1\)と\(F_2\)が同一平面上にない場合は,誤った計算になります。特にシャドウマップの解像度が低い場合とか,こういった状況が発生しやすくなります。そのためか,論文の結果部分にも1024×1024の場合は殆ど誤判定を排除できるが,512×512にした場合は誤判定がでるという記述があります。
普通の定数バイアスとか深度傾斜バイアスなどの手法と比べるとコストが約20%増しというのが個人的には導入を悩むところですね。このコスト増が許容できるのであれば,積極的に取り入れるべきと個人的には思います。
大抵のアーティファクトが消えるのであれば,2割のコスト増でも導入するに値しそうな気がするのですが,同一平面上にあるという仮定を満たすことが手元で作っているゲームだと満たせそうにないのと,そもそもシャドウマップのコストが高すぎて,これ以上負荷を上げられないという問題があり,今やっているプロジェクトでは採用を見送りました。
PCや次世代機等では,コスト増は軽微で問題にならなそうな気がするので,シャドウマップを使っているのであれば採用してもよいのではないかなぁと思います。

参考文献

[Dou 2014] Hang Dou, Yajie Yan, Ehan Kerzner, Zeng Dai, Chris Wyman, “Adaptive Depth Bias for Shadow Maps”, Journal of Computer Graphics Techniques (JCGT), Vol.3, No.4, pp.146-162, 2014.

DirectX Raytracing

Share

GDC2018でDirectX Raytracingが発表されたようです。
実験段階のSDKが公開されています。
http://forums.directxtech.com/index.php?topic=5860.0

今日帰ったら,本の執筆の息抜きとしてちょっと触ってみようかなと思います。