HLSLのTextureCubeの情報が少なすぎる

ランス10やってたら気づいたら土日が終わっていた

さて、ポイントライトの全方位シャドウや、リフレクション用の環境マップの実装をしているのだが

HLSLのTextureCubeの情報が少なすぎる

c++ - Direct3D 11: How to access cube map faces in memory on the CPU side (should ID3D11DeviceContext::Map work with subresources?)? - Stack Overflow

A: CPUでCubeMap読み取りたいんだけどバグる。そもそもマイクロソフトの資料がうんちすぎて、そもそもTextureCubeの6面がメモリ用で連続なのかもわからん。でも常識的に考えて連続やと思うんだけど。誰か教えて。

B:連続やで。多分あんさんのコード間違ってる。



うーん皆苦労してんだなあ。
ただ、GPUメモリ内で連続である確証が得られたのはよかった。私も同様のことをしたいと思っていたから。

環境マップの作成のためには、当然ながら立方体六面のすべての面の描画が必要になるわけだが、残念ながらTextureCubeを使ってバーン!と描画するわけにはいかない。
なぜならば、そもそも根本的に描画する対象が違うからだ。法線、深度、もろもろの情報をMRTを使って一度の描画キックで描画するディファードレンダリングとは根本的に用途が違う。6面描画するなら6回描画しなければならない。つまり、個別のテクスチャとして描画する必要がある。

具体的には6方向のビュー行列を作って、それを定数バッファに格納して6回頂点シェーダを起動する。一般的にはZバッファ...Directx11ではDepthStencilViewの描画テクスチャを、シェーダーリソースとして再利用する形式が一般的。ピクセルシェーダとレンダーターゲットはnullptrを渡せばよい。

適当に一部抜粋

DirectX::XMVECTOR CubeTexture::lookAt[6] = {
XMVectorSet(1.0f, 0.0f, 0.0f, 0.0f), // +X
XMVectorSet(-1.0f, 0.0f, 0.0f, 0.0f), // -X
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // +Y
XMVectorSet(0.0f, -1.0f, 0.0f, 0.0f), // -Y
XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f), // +Z
XMVectorSet(0.0f, 0.0f, -1.0f, 0.0f) // -Z
};
DirectX::XMVECTOR CubeTexture::up[6] = {
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // +X(Up = +Y)
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // -X(Up = +Y)
XMVectorSet(0.0f, 0.0f, -1.0f, 0.0f), // +Y(Up = -Z)
XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f), // -X(Up = +Z)
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // +Z(Up = +Y)
XMVectorSet(0.0f, 1.0f, 0.0f, 0.0f), // -Z(Up = +Y)
};
 
DirectX::XMMATRIX* PointLight::GetViewMatrixes() {
if (_isDirtyMatrix) {
  for (int i = 0; i < 6; i++) {
    DirectX::XMVECTOR pos = XMVectorSet(_point.x, _point.y, _point.z, 0.0f);
    auto dir = static_cast<CUBE_DIRECTION>(i);
    _viewMatrixes[i] = XMMatrixLookToLH(pos, CubeTexture::lookAt[dir],   CubeTexture::up[dir]);
  }
  _isDirtyMatrix = false;
}
return _viewMatrixes;
}


で、あとはこれで得られるビュー行列の配列と、共通の視野角90度・アスペクト比1:1のパースペクティブ行列で描画すれば、6枚分の深度テクスチャが得られるが....
これをCubeTextureに再パックしたい。

D3D11_TEXTURE2D_DESC texElementDesc;
depthTexture[0]->GetDesc(&texElementDesc);
D3D11_TEXTURE2D_DESC texArrayDesc;
texArrayDesc.Width = texElementDesc.Width;
texArrayDesc.Height = texElementDesc.Height;
texArrayDesc.MipLevels = texElementDesc.MipLevels;
texArrayDesc.ArraySize = 6;
texArrayDesc.Format = texElementDesc.Format;
texArrayDesc.SampleDesc.Count = 1;
texArrayDesc.SampleDesc.Quality = 0;
texArrayDesc.Usage = D3D11_USAGE_DEFAULT;
texArrayDesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
texArrayDesc.CPUAccessFlags = 0;
texArrayDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE;
ComPtr<ID3D11Texture2D> texArray = 0;
if (FAILED(hpDevice->CreateTexture2D(&texArrayDesc, 0, texArray.ToCreator())))
return;
 
for (UINT i = 0; i < 6; i++)
{
  hpDeviceContext->CopySubresourceRegion(texArray.Get(), D3D11CalcSubresource(0, i, texArrayDesc.MipLevels), 0, 0, 0, depthTexture[i], 0, nullptr);
}


唐突に出てくる変数名とかは雰囲気で。
つまるところ、CopySubresourceRegionを使ってテクスチャを一枚一枚TextureCubeに先頭からコピーしていく。
正直TextureCubeのミップレベルの構造がよくわからんかったので自信がなかったが、うまく動いた。+X-X+Y-Y...と一通りのミップレベル0のテクスチャの後に、ミップレベル1...2...と続いていく形式じゃないのかと不安だった。どうやらただの6枚のテクスチャ配列らしい。

これでCubeTexture自体は実装できたので、さっそくシャドウを実装したいところだが、先にTextureCubeビューワーを作る。兎にも角にも、バグ発生時にシャドウの計算と、深度環境マップの作成のどちらでバグっているのかが分からなかったら話にならないからだ。

HLSLでは、TextureCubeのSampleメソッドはこういうシグニチャになっている。
float4 Sample(Sampler, float3);
Texture2DArrayと同じシグニチャ。つまり、第二引数の三次元ベクトルは、x、yはテクスチャUVで、Zがインデックスだな?と思ったら全然違った

あまりのも情報が少なすぎるので、DirectxのサンプルをあさりまくってTextureCubeを実際に使っているプログラムを探す。すると、引数に渡している引数の変数に代入している式に見覚えのある数式が....あ、これ反射ベクトルの計算式だ。

というわけで、TextureCubeの第二引数はUVではなく反射ベクトルだった。わ、わかりづれぇ..
ちがった。
反射ベクトルを使うのはリフレクション用途で環境マップにアクセスするとき。もっとシンプルに、立方体の中心から面に向かう3次元ベクトルでアクセスする。

これは、単純なテクスチャの表示には不便そうだ。なので、テクスチャビューアーはTextureCubeが渡された時には内部で展開して一枚のテクスチャにする機能を加えることにする。さっきのプログラムの真逆のことをするだけでいい。

D3D11_TEXTURE2D_DESC faceDesc(desc);
D3D11Texture faceTexture;
ComPtr<ID3D11Texture2D> faceTextureSrc;
faceDesc.ArraySize = 1;
faceDesc.BindFlags = D3D11_BIND_FLAG::D3D11_BIND_SHADER_RESOURCE;
faceDesc.MiscFlags = 0;
hpDevice->CreateTexture2D(&faceDesc, nullptr, faceTextureSrc.ToCreator());
hpDeviceContext->CopySubresourceRegion(faceTextureSrc.Get(), 
0, 0, 0, 0, texture, 
D3D11CalcSubresource(0, index, desc.MipLevels), nullptr);


これで深度環境マップが正しく描画されているか確認できるようになった。

f:id:riyaaaaasan:20180306004132p:plain


下六枚が部屋の中心に置いたポイントライトのシャドウマップ。
左上のは遥か上空にあるディレクショナルライトのシャドウマップ。
単純計算で全方位シャドウは6倍のコストがかかる。メモリも6倍。うーん、これはほいほいと毎フレーム計算したくないな。

左から順に+X, -X, +Y, -Y, +Z, -Zの面なので、雰囲気的にそれらしく描画はできているようだ。まだシャドウそのものを実装したわけではないので、確信はないが。
今週中にポイントライトの実際のシャドウの計算をHLSLを使って実装していきたい。

今回はこの辺で。