1-Phase Occlusion Culling

完全に私的な実装メモです。
通常,オクルージョンカリングを実装する際は,GPU Zenやもんしょさんのサイトに載っているように2-Phaseでジオメトリ描画して実装するのが普通かと思います。
少し前ですが,GDC 2021の“Samurai Landscapes: Building and Rendering Tsushima Island on PS4”というセッションの,43:45あたりから,Occlusion-Cullingについての説明があり,Ghost of Tsushimaの実装では,前フレームの深度バッファと,それらから保守的(conservative)に作成したミップレベル,エピポラーサーチを用いて,現在フレームの深度バッファを復元し,1回のジオメトリ描画でオクルージョンカリングを実装する方法が紹介されています。
説明が分かりやすいので,アルゴリズムについては元動画を参照してください。
馬鹿まじめに線形探索をせずに,ミップマップを使って検索するのがアルゴリズムのキモみたいです。

一応実装コードが紹介されていますが,動画の品質が低すぎて全然良く見えませんw。
そこで,それっぽい感じに見えるコードを自分で推測しながら書いてみました。
推測で書いているのと,きちんと動作検証もしていないので,バグっている可能性があるので,まんまコピペで使用して不都合・不利益が発生しても何ら責任は負いませんのでご注意ください。
もし、「ここはこうじゃね?」とかアドバイスあればコメントください。

Depth Reprojection
[hlsl]
// Forward-project last frame’s depth to this frame’s space
{
float zPrev = pSrt->m_texIn[dispatchThreadId.xy];
float zDepthPrev = DepthFromWorld(zPrev, pSrt->m_vecRecoryHypebolicDepth);

float4 posClipPrev = float4(VecClipFromUv(uvThread), zDepthPrev, 1.0f);
posClipPrev *= zPrev; // This ensure that zCur will end up in posClip.w;

float4 posClip = mul(posClipPrev, pSrt->m_matClipPrevToClipCur);
posClip.xyz /= posClip.w;

float2 uv = UvFromVecClip(posClip.xy);

if (all(uv >= pSrc->m_rectScissor.xy) && all(uv >= pSrc->m_rectScissor.zw))
{
// NOTE : z ends up in posClip.w so we can use that directly.
float z = posClip.w;

if (z < pSrt->m_zNear)
z = pSrt->m_zFar;

// NOTE : Add a small bias to resolve self-occlusion artifacts
z += pSrt->m_dsBias;

if (z > pSrt->m_zFar || zPrev >= pSrt->m_zFar)
z = pSrt->m_zFar;

float2 xy = floor(uv * pSrt->m_texture.m_dXy); // 多分、テクスチャサイズを乗算してウィンドウサイズに戻している。

AtomicMax(pSrt->m_texOut[xy], z);
}
}

{
float4 posClip = float4(VecClipFromUv(uvThread), 0.0f, 1.0f);

float4 posClipPrev = mul(posClip, pSrt->matClipCurToClipPrev);

if (any(abs(posClipPrev.xy) >= abs(posClipPrev.w))
{
float2 uv = UvFromVecClip(posClipPrev.xy / posClipPrev.w);

float z = pSrt->m_texIn->SampleLOD(pSrt->m_sampler, uv, 0) * pSrt->m_dsBias;

AtomicMax(pSrt->m_texOut[dispatchThreadId.xy], z);
}
}
[/hlsl]

Hole-Filling
[hlsl]
float2 uv = (dispatchThreadId.xy + 0.5f.xx) * pSrt->m_texture.m_div;
float zMax = pSrt->m_texIn.SampleLOD(pSrt->m_sampler, uv, 0);

if (zMax == -FLT_MAX)
{
float2 normalEpipole = normalize(pSrt->m_uvEpipole * uv);
float2 dUvStep = pSrt->m_texture.m_div * normalEpipole;

for(uint iMip = 1; iMip <= pSrt->m_iMipLast; ++iMip)
{
float2 uvSearch = uv – dUvStep;
dUvStep *= 2.0;

float z = pSrt->m_texIn.SampleLOD(pSrt->m_sampler, uvSearch, iMip);

if (z != -FLT_MAX)
{
z = max(z, pSrt->m_texOut[dispatchThreadId.xy]);
z *= pSrt->m_dsBias;
z = min(z, pSrt->m_zFar);
pSrt->m_texOut[dispatchThreadId.xy] = z;

return;
}
}

if (zMax < pSrt->m_zNear)
zMax = pSrt->m_zFar;

zMax = min(zMax, pSrt->m_zFar);

pSrt->m_texOut[dispatchThreadId.xy] = zMax;
}
[/hlsl]

premake5覚書

こんねね~。Pocolです。
premake5の覚書をしておこうと思います。

XBox系のプラットフォームの場合は,Billzard社がpremake-consolesというGithubリポジトリを公開しているので,これを使えばよさそうです。
https://github.com/Blizzard/premake-consoles

で,青いプラットフォームの場合は,これまたBillzard社がソースコードをフォーラムにて公開しているので,それを使えばいいです。

問題はこれらが使えない場合です。既にpremake作っちゃっているやつとか,これから出てくる新規プラットフォームの場合とかですね。
自力で実装する必要があります。ほとんどの場合はSDKがC++だと思われるので,C++のコンパイルは問題なく対応できるでしょう。
じゃ、何が問題か?
そうです。前回やったシェーダ設定ですね。恐らく問題になるでしょう。
SDKでVS拡張が提供されている場合は,プロジェクトファイルに記述すればいいので,premakeを拡張していけばいいです。

やり方としては,
https://github.com/premake/premake-core/blob/master/modules/vstudio/vs2010_vcxproj.lua
のFxCompileおよびfxCompileで記載されている箇所を参考にします。

premake用に公開するAPIは,
https://github.com/premake/premake-core/blob/master/modules/vstudio/_preload.lua
のshadertypeとか,shadermodel,shaderincludedirsあたりを参考にLuaに実装を付け足します。

先ほど言った、VS拡張が提供されている場合はこんな感じでいいのですが,じゃ、提供されていない場合はどうすればいいでしょうか?
残念ながら,自分で作るしかないですね。

作り方については,以前に書いた「カスタムビルドルール!」「Visual Studioと格闘中…。」の記事で紹介しているので,これを参考に実装すれば良いでしょう。

…ということでコンソールプラットフォーム用のpremakeのシェーダビルド対応について,覚書を書いてみました。
殆どの人には,全く役に立たないんですが,自分だけには役立つというしょうもない記事でした。

premake5でシェーダ設定する

こんこよー。Pocolです。

ちと分け合って,premake5でプロジェクトを作成する際に,シェーダのコンパイル設定を含めてしまいたいな~と思いました。
premake5からHLSLがサポートされているようなので,実際に書いてみました。
リポジトリは下記です。
https://github.com/ProjectAsura/premake_test

プロジェクトを生成するluaは下記のような感じで書きます。

local project_dir="project"
location(project_dir)

-- ソリューション名.
workspace "SampleSolution"
    -- 構成名.
    configurations { "Debug", "Develop", "Release" }

-- プロジェクト名.
project "SampleProject"

    kind "ConsoleApp"
    language "C++"

    -- 出力ディレクトリ.
    targetdir "bin/%{cfg.buildcfg}"

    -- インクルードディレクトリ.
    includedirs { "include/**" }
    shaderincludedirs { "$(ProjectDir)../res/shader" }

    -- 対称ファイル
    files { "**.h", "**.cpp", "**.hlsl", "**.hlsli" }

    -- シェーダモデルとエントリー名設定.
    shadermodel "6.5"
    shaderentry "main"

    -- ヘッダ変数名とヘッダーファイル名設定.
    shadervariablename "%%(Filename)"
    shaderheaderfileoutput "$(ProjectDir)../res/shader/Compiled/%%(Filename).inc"

    -- シェーダヘッダファイル設定.
    filter { "files:**.hlsli" }
        flags "ExcludeFromBuild"

    -- 頂点シェーダ設定.
    filter { "files:**_vs.hlsl" }
        shadertype "Vertex"

    -- ピクセルシェーダ設定.
    filter { "files:**_ps.hlsl" }
        shadertype "Pixel"

    -- コンピュートシェーダ設定.
    filter { "files:**_cs.hlsl" }
        shadertype "Compute"

    -- ジオメトリシェーダ設定.
    filter { "files:**_gs.hlsl" }
        shadertype "Geometry"

    -- ドメインシェーダ設定.
    filter { "files:**_ds.hlsl" }
        shadertype "Domain"

    -- ハルシェーダ設定.
    filter { "files:**_hs.hlsl" }
        shadertype "Hull"

    -- 増幅シェーダ設定.
    filter { "files:**_as.hlsl" }
        shadertype "Amplification"

    -- メッシュシェーダ設定.
    filter { "files:**_ms.hlsl" }
        shadertype "Mesh"

    -- 構成設定.
    filter { "configurations:Debug" }
        defines {"DEBUG"}
        shaderdefines {"DEBUG=1"}
        symbols "On"
    filter { "configurations:Develop" }
        defines {"NDEBUG"}
        shaderdefines {"NDEBUG=1"}
        optimize "On"
        symbols "On"
    filter { "configurations:Release" }
        defines {"NDEBUG"}
        shaderdefines {"NDEBUG=1"}
        optimize "On"

このluaファイルを使って,サンプルのリポジトリで下記のコマンドで,プロジェクトを生成します。

.\premake5.exe vs2022 --file=my_premake.lua

プロジェクトを生成すると projectフォルダが出来上がり,そこにソリューションファイルが作成されます。ソリューションファイルを開くと次の画像のような感じになります。

シェーダファイルのプロパティを見ると次のように設定されているはずです。

これで,シェーダのビルドが出来るようになるはずです。
下記のPremakeドキュメントに設定できる項目が記載されているので,より細かい設定をしたい方はドキュメントを参照すると良いと思います。

…ということで,premake5でのシェーダ設定方法について紹介してみました!

ReSTIRがサッパリ解からない。

この記事はレイトレ合宿9のアドベントカレンダー2023 7/13の担当分の記事です。

こんにちわ,Pocolです。
最近,ReSTIRを調べているのですが,未だにサッパリ解かりません(特にGRIS周辺)。

今回は,自分の理解を促すために自分なりにまとめたスライドを作ってみました!
しかし,内容をサッパリ理解していない人が作っているスライドなので内容が盛大に間違えている場合があります。
間違いがあった場合は,間違い箇所の指摘とその修正例を教えて頂けると大変ありがたいです。あるいは,「そういう説明よりもこういう説明の方がいいよ」とか「この項目の説明抜けてる!」とか,アドバイスも貰えると嬉しいです。
または,有識者が分かりやすい解説記事を書いてもらえると大変助かります。
あと,論文の内容を切り貼りしている関係で,数式の表記ゆれがあります。予め注意してください。

Bilateral Upsampling

こんにちわ,Pocolです。
今日はバイラテラルアップサンプリングについてメモをしておこうと思います。
パフォーマンスを稼ぐために,低解像度で描画しておき,それを元解像度に戻したいという場面が,ゲームグラフィックスでは多々出てきます。具体的には,SSAOやSSRなどの計算です。
ただ単にバイリニア補間で元解像度に戻してしまうとエッジ部分などでアーティファクトが発生してしまうことがあります。
こうしたアーティファクトを避けるために使われる手法の中の一つとして,Bilateral Upsamplingがあります。

通常のバイリニア補間は4点から計算を行います。

バイラテラルアップサンプリングは,法線と深度によってバイリニアウェイトを修正します。サンプルは以下のように,バイリニアの重み,法線の類似度による重み,深度の類似度による重みの3つによって重みづけされます。

バイリニアの重みは以下です。

法線の重みは次のように求めます。

深度の重みは次のように求めます。

以上から求められた重みを使ってサンプルを重みづけします。下図の通りです。

実装例ですが,もんしょさんが「DirectXの話 第121回 Bilateral Upsampling」の記事にてサンプルコードをアップしてくださっています。有難いです。
シェーダコードを抜粋すると下記の通りです。

float4 RenderUpsamplingPS( OutputVS inPixel ) : SV_TARGET
{
	const float2 kScreenSize = g_ScreenParam.xy * 2.0;
	const float2 kScreenHalfSize = g_ScreenParam.xy;
	const float4 kBilinearWeights[4] =
	{
		float4( 9.0/16.0, 3.0/16.0, 3.0/16.0, 1.0/16.0 ),
		float4( 3.0/16.0, 9.0/16.0, 1.0/16.0, 3.0/16.0 ),
		float4( 3.0/16.0, 1.0/16.0, 9.0/16.0, 3.0/16.0 ),
		float4( 1.0/16.0, 3.0/16.0, 3.0/16.0, 9.0/16.0 )
	};

	// Hi-Resピクセルのインデックスを求める
	int2 hiResUV = (int2)(inPixel.texCoord0 * kScreenSize + float2(0.1, 0.1));
	int hiResIndex = (1 - (hiResUV.y & 0x01)) * 2 + (1 - (hiResUV.x & 0x01));
	float4 hiResND = texNormalDepth.Load( int3(hiResUV, 0), int2(0, 0) );

	// Low-Resから4ピクセルの法線・深度を求める
	int2 lowResUV = (int2)(inPixel.texCoord0 * kScreenHalfSize.xy + float2(0.1, 0.1));
	float4 lowResND[4];
	float lowResAO[4];
	switch (hiResIndex)
	{
	case 0:
		lowResND[0] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(0, 0) );
		lowResND[1] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(1, 0) );
		lowResND[2] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(0, 1) );
		lowResND[3] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(1, 1) );
		lowResAO[0] = texHDAO.Load( int3(lowResUV, 0), int2(0, 0) ).r;
		lowResAO[1] = texHDAO.Load( int3(lowResUV, 0), int2(1, 0) ).r;
		lowResAO[2] = texHDAO.Load( int3(lowResUV, 0), int2(0, 1) ).r;
		lowResAO[3] = texHDAO.Load( int3(lowResUV, 0), int2(1, 1) ).r;
		break;
	case 1:
		lowResND[0] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(-1, 0) );
		lowResND[1] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(0, 0) );
		lowResND[2] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(-1, 1) );
		lowResND[3] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(0, 1) );
		lowResAO[0] = texHDAO.Load( int3(lowResUV, 0), int2(-1, 0) ).r;
		lowResAO[1] = texHDAO.Load( int3(lowResUV, 0), int2(0, 0) ).r;
		lowResAO[2] = texHDAO.Load( int3(lowResUV, 0), int2(-1, 1) ).r;
		lowResAO[3] = texHDAO.Load( int3(lowResUV, 0), int2(0, 1) ).r;
		break;
	case 2:
		lowResND[0] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(0, -1) );
		lowResND[1] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(1, -1) );
		lowResND[2] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(0, 0) );
		lowResND[3] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(1, 0) );
		lowResAO[0] = texHDAO.Load( int3(lowResUV, 0), int2(0, -1) ).r;
		lowResAO[1] = texHDAO.Load( int3(lowResUV, 0), int2(1, -1) ).r;
		lowResAO[2] = texHDAO.Load( int3(lowResUV, 0), int2(0, 0) ).r;
		lowResAO[3] = texHDAO.Load( int3(lowResUV, 0), int2(1, 0) ).r;
		break;
	case 3:
		lowResND[0] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(-1, -1) );
		lowResND[1] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(0, -1) );
		lowResND[2] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(-1, 0) );
		lowResND[3] = texHalfNormalDepth.Load( int3(lowResUV, 0), int2(0, 0) );
		lowResAO[0] = texHDAO.Load( int3(lowResUV, 0), int2(-1, -1) ).r;
		lowResAO[1] = texHDAO.Load( int3(lowResUV, 0), int2(0, -1) ).r;
		lowResAO[2] = texHDAO.Load( int3(lowResUV, 0), int2(-1, 0) ).r;
		lowResAO[3] = texHDAO.Load( int3(lowResUV, 0), int2(0, 0) ).r;
		break;
	}

	// 法線のウェイトを求める
	float totalWeight = 0.0;
	float ao = 0.0;
	for( int i = 0; i < 4; ++i )
	{
		// 法線のウェイトを求める
		float normalWeight = dot( lowResND[i].xyz, hiResND.xyz );
		normalWeight = pow( saturate(normalWeight), 32.0 );

		// 深度のウェイトを求める
		float depthDiff = hiResND.w - lowResND[i].w;
		float depthWeight = 1.0 / (1.0 + abs(depthDiff));

		// 総合する
		float weight = normalWeight * depthWeight * kBilinearWeights[hiResIndex][i];
		totalWeight += weight;
		ao += lowResAO[i] * weight;
	}

	ao /= totalWeight;

	return float4(ao, ao, ao, 1);
}

…ということで,Bilateral Upsamplingの話でした。
もしかしたら,Quad系のWaveIntrinsics使って実装した方がナウいかもしれないですね(※試してないので,出来なかったらごめんなさい)。

※追記
Quad Intrinsics使って実装できました。
WaveGetLaneIndex() % 4でhiResIndexを算出します。一度現在位置での,lowResNDとlowResAOを先頭の方でサンプリングしておき,あとはループでQuadReadLaneAt(lowResND, i)と QuadReadLaneAt(lowResAO, i)で,処理対象を持ってきます。これでswitchケース分が丸っとなくせるのと,テクスチャフェッチ回数が減らせます。

GPU Architecture & Compute Shader

いつもメモし忘れて,ツイートを見失うのでメモ。
Carlos Fuentes 氏によるGPU Architecture & Compute Shaderというスライドが非常にわかりやすくておススメです。