本の執筆状況。

こんにちわ,Pocolです。
気になっている方もいるかと思うので,共有しておきましょう。

本の執筆状況ですが,昨年年始頃に仮脱稿を終えまして,出版に向け色々と調整を行っている状況です。
転職の兼ね合いもありまして,副業にあたる・あたらないとかの周りで少々問題がありましたが,それもクリア出来まして
皆様にきちんとしたものをお届けできるようクオリティアップの作業の真っ最中でございます。

来るべき時が来たら「本年度中に」アナウンス致しますので,もうしばらくお待ちいただければと思います。
豪華なレビューアによるきちんとした和書になると思いますので,是非ご期待頂ければと思います。

サンプルプログラムも公開予定ですが,こちらはVisual Studio 2019対応済みです。
公開方法は詳しく決まっておりませんが,サンプルプログラムと合わせて読むことで本が完結する作りになっておりますので,
ご購入前にはご検討頂ければと思います。
※プログラム行数がかなり多いため,書籍のページの都合上要所部分だけを抜粋して載せる仕組みになっていますので予めご注意してください。

また大事なことなのですが,自分は本の著者として実績が無いのと,専門書は部数が出ない。
…という背景事情がありまして,初版の冊数はそんなに多くありません。

そのため…
万一冊数が出てしまった場合は品薄のため買えないという状況が起こりえるのと,
逆に販売数が振るわなかった場合は,すぐに絶版になり二度と手に入れることが出来なくなる可能性がありえます。

特に前者よりも後者の絶版になる可能性の方が高いと思います。
確実に手に入れていただくためには,予約購入して頂くのが今のところ確実かと思います。
来るべきアナウンスがありましたら,早めにご決断して頂き,予約購入して頂くことをお勧めいたします。

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

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程度負荷が増えました。

ラスタライザーの効率性を測るシェーダ

“Optimizing the Graphics Pipeline with Compute”を見ていたら,ラスタライザーの効率性を表示するピクセルシェーダが載っていたので,忘れないようにメモしておこうかと思います。

float3 main() : SV_TARGET0
{
    bool inside = false;
    float2 barycentric = fbGetBarycentricLinearCenter(); //__XB_GetBarycentricCoords_Linear_Center();

    if (barycentric.x >= 0 && barycentric.y >= 0 && barycentric.x + barycentric.y <= 1)
        inside = true;

    uint2 insideBallot = fbBallot(inside); //__XB_Ballot64();
    uint  insideCount  = countbits(insideBallot.x) + countbits(insideBallot.y);
    float insidePrecent = insideCount * (1.0 / 64.0);
    return float3(1 - insidePercent, insidePercent, 0);
}

合っているどうか全くわからないけども,Shader Model 6.0以降で書くと次のような感じ???
SM6.0全然弄ってないから分からん。

float3 main(linear float3 barycentric : SV_Barycentrics) : SV_TARGET0
{
    bool inside = false;
    if (baryenctric.x >= 0 && barycentric.y >= 0 && barycentric.x + barycentric.y <= 1)
        inside = true;

    uint  insideCount  = WaveActiveCountBits(inside);
    float insidePercent = insideCount * (1.0 / 64.0);
    return float3(1 - insidePercent, insidePercent, 0);
}

たぶん,間違っていると思うので誰か正しいコード教えてください。

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

こんちわ、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)

前回の続きです。
今回は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)

新年明けましておめでとうございます。
本年も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は次回書きます。