DirectX12 でキューブマップ

DirectX12 でキューブマップを使ってみます。 今回は素直な実装で、キューブマップへの描画&利用についても試してみました。

キューブマップは DirectX10 以降では、2Dのテクスチャ画像を6つ保持するテクスチャ配列です。
この6つの面に、±X, ±Y, ±Z の方向を向いた3Dシーンが描画されています。これを環境マップとして使用して、反射先の色として取得します。これによりメタルマリオ的な描画結果を得ることができます。

キューブマップのロード

DirectX12 では DirectXTex を用いると簡単にテクスチャをロードすることができます。DirectX12 Programming シリーズでもやっているように、画像データをアップロードヒープにロードして、実際のテクスチャ領域にコピーを行うコマンドを積む処理を行います。

// コードの抜粋
DirectX::TexMetadata metadata;
DirectX::ScratchImage image;
DirectX::LoadFromDDSFile(L"LobbyCube.dds", 0, &metadata, image);

ComPtr<ID3D12Resource> cubemap;
CreateTexture(m_device.Get(), metadata, &cubemap);
ComPtr<ID3D12Resource> srcBuffer;
std::vector<D3D12_SUBRESOURCE_DATA> subresources;
PrepareUpload(m_device.Get(), image.GetImages(), image.GetImageCount(), metadata, subresources);
totalBytes = GetRequiredIntermediateSize(cubemap.Get(), 0, UINT(subresources.size()));
auto staging = CreateResource(CD3DX12_RESOURCE_DESC::Buffer(totalBytes), D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, D3D12_HEAP_TYPE_UPLOAD);
auto command = CreateCommandList();
UpdateSubresources(
  command.Get(),
  cubemap.Get(), staging.Get(), 
  0, 0, subresources.size(), subresources.data());
auto barrier = CD3DX12_RESOURCE_BARRIER::Transition(
  cubemap.Get(), 
  D3D12_RESOURCE_STATE_COPY_DEST, D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE);
command->ResourceBarrier(1, &barrier);
FinishCommandList(command);

キューブマップ用シェーダーリソースビューの準備

キューブマップテクスチャを参照するためのシェーダーリソースビューを準備します。ディスクリプタは既に確保してあるものとして、次のように D3D12_SHADER_RESOURCE_VIEW_DESC 構造体を設定してビューを生成します。

D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc{};
srvDesc.Format = metadata.format;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURECUBE;
srvDesc.TextureCube.MipLevels = metadata.mipLevels;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
m_device->CreateShaderResourceView(cubemap.Get(), &srvDesc, m_cubeSrv);

シェーダーでキューブマップを参照

キューブマップを参照するためには、シェーダーコードの中で3次元ベクトルでキューブマップをフェッチします。ここでは反射ベクトルを頂点シェーダーで計算してしまっています。

// 頂点シェーダー(抜粋)
VSOutput main( VSInput In )
{
    :
  float3 eyeDir = normalize(In.Position.xyz - cameraPos.xyz);
  result.Reflect = reflect(eyeDir, In.Normal.xyz);
    :
}

// ピクセルシェーダー(抜粋)
float4 mainPS(VSOutput In) : SV_TARGET
{
  return texCube.Sample(samp,In.Reflect);
}

キューブマップを扱うときには、まずは素直に DirectX 系の流儀に従って、左手座標系でやってみることにしましょう。
DirectX Math には右手系の関数も用意されていて簡単に右手系も使えるのですが、キューブマップのことを考えるとシェーダーでの処理も考慮が必要になってきます。

動的キューブマップ

折角の映り込みを実現できるのであれば、動的に描画したいと思うのは自然な流れでしょう。
これにはレンダーテクスチャとしてキューブマップを作成します。通常のレンダーテクスチャと同じようにしてリソースを確保しますが、先ほど説明したように6つのテクスチャを持つテクスチャ配列で D3DX12_RESOURCE_DESC を設定します。

この6面に対してそれぞれ3Dのシーンを描画していけば、動的キューブマップを作ることが出来ます。
そこで各面毎にレンダーターゲットビューを用意します。ポイントは、 FirstArraySlice には面の番号を設定し、ViewDimension には D3D12_RTV_DIMENSION_TEXTURE2DARRAY を設定、描画対象は1枚なので ArraySize には 1 を設定します。
配列サイズの指定が意外と罠で、6面のままだとエラーが発生します。

for (int i = 0; i < 6; ++i)
{
  D3D12_RENDER_TARGET_VIEW_DESC rtvDesc{};
  rtvDesc.Format = desc.Format;
  rtvDesc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2DARRAY;
  rtvDesc.Texture2DArray.FirstArraySlice = i;
  rtvDesc.Texture2DArray.ArraySize = 1;
 
  m_device->CreateRenderTargetView(
    m_renderCubemap.Get(),
    &rtvDesc,
    handleRTV[i]
);

あとは各X,Y,Z方向にカメラを向けてこれらの面を描画します。最初にトライするときには、左手座標系でやりましょう

描画して中央のティーポットに貼り付けている図がこちらです。ファイルから読み込んだテクスチャを表示してみたり、回転させてみたりしています。参照している面がわかりやすいように、各面毎にクリア色を変えています。

左手座標系で動的キューブマップ

右手系で同じシーンを描画してみたものがこちらとなります。色々と注意事項が多めなので今回の説明は省略します。一応ちゃんと色々な事項を考慮して手続きを踏めば出来るよということで。

上記の左手系と見比べてみてもらうと分かるのですが、Z軸の向きが違うので左右が反転しているように見えるかと思います。

それでも映り込んでいる面としての参照先は、近い色のティーポットが見えていることもあり正しそうなことは分かるかと思います。

素直に右手系を使うとどうなるか

なお、何も考慮せずに描画するとこのような場面に出遭うでしょう、そして面の反転を直してみても、もう1枚の絵のようになったりすると思います。もしかすると、これ以外のケースに陥るかもしれません。ポリゴン面がどちらを正とする向きと指定するか、反転はどのように対処するのかなど、方針は色々とあると思いますが、組み合わせ次第ではちょっと難しい状況に陥ると思っています。

なので、DirectXの場合は、左手系でキューブマップを貼るところからやってみましょう

まとめ

今回は愚直に各面ごとを描画するための、キューブマップ用のRTVを紹介したところがポイントです。また、真っ先に右手系でキューブマップ参照をやると大変なので、左手系がオススメであることを示したつもりです。

 最近のジオメトリシェーダーと今回のキューブマップで、ピンとくる方も居ると思います。次回は、1回の描画指示でキューブマップ6面を描画するDirectX10以降でのキューブマップの書き方を紹介する計画です。

DirectX
すらりんをフォローする
すらりん日記

コメント

タイトルとURLをコピーしました