ゲーム開発の備忘録

趣味のゲーム開発でのノウハウや、技術的に嵌ったポイントを忘れないように書き記しておくブログです。

DirectX12でステンシルテストを実行する

はじめに

本記事では、DirectX12でのステンシルテストの実行方法を解説します。
ステンシルテストはDirectXに昔から備わっている機能ですが、DirectX12での実行方法については書籍、Webサイトはおろか、Microsoft Docsにも解説がありません。また、サンプルコードも存在しません。とはいえ、基本は深度ステンシルステートの設定を変更してグラフィックスパイプラインステートオブジェクトに紐づければよいので、DirectX12をある程度触って慣れていれば大きく詰まる箇所は少ないです。しかし、1箇所だけDirectX12特有の設定項目があり、その部分に苦戦して2時間ほど消費しました。この記事が、今後DirectX12でステンシルテストを実行したいと考える方の助けとなれば幸いです。
なお、本記事では読者の方がステンシルテストそのものについては理解されている前提で話を進めます。

深度ステンシルバッファの設定変更

まず、ステンシルテストが実行できるように深度ステンシルバッファの設定を変更する必要があります。実はこの部分が最も見落としやすい要注意箇所なのです。分かってしまえば簡単な話なのですが……

深度ステンシルバッファのリソースを生成している箇所で、D3D12_RESOURCE_DESC構造体のFormatメンバの値をステンシルバッファのためのメモリを確保するようなメモリフォーマットに変更する必要があります。深度テストのみの場合、大抵はDXGI_FORMAT_D32_FLOATを指定しているかと思いますが、これをDXGI_FORMAT_D24_UNORM_S8_UINTのように、ステンシルバッファ用のメモリを確保するようなフォーマットに変更します。上記の定数の場合、D24で深度バッファが3Byte分、S8でステンシルバッファが1Byte分確保されます。
同様に、D3D12_CLEAR_VALUE構造体のFormatメンバ、D3D12_DEPTH_STENCIL_VIEW_DESC構造体のFormatメンバの値も変更しましょう。

この設定を忘れると、グラフィックスパイプラインステートオブジェクトに紐づけた深度ステンシルステートでステンシルテストを有効にしていても、ステンシルテストが実行されません。しかし、その場合でもエラーは全く表示されないため、原因を掴むのに非常に苦労しました……

深度ステンシルバッファのクリア処理の変更

深度ステンシルバッファの設定変更が完了したら、ID3D12GraphicsCommandList#ClearDepthStencilView()の第2引数に論理和を用いてD3D12_CLEAR_FLAG_DEPTH | D3D12_CLEAR_FLAG_STENCILのように指定し、ステンシルバッファ側もクリアするように変更しましょう。第4引数に指定するクリア時の設定値は0Uで問題ありません。

ステンシル参照値を初期化したい場合、ID3D12GraphicsCommandList#OMSetStencilRef()でステンシル参照値を設定することができます。

深度ステンシルステートの作成

ステンシルバッファ値を更新するための深度ステンシルステートと、ステンシルテストを実行するための深度ステンシルステートを作成します。
ステンシルバッファ値を更新するための深度ステンシルステートのD3D12_DEPTH_STENCIL_DESC構造体の設定例を以下に示します。

D3D12_DEPTH_STENCIL_DESC depthStencilDesc = {};

depthStencilDesc.DepthEnable = FALSE;
depthStencilDesc.StencilEnable = TRUE;
depthStencilDesc.StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK;
depthStencilDesc.StencilWriteMask = D3D12_DEFAULT_STENCIL_WRITE_MASK;
depthStencilDesc.FrontFace.StencilFailOp = D3D12_STENCIL_OP_KEEP;
depthStencilDesc.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
depthStencilDesc.FrontFace.StencilPassOp = D3D12_STENCIL_OP_INCR;
depthStencilDesc.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_ALWAYS;
depthStencilDesc.BackFace.StencilFailOp = depthStencilDesc.FrontFace.StencilFailOp;
depthStencilDesc.BackFace.StencilDepthFailOp = depthStencilDesc.FrontFace.StencilDepthFailOp;
depthStencilDesc.BackFace.StencilPassOp = depthStencilDesc.FrontFace.StencilPassOp;
depthStencilDesc.BackFace.StencilFunc = depthStencilDesc.FrontFace.StencilFunc;

ステンシルテストを実行するための深度ステンシルステートのD3D12_DEPTH_STENCIL_DESC構造体の設定例を以下に示します。

D3D12_DEPTH_STENCIL_DESC depthStencilDesc = {};

depthStencilDesc.DepthEnable = FALSE;
depthStencilDesc.StencilEnable = TRUE;
depthStencilDesc.StencilReadMask = D3D12_DEFAULT_STENCIL_READ_MASK;
depthStencilDesc.StencilWriteMask = 0x00;
depthStencilDesc.FrontFace.StencilFailOp = D3D12_STENCIL_OP_ZERO;
depthStencilDesc.FrontFace.StencilDepthFailOp = D3D12_STENCIL_OP_KEEP;
depthStencilDesc.FrontFace.StencilPassOp = D3D12_STENCIL_OP_KEEP;
depthStencilDesc.FrontFace.StencilFunc = D3D12_COMPARISON_FUNC_EQUAL;
depthStencilDesc.BackFace.StencilFailOp = depthStencilDesc.FrontFace.StencilFailOp;
depthStencilDesc.BackFace.StencilDepthFailOp = depthStencilDesc.FrontFace.StencilDepthFailOp;
depthStencilDesc.BackFace.StencilPassOp = depthStencilDesc.FrontFace.StencilPassOp;
depthStencilDesc.BackFace.StencilFunc = depthStencilDesc.FrontFace.StencilFunc;

depthStencilDesc.FrontFace.StencilFuncに指定する値をD3D12_COMPARISON_FUNC_NOT_EQUALに変更することで、くりぬく範囲を反転させることができます。

描画

ここまで準備できたら、早速ステンシルテストを利用した描画を試してみましょう。
ステンシルバッファ値を更新するための深度ステンシルステートを用いて描画し、その後、ステンシルテストを実行するための深度ステンシルステートを用いて描画することでステンシルテストを実行できます。
ただし、ここで一つ注意点があります。DirectX12ではステンシルバッファ値を更新するための深度ステンシルステートを用いて描画する際に、レンダリングターゲットへの書込みを抑制することができません。そのため、ピクセルシェーダ側で常に自身の描画結果を破棄するか、透過して出力する必要があります。

終わりに

お疲れ様でした。分かってしまえば、DirectX12でもDirectX11とほぼ同じ労力でステンシルテストを実行することができます。是非、エフェクト作成等で役立ててみてください!