[DirectX12] Null Descriptorとその活用法

# はじめに
みなさん、Null Descriptorをご存知でしょうか？
Null DescriptorとはID3D12Device::CreateShaderResourceViewなどのDescriptor作成関数の引数であるID3D12Resourceへのポインタにnullptrを指定することで作成されるDescriptorの事です。
今回はこのNull Descriptorについて解説していきたいと思います。

# Null Descriptorとは？
```cpp
void CreateShaderResourceView(
  [in, optional] ID3D12Resource                        *pResource,
  [in, optional] const D3D12_SHADER_RESOURCE_VIEW_DESC *pDesc,
  [in]           D3D12_CPU_DESCRIPTOR_HANDLE           DestDescriptor
);
```
冒頭で書いた通り、Null Descriptorとは第１引数のpResourceにnullptrを指定することで作成できるDescriptorです。
pResourceをnullptrに指定した場合は第２引数のpDescは必ず設定する必要があります。
pResourceをnullptrに指定する場合でも第３引数のDescriptorHeap上のHandleは省略できません。必ず必要です。

作成したNull DescriptorをID3D12GraphicsCommandList::SetGraphicsRootDescriptorTable関数などでRootにセットすると、シェーダー上で「Resourceがバインドされていない」という状態にすることができます。
当然ですが、セットするRootのD3D12_DESCRIPTOR_RANGE_TYPEと作成したNull Descriptorの種類は一致している必要があります。
※参考1,2

# 何に使用するのか？
先ほどNull DescriptorをRootにセットすると「Resourceがバインドされていない」という状態にすることができると書きましたが、そもそもシェーダーでそのパラメータにアクセスしなければ問題ないのでは？という疑問が浮かび上がりますが、概ねその通りです。
具体的にはGPUごとにTier1からTier3のハードウェアレベルというものが設定されており、近年のGPUはこのTier3に該当します。
Tier3のGPUの場合、DescriptorがバインドされていないRootParameterがあっても問題ありません。
しかしTier2以下のレベルではシェーダー上で使用していないパラメータがあったとしても、RootSignatureで設定された全てのRootParameterはシェーダーが実行されるまでにDescriptorがセットされている必要があります。
ただ、このTier2以下に該当するGPUがNVIDIA GTX700シリーズ以前などのかなり古い世代なのでまず気にする必要はないと思います。
以降も使用するGPUはTier3であることを前提に話を進めていきます。
※参考3,4

では、Null Descriptorは必要ないのかというとそうではありません。
例として下記のようにRootDescriptorTableにColorMap,NormalMap,RoughnessMap用のSRVを３つ設定します。
```cpp:main.cpp
//! SRV3つ(t0 ~ t2)
D3D12_DESCRIPTOR_RANGE1 range{};
range = CD3DX12_DESCRIPTOR_RANGE1(D3D12_DESCRIPTOR_RANGE_TYPE_SRV, 3, 0, 0);
CD3DX12_ROOT_PARAMETER1 rootParam{};
rootParam.InitAsDescriptorTable(1, &range, D3D12_SHADER_VISIBILITY_PIXEL);
```
そしてHLSL側でTexture2Dを宣言します。
```hlsl:shader.hlsl
Texture2D gColorMap : register(t0, space0);
Texture2D gNormalMap : register(t1, space0);
Texture2D gRoughnessMap : register(t2, space0);
```
このRootDescriptorTableをDrawCallの直前に、使用するテクスチャのDescriptorを指定するとします。
この時にColorMapは使用するけどNormalMap又はRoughnessMap（或いは両方）は使用しないテクスチャがあるとします。
たとえColorMapしか使用しないとしてもRootDescriptorTableにSRV3つと設定してあるためColorMapのDescriptorを含めたDescriptorHeap上で連続した３つのHandleが必要になります。
```cpp:main.cpp
//! 各DirectXオブジェクトは作成済みとします。
ComPtr<ID3D12Device> pDevice;
ComPtr<ID3D12GraphicsCommandList> pCommandList;
ComPtr<ID3D12DescriptorHeap> pCBVSRVUAVHeap;
ComPtr<ID3D12Resource> pColorMapTexture;

//! CBVSRVUAVのDescriptorSizeを取得
auto incrementSize = pDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
//! DescriptorHeapの先頭ハンドルを取得
auto CPUHandle = pHeap->GetCPUDescriptorHandleForHeapStart();
auto GPUHandle = pHeap->GetGPUDescriptorHandleForHeapStart();

//! ColorMapのDescriptorを作成
pDevice->CreateShaderResourceView(pColorMapTexture->Get(), nullptr, CPUHandle);

//! RootDescriptorTable(SRV=3)に先頭Handleをセット
//! DescriptorTableのDescriptor数が３つなのでGPUHandleからGPUHandle.ptr += incrementSize * 2
//! までのHandleがTableにセットされる
pCommandList->SetGraphicsRootDescriptorTable(0, GPUHandle);
```
上記のようにColorMapだけDescriptorを作成して後に続く2つのHandleに対し何も行わずにRootDescriptorTableへセットしてシェーダーが実行されると、DirectXのデバッグレイヤーにDescriptorが初期化されていませんとエラーが出てしまいます。
このように空のHandleをRootParameterにセットすることは禁じられているので、先述の「Resourceがバインドされていない」状態にするにはNull Descriptorが必要なのです。
先ほどのコードにNull Descriptorを作成するコードを付け足すと、
```cpp:main.cpp
//! ～省略～

//! ColorMapのDescriptorを作成
pDevice->CreateShaderResourceView(pColorMapTexture->Get(), nullptr, CPUHandle);

//! ハンドルを1つ進める
CPUHandle.ptr += incrementSize;

D3D12_SHADER_RESOURCE_VIEW_DESC nullSRVDesc{};
//! 最低でもこの3項目は指定しなければ正常にDescriptorが作成されません。
nullSRVDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
nullSRVDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
nullSRVDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;

//! Null Descriptorの作成
pDevice->CreateShaderResourceView(nullptr, &nullSRVDesc, CPUHandle);

//! ハンドルを1つ進める
CPUHandle.ptr += incrementSize;

//! RoughnessMapが1ピクセル8ビットのグレースケールなので形式を合わせてますが、
//! DXGI_FORMAT_R8G8B8A8_UNORMでも特にエラーは起こりません。
nullSRVDesc.Format = DXGI_FORMAT_R8_UNORM;
//! Null Descriptorの作成
pDevice->CreateShaderResourceView(nullptr, &nullSRVDesc, CPUHandle);

//! RootDescriptorTableにDescriptorをセット
pCommandList->SetGraphicsRootDescriptorTable(0, GPUHandle);
```
このようになります。
この状態でシェーダーを実行するとデバッグレイヤーはエラーを吐かなくなります。

# Null Descriptorの活用
これまでNull Descriptorの作成方法を説明してきましたが、
ResourceがバインドされていないTexture2Dの変数に対してサンプリングを行うと
規定値(0)が返されます。
この規定値である0が返される性質を利用して例えば下記のようなことができます。
```hlsl:shader.hlsl
//! NormalMapから法線をサンプリング
float3 normalMapValue = gNormalMap.Sample(gSampler, input.UV).xyz;

//! ワールド空間上の頂点の法線を取得
float3 normalW = input.NormalW;

if(normalMapValue.z)
{
	//! NormalMapからサンプリングした法線をワールド空間へ変換
}
```
重要なのはここの部分です。
```hlsl
if(normalMapValue.z)
{
	//! NormalMapからサンプリングした法線をワールド空間へ変換
}
```
NormalMapの要素Zは基本的に1.0に近い値になっていて、0であることはほぼあり得ません。
（NormalMapの詳しい説明はここではしません）
これを利用してnormalMapValueの要素Zが0だった場合、「NormalMapがバインドされていない」と見做すことができます。
こうすると1つのシェーダーで定数などで追加情報を送らずとも「NormalMapがあればその値を使い、無ければ頂点の法線を使用する。」といった事ができます。

他にも、
```hlsl:shader.hlsl
//! RoughnessMapからRoughnessをサンプリング
float roughness = gRoughnessMap.Sample(gSampler, input.UV).r;

if (roughness)
{
	//! Specularの計算
}
```
このようにroughnessが0の場合はRoughnessMapがバインドされていないと見做して
スペキュラの計算を行わないようにすることもできます。
(ただし、Roughness値は1に近いほど表面は粗くなり、0の場合表面は完全に平坦となり完全反射するのでこの分岐は正しくありません。しかし、RoughnessMapの値が0であることはほとんどないので思い切った処理をすることができます。勿論、RoughnessMapからサンプリングした値が0である場合も十分あり得るのでケースバイケースです。)

# おわりに
Microsoft公式のサンプルなどにあるNull Descriptorについての情報があまりなかったのでこのような記事を書いてみました。
間違いや質問、アドバイスなどがあればコメントで教えていただければ幸いです。


# 参考文献
参考１：[Microsoft ID3D12Device::CreateShaderResourceView](https://learn.microsoft.com/ja-jp/windows/win32/api/d3d12/nf-d3d12-id3d12device-createshaderresourceview)
参考２：[Microsoft Descriptorの概要](https://learn.microsoft.com/ja-jp/windows/win32/direct3d12/descriptors-overview#null-descriptors)
参考３：[Microsoft ハードウェアの階層](https://learn.microsoft.com/ja-jp/windows/win32/direct3d12/hardware-support)
参考４：[Wikipedia 各ハードウェアレベルに該当するGPU](https://en.wikipedia.org/wiki/Feature_levels_in_Direct3D)