アヒルのある日

株式会社AHIRUの社員ブログです。毎週更新!社員が自由に思いついたことを書きます。

オクルージョンクエリ

こんにちは、みにくい社長です。

前回はカリング手法の概要について書きましたが、オクルージョンカリングの手法の1つ、オクルージョンクエリについて解説します。
オクルージョンクエリはCPUとGPUの両方を使用する手法です。1度GPUにオクルージョンクエリを発行することで、遮蔽されたかどうかを取得することできます。
その結果を次のフレームで使用して描画するかどうかを判断します。次のフレームではオブジェクトの配置が変化している可能性がある為、描画されるべきオブジェクトが遮蔽扱いになることもあり、ポップと呼ばれる現象が時々発生します。
また遮蔽物が多い場合も少ない場合も、クエリの発行にかかる固定コストが追加で発生しますが、遮蔽オブジェクトが多い場合は描画コールを減らすことができます。逆に遮蔽物が少なかった場合は、クエリ発行のコスト分だけ負荷が上がってしまいます。
クエリ発行は必ずしもオブジェクトのメッシュで行う必要はなく、AABBやバウンディングスフィア等で行うことで、クエリ発行の負荷を下げることは可能です。その場合は描画コールがやや増える可能性はありますが、メッシュの転送負荷とのトレードオフになります。

処理手順

  1. クエリの初期化
  2. Zプリパス(深度書き込み)
  3. クエリの発行(深度テスト)
  4. クエリ結果を解決
  5. 次のフレームでクエリ結果をカリングフラグに反映して描画に使用する

クエリの初期化

DX12デバイスの初期化時に、描画バッファ分(トリプルバッファリングなら3つ)のオクルージョンクエリを作成して初期化します。 作成するのは、クエリヒープ、クエリ結果、カリングフラグです。

ComPtr<ID3D12QueryHeap>                _pQueryHeap;        //!< クエリヒープ
ComPtr<ID3D12Resource>                _pQueryResult;  //!< クエリ結果
u64*                                _pOcclusionResult;  //!< オクルージョンの結果(カリングフラグに使用)

// クエリヒープを作成
D3D12_QUERY_HEAP_DESC heapDesc = {};
heapDesc.Count = DRAW_CALL_MAX;
heapDesc.Type = D3D12_QUERY_HEAP_TYPE_OCCLUSION;
auto hResult = getGxRenderDevice()->getDevice()->CreateQueryHeap(&heapDesc, __uuidof(**(_pQueryHeap.ReleaseAndGetAddressOf())), IID_PPV_ARGS_Helper(_pQueryHeap.ReleaseAndGetAddressOf()));
assert(hResult, "CreateQueryHeap error");

_pQueryHeap->SetName(L"queryHeapOcclusion");

// クエリ結果を作成
CD3DX12_HEAP_PROPERTIES heapProperties(D3D12_HEAP_TYPE_READBACK);
auto queryBufferDesc = CD3DX12_RESOURCE_DESC::Buffer(sizeof(u64) * RENDER_QUERY_COUNT_MAX);
hResult = _pDevice->CreateCommittedResource(&heapProperties, D3D12_HEAP_FLAG_NONE, &queryBufferDesc, D3D12_RESOURCE_STATE_COPY_DEST, nullptr, GX_PPV_ARGS(_pQueryResult.ReleaseAndGetAddressOf()));
assert(hResult, "CreateCommittedResource error");
_pQueryResult->SetName(L"queryResult");
// カリングフラグをリセット
_pOcclusionResult = reinterpret_cast<u64*>(malloc(sizeof(u64) * DRAW_CALL_MAX));

クエリの発行

Zプリパスで深度を書き込んだ後に、深度をテストのみにした状態でクエリを発行します。 描画するコマンドの前後にクエリの開始と終了のコマンドを送信します。描画コマンドは通常通りです。 この時、描画するメッシュに応じたqueryIndexを指定しますが、クエリを発行するフレームと結果を使用するフレームはズレますので、管理には注意して下さい。

_pCommandList->BeginQuery(_pQueryHeap.Get(), D3D12_QUERY_TYPE_BINARY_OCCLUSION, queryIndex);
_pD3D12GraphicsCommandList->DrawIndexedInstanced(_indexCount, _instanceCount, _startIndex, _baseVertexIndex, 0);
_pCommandList->EndQuery(_pQueryHeap.Get(), D3D12_QUERY_TYPE_BINARY_OCCLUSION, queryIndex);

クエリ結果を解決

クエリの結果を解決するコマンドを発行する必要があります。注意点として、クエリの発行が完了してから解決する必要がありますので、うまくいかない場合は次のフレームで行うと確実です。

_pCommandList->ResolveQueryData(_pQueryHeap.Get(), D3D12_QUERY_TYPE_BINARY_OCCLUSION, 0, _queryCount, _pQueryResult.Get(), 0);

カリングフラグに反映

クエリ結果の解決を行った次のフレームで、結果をメインメモリに反映します。ここで得られたカリングフラグを使用して、描画するかどうかを決めます。

u64* pBuffer;
D3D12_RANGE range{ 0, _queryCount };
auto hResult = _pQueryResult->Map(0, &range, reinterpret_cast<void**>(&pBuffer));
assert(hResult, "Map error");
memcpy(_pOcclusionResult, pBuffer, sizeof(u64) * _queryCount);
_pQueryResult->Unmap(0, nullptr);

コードはこれだけですが、フレームを跨いだ処理になるので実装難易度はやや高めです。 ただ処理負荷を大幅に下げる可能性のある機能なので、是非挑戦してみてください。

では、またねー。