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