前回
こんにちは。前回に引き続き、今回は画面に四角形のポリゴンを描画してみます。
ポリゴン
しばらくポリゴンについて話します。3Dグラフィックスに関する知識がある方はすでにご存知の内容でしょうから、この部分は飛ばしてもかまいません。
ポリゴンの定義については、調べればもっと詳しく解説した文章がたくさんありますので、ポリゴン(三角形)がどのように構成されるかについてお話しします。
ここからは三角形と呼びます。
三角形を描くには、3つの頂点と3つのインデックスが必要です。
2つを描くには、4つの頂点と6つのインデックスが必要です。
このようにインデックスを利用して頂点の重複を防ぐ方式をインデックス三角形リスト(indexed triangle list)と呼びます。(実際のモデルの中には、わざと頂点を重複させて制作する場合もあります。)
DirectXでポリゴンを描画するには、バーテックスバッファとインデックスバッファを準備する必要があります。
頂点データとインデックスデータ
頂点データを定義する必要があります。
頂点の最も基本的な要素は頂点の座標です。3D空間ですので、3次元ベクトル値を位置として持てば良いですね。
直接ベクトル構造体を作成しても良いですが、私はDirectXのライブラリを使用します。
ヘッダーを追加してください。
#include <DirectXMath.h>
struct Vertex
{
DirectX::XMFLOAT3 pos;
DirectX::XMFLOAT2 uv;
};
XMFLOAT3で位置座標を持ち、XMFLOAT2でUV値を持つ頂点を定義しました。
UVについては後でテクスチャについて扱うときに話しましょう。
私たちは四角形を描くので、簡単に頂点4つとインデックス6つを準備します。
Vertex vertices[] =
{
{{-1.0f, -1.0f, 0.0f}, {0.0f, 1.0f}},
{{-1.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
{{1.0f, -1.0f, 0.0f}, {1.0f, 1.0f} },
{{1.0f, 1.0f, 0.0f}, {1.0f, 0.0f}},
};
unsigned short indices[] =
{
0,1,2,
2,1,3
};
このように2つの三角形を描いて1つの四角形を作成することができますね。
これらをGPUに渡せるようにDirectXリソースを生成する必要があります。
頂点バッファとインデックスバッファ
前回の記事でバッファについて話したことがあります。
今回は実際に作ってみましょう。DirectXのバッファはほとんどがID3D12Resourceオブジェクトを指します。
GPUのメモリを確保することだと考えてください。CPUの観点から言えば、C++のnewキーワードに似ています。
ID3D12Resourceを頂点バッファとして使用するコードを見てみましょう。
D3D12_HEAP_PROPERTIES heapprop = {};
heapprop.Type = D3D12_HEAP_TYPE_UPLOAD;
heapprop.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapprop.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
D3D12_RESOURCE_DESC resdesc = {};
resdesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
resdesc.Width = sizeof(vertices);
resdesc.Height = 1;
resdesc.DepthOrArraySize = 1;
resdesc.MipLevels = 1;
resdesc.Format = DXGI_FORMAT_UNKNOWN;
resdesc.SampleDesc.Count = 1;
resdesc.Flags = D3D12_RESOURCE_FLAG_NONE;
resdesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
ID3D12Resource* vertBuff = nullptr;
result = _dev->CreateCommittedResource(&heapprop, D3D12_HEAP_FLAG_NONE, &resdesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&vertBuff));
結局、CreateCommittedResourceを使用して生成しますが、パラメータとしてD3D12_HEAP_PROPERTIESとD3D12_RESOURCE_DESCが渡されるのが見えます。一つずつ見てみましょう。
D3D12_HEAP_PROPERTIES heapprop = {};
heapprop.Type = D3D12_HEAP_TYPE_UPLOAD;
heapprop.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN;
heapprop.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN;
Type パラメータはヒープに関する設定です。
Type の種類は次のとおりです。
enum D3D12_HEAP_TYPE
{
D3D12_HEAP_TYPE_DEFAULT = 1, //CPUからアクセスできない(Mapできない)
D3D12_HEAP_TYPE_UPLOAD = 2, //CPUがらアクセスできる (Mapできる)
D3D12_HEAP_TYPE_READBACK = 3, //CPUから読み取れる
D3D12_HEAP_TYPE_CUSTOM = 4 //カスタム
} D3D12_HEAP_TYPE;
今回の頂点バッファにはD3D12_HEAP_TYPE_UPLOADを使用します。
D3D12_HEAP_TYPE_UPLOADは、後で見るMapメソッドが使用可能です。
CPUからアクセス可能なヒープです。
D3D12_HEAP_TYPE_DEFAULTはMapが不可能です。しかし、バンド幅が広いためアクセスが速いです。GPUでのみ使用するのに適したヒープです。
D3D12_HEAP_TYPE_READBACKは、CPUからアクセス可能な読み取り専用のヒープです。しかし、バンド幅が狭いため、GPUに演算を任せてその値を取得する際に使用します。
D3D12_HEAP_TYPE_CUSTOMに設定すると、後で見るCPUページとメモリプールに関する設定を正しく指定する必要があります。
D3D12_HEAP_TYPE_CUSTOMでなければ、上記のコードのようにD3D12_CPU_PAGE_PROPERTY_UNKNOWN、D3D12_MEMORY_POOL_UNKNOWNにしても問題ありません。
typedef
enum D3D12_CPU_PAGE_PROPERTY
{
D3D12_CPU_PAGE_PROPERTY_UNKNOWN = 0,
D3D12_CPU_PAGE_PROPERTY_NOT_AVAILABLE = 1,
D3D12_CPU_PAGE_PROPERTY_WRITE_COMBINE = 2,
D3D12_CPU_PAGE_PROPERTY_WRITE_BACK = 3
} D3D12_CPU_PAGE_PROPERTY;
typedef
enum D3D12_MEMORY_POOL
{
D3D12_MEMORY_POOL_UNKNOWN = 0,
D3D12_MEMORY_POOL_L0 = 1,
D3D12_MEMORY_POOL_L1 = 2
} D3D12_MEMORY_POOL;
D3D12_CPU_PAGE_PROPERTYとD3D12_MEMORY_POOLの種類です。
D3D12_HEAP_TYPE_CUSTOMは後でテクスチャを作成する際に使用するため、その時にUNKNOWN以外のものを使用してみましょう。
D3D12_RESOURCE_DESCを設定しましょう。
D3D12_RESOURCE_DESC resdesc = {};
resdesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER;
resdesc.Width = sizeof(vertices);
resdesc.Height = 1;
resdesc.DepthOrArraySize = 1;
resdesc.MipLevels = 1;
resdesc.Format = DXGI_FORMAT_UNKNOWN;
resdesc.SampleDesc.Count = 1;
resdesc.Flags = D3D12_RESOURCE_FLAG_NONE;
resdesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR;
バッファとして使用するため、D3D12_RESOURCE_DIMENSION_BUFFER
WidthとHeightはテクスチャであればテクスチャの幅と高さを入れるべきですが、頂点バッファなので、Widthに全体のサイズを入れます。
Formatもテクスチャではないため、DXGI_FORMAT_UNKNOWN
SampleDescはアンチエイリアシングのためのパラメータですが、頂点バッファなので、Countを1にします。
LayoutはD3D12_TEXTURE_LAYOUT_UNKNOWNを入れると適切なテクスチャレイアウトが自動的に入りますが、テクスチャではないから、D3D12_TEXTURE_LAYOUT_ROW_MAJORにします。
これで設定した D3D12_HEAP_PROPERTIES と D3D12_RESOURCE_DESC を使って ID3D12Resource を生成します。
GPUから読み取るため、D3D12_RESOURCE_STATE_GENERIC_READ に設定します。
result = _dev->CreateCommittedResource(&heapprop, D3D12_HEAP_FLAG_NONE, &resdesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&vertBuff));
Map
頂点バッファリソースを作成しましたが、まだバッファに実際の頂点データを入れていません。
Mapを使用して頂点データをGPUに渡すようにしましょう。
Mapを使用すると、バッファに対応するアドレスを取得できます。
そのアドレスにデータを書いたら、その内容がそのままGPUにコピーされます。
頂点データをマッピングするコードは次のとおりです。
unsigned char* vertMap = nullptr;
result = _vb->Map(0, nullptr, (void**)&vertMap);
std::copy(std::begin(vertices), std::end(vertices), vertMap);
_vb->Unmap(0, nullptr);
Mapの最初のパラメータはリソースの配列またはミップマップの場合、その番号を渡します。しかし、今回設定したバッファは1つなので、0を渡します。
2番目のパラメータはマップする範囲ですが、今回は全範囲なのでnullptrで大丈夫です。
3番目のパラメータに渡されたポインタにマッピングされたアドレスが渡されます。マッピングされたアドレスを持つポインタを使ってデータを埋めることにしましょう。
Unmapはマップを解除するメソッドです。
ここまでで頂点バッファの準備が完了します。
頂点バッファビュー
実際に頂点バッファを使用するためには、頂点バッファビューが必要です。
何バイトのデータが存在するのか、一つの頂点のサイズはどれくらいかを示すものです。
D3D12_VERTEX_BUFFER_VIEW vbView = {};
vbView.BufferLocation = vertBuff->GetGPUVirtualAddress(); //頂点バッファ仮想アドレス
vbView.SizeInBytes = sizeof(vertices); //全バイト数
vbView.StrideInBytes = sizeof(vertices[0]); //頂点1つのバイト数
インデックスバッファー
インデックスバッファーの作成は、頂点バッファーの生成とほぼ同じです。
ID3D12Resource* idxBuff = nullptr;
resdesc.Width = sizeof(indices);
result = _dev->CreateCommittedResource(&heapprop, D3D12_HEAP_FLAG_NONE, &resdesc, D3D12_RESOURCE_STATE_GENERIC_READ, nullptr, IID_PPV_ARGS(&idxBuff));
unsigned short* mappedIdx = nullptr;
idxBuff->Map(0, nullptr, (void**)&mappedIdx);
std::copy(std::begin(indices), std::end(indices), mappedIdx);
idxBuff->Unmap(0, nullptr);
インデックスバッファビュー
D3D12_INDEX_BUFFER_VIEW ibView = {};
ibView.BufferLocation = idxBuff->GetGPUVirtualAddress();
ibView.Format = DXGI_FORMAT_R16_UINT;
ibView.SizeInBytes = sizeof(indices);
インデックスバッファはこのように作成します。
バーテックスバッファビューと異なるのはFormat程度ですね。
使用するインデックス値は4バイト整数なので、DXGI_FORMAT_R16_UINTにします。
シェーダー
何かを描画するにはシェーダーが必要です。3Dプログラミングの経験がある方にはシェーダーは馴染みがあるかもしれませんが、初めての方にはそうではないかもしれません。
シェーダーはGPUで実行されるプログラムだと考えてください。
代表的には、頂点シェーダー、ピクセルシェーダーを主に作成します。
頂点シェーダーはバーテックスをどこに配置するかを計算するプログラム。
ピクセルシェーダーはピクセルをどの色で表示するかを計算するプログラムです。
他のシェーダーには、フルシェーダー、コンピュートシェーダー、ジオメトリシェーダーなどがありますが、オブジェクトを、バーテックスシェーダーとピクセルシェーダーだけで十分です。
DirectXでは、C言語に似ているHLSLという言語でシェーダープログラムを作成します。OpenGLはGLSLという言語を使用します。
C言語のキーワードや文法に似ておりますが、CPUとは異なりGPUで動作するため、最適化に関する観点が異なることを覚えておいてください。
では、シェーダーを作成し、それを読み込んでシェーダーオブジェクトを生成してみましょう。
頂点シェーダー
さっそくシェーダーを作成してみましょう。
Visual Studioのプロジェクトエクスプローラーを右クリックし、追加 → 新しい項目を選択すると、次のような画面が表示されます。
それでは、左側からHLSLを選択し、頂点シェーダーファイルを選択します。
作成するシェーダーファイルの名前は自由に作成して構いません。
追加をクリックすると、.hlsl拡張子のファイルが追加されます。
作成したシェーダーファイルを開くと、次のようなコードを見ることができます。
float4 main (float4 pos : POSITION) : SV_POSITION
{
return pos;
}
まず、mainの名前をこのように変更してみましょう。
float4 BasicVS(float4 pos : POSITION, float2 uv) : SV_POSITION
{
return pos;
}
このように変更してコンパイルが正常にできるように設定を変更する必要があります。
このシェーダーファイルを右クリックしてプロパティを選択します。
すると、次のような画面が表示されます。
写真と同じようにエントリーポイントの名前を先ほど変更した名前(BasicVS)に変更し、シェーダーモデルをシェーダーモデル5.0に変更します。 このように設定しないと、後でコンパイルする際にエラーが発生します。
ピクセルシェーダー
バーテックスシェーダーを作成したように、今回はピクセルシェーダーを作成してみましょう。
作成したシェーダーファイルを開くと、バーテックスシェーダーとは少し異なることがわかるでしょう。
ピクセルシェーダーもエントリーポイントの名前を変更し、次のように作成してください。
float4 BasicPS(Output input) : SV_TARGET
{
return float4(1, 1, 1, 1);
}
プロパティでエントリーポイント名を変更し、頂点シェーダーのようにシェーダーモデルを5.0に設定してください。
シェーダーについての詳しい説明はしていませんが、とりあえず頂点シェーダーとピクセルシェーダーの準備は終わりました。
これらを読み込んでシェーダーオブジェクトを生成することにしましょう。
シェーダーの読み込み
シェーダーオブジェクトはID3DBlobというオブジェクトを使用します。このタイプはシェーダーにのみ使用されるものではなく、使用する側がどのような目的で使用するかを決定できるオブジェクトです。
シェーダーを読み込むために必要なヘッダーとライブラリをリンクします。
#include <d3dcompiler.h>
#pragma comment(lib, "d3dcompiler.lib")
すると、D3DCompileFromFileを使用して作成したシェーダーを読み込むことができます。
HRESULT D3DCompileFromFile(
[in] LPCWSTR pFileName,
[in, optional] const D3D_SHADER_MACRO *pDefines,
[in, optional] ID3DInclude *pInclude,
[in] LPCSTR pEntrypoint,
[in] LPCSTR pTarget,
[in] UINT Flags1,
[in] UINT Flags2,
[out] ID3DBlob **ppCode,
[out, optional] ID3DBlob **ppErrorMsgs
);
最も重要なのは最初のファイル名、4番目のエントリーポイント、5番目のシェーダーターゲットです。
2番目のパラメーターpDefinesは今回は使用しません。
3番目のパラメーターはシェーダー内でインクルードを使用するためにD3D_COMPILE_STANDARD_FILE_INCLUDEにします。
Flags1はD3DCOMPILE_DEBUGとD3DCOMPILE_SKIP_OPTIMIZATIONに指定します。
書くとこうなります。
ID3DBlob* vsBlob = nullptr;
ID3DBlob* psBlob = nullptr;
ID3DBlob* errorBlob = nullptr;
result = D3DCompileFromFile(L"BasicVertexShader.hlsl",
nullptr,
D3D_COMPILE_STANDARD_FILE_INCLUDE,
"BasicVS",
"vs_5_0",
D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION,
0,
&vsBlob,
&errorBlob);
if (FAILED(result) == true)
{
ShowErrorMassege(result, errorBlob);
}
BasicVertexShader.hlslというファイルをコンパイルし、
インクルードを使用できるようにして、
エントリーポイントはBasicVS、
シェーダーモデル5.0の頂点シェーダー、
デバッグが可能で最適化はしないようにフラグを設定
シェーダーのコンパイルに失敗すると、最後に渡されたerrorBlobにメッセージが入ります。
しばらくエラーメッセージを確認するためにShowErrorMassageメソッドを見てみましょう。
void ShowErrorMassage(HRESULT result, ID3DBlob* errorBlob)
{
if (result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND))
{
::OutputDebugStringA("ファイルが見つかりません");
return;
}
else
{
std::string errstr;
errstr.resize(errorBlob->GetBufferSize());
std::copy_n((char*)errorBlob->GetBufferPointer(), errorBlob->GetBufferSize(), errstr.begin());
errstr += "\n";
::OutputDebugStringA(errstr.c_str());
}
}
ID3DBlobのGetBufferSizeでバッファのサイズを知ることができ、GetBufferPointerでバッファのポインタを得ることができます。上記のようにすればエラーメッセージを確認することができます。
ピクセルシェーダーもほぼ同じ方法でコンパイルすることができます。
result = D3DCompileFromFile(L"BasicPixelShader.hlsl",
nullptr,
D3D_COMPILE_STANDARD_FILE_INCLUDE,
"BasicPS",
"ps_5_0",
D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION,
0,
&psBlob,
&errorBlob);
if (FAILED(result) == true)
{
ShowErrorMassege(result, errorBlob);
}
ピクセルシェーダーはps_5_0に設定します。
頂点レイアウト
さて、頂点レイアウトを作成しましょう。
上記で私たちは頂点データを定義しました。しかし、GPUは私たちが定義した頂点について知りません。そこで、頂点レイアウトでそれを知らせます。
頂点レイアウト構造体は D3D12_INPUT_ELEMENT_DESC です。
typedef struct D3D12_INPUT_ELEMENT_DESC {
LPCSTR SemanticName;
UINT SemanticIndex;
DXGI_FORMAT Format;
UINT InputSlot;
UINT AlignedByteOffset;
D3D12_INPUT_CLASSIFICATION InputSlotClass;
UINT InstanceDataStepRate;
} D3D12_INPUT_ELEMENT_DESC;
実際に私たちが使用する頂点について記述すると次のようになります。
D3D12_INPUT_ELEMENT_DESC inputLayout[] =
{
{
"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0,
D3D12_APPEND_ALIGNED_ELEMENT,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0
},
{
"TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0,
D3D12_APPEND_ALIGNED_ELEMENT,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0
},
};
シメントはシェーダーコードでも見たことがあるでしょう。データの意味を付与する文字列です。
float4 BasicVS(float4 pos : POSITION, float2 uv : TEXCOORD) : SV_POSITION
頂点シェーダのパラメータを覚えてください.
2番目のパラメーターであるセマンティックインデックスは、同じセマンティックをインデックスで使用するためのものです。
もしuv以外に頂点構造体に追加でfloat2 uv2という変数を追加し、これをTEXCOORDセマンティックとして使用する場合、インデックスを1にすればよいです。
シェーダー側ではTEXCOORD1と付きます。
3番目のパラメータはフォーマットです。
頂点の位置は4バイトの実数3つを使用するため、DXGI_FORMAT_R32G32B32_FLOATです。
uvは4バイトの実数2つなので、DXGI_FORMAT_R32G32_FLOATです。
4 番目のパラメーターはスロット番号です。私たちは頂点データを一つ使用しているので 0 にすれば良いですが、複数の頂点データを合わせて使用する場合もあります。
IASetVertexBuffers メソッドにスロット番号を渡すことで、望むスロットのデータを GPU が見るように指定することが可能です。
5番目のパラメータはデータオフセットを指定します。
つまり、この要素が何バイトから始まるかを教える必要があります。しかし、これを自ら計算するのは面倒ですので、D3D12_APPEND_ALIGNED_ELEMENTに設定すると、自動的に直前の要素に続くようにしてくれます。
6番目のパラメーターはD3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATAにします。これは1つの頂点ごとに1つのレイアウトが含まれることを意味します。
最後のパラメータは、インスタンシングを使用する際に複数のデータが同じデータを再利用するためのものです。今回はインスタンシングとは関係がないため、0にします。
ルートシグネチャ
今回はルートシグネチャを生成してみましょう。
ルートシグネチャはどのシェーダリソースを使用するのか、どのサンプラーを使用するのかを設定するものです。
今回は特にシェーダでテクスチャをサンプリングしたりコンスタントバッファを使用したりしないので、フラグだけを設定します。
D3D12_ROOT_SIGNATURE_DESC rootSignatureDesc = {};
rootSignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
ID3DBlob* rootSigBlob = nullptr;
result = D3D12SerializeRootSignature(&rootSignatureDesc, D3D_ROOT_SIGNATURE_VERSION_1_0, &rootSigBlob, &errorBlob);
if (FAILED(result) == true)
{
ShowErrorMassege(result, errorBlob);
}
ID3D12RootSignature* rootsignature = nullptr;
result = _dev->CreateRootSignature(0, rootSigBlob->GetBufferPointer(), rootSigBlob->GetBufferSize(), IID_PPV_ARGS(&rootsignature));
rootSigBlob->Release();
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUTは、頂点情報が存在することを意味します。
グラフィックスパイプラインステート
グラフィックスパイプラインステートは、DirectX12でグラフィックスパイプラインの設定を定義するオブジェクトです。
どのシェーダーで描画するか、アルファブレンドをどうするか、デプステストを行うか、ステンシルテストを行うか、レンダーターゲットをどうするか、どのルートシグネチャを使用するかなど、全体的な設定を持っています。
D3D12_GRAPHICS_PIPELINE_STATE_DESC gpipeline = {};
gpipeline.pRootSignature = rootsignature;
gpipeline.VS.pShaderBytecode = vsBlob->GetBufferPointer();
gpipeline.VS.BytecodeLength = vsBlob->GetBufferSize();
gpipeline.PS.pShaderBytecode = psBlob->GetBufferPointer();
gpipeline.PS.BytecodeLength = psBlob->GetBufferSize();
ルートシグネチャとシェーダーを設定
gpipeline.SampleMask = D3D12_DEFAULT_SAMPLE_MASK;
gpipeline.RasterizerState.MultisampleEnable = false;
gpipeline.RasterizerState.CullMode = D3D12_CULL_MODE_NONE;
gpipeline.RasterizerState.FillMode = D3D12_FILL_MODE_SOLID;
gpipeline.RasterizerState.DepthClipEnable = true;
基本的なサンプリングマスクを使用し、アンチエイリアシングは使用しないように
カリングはせず、内側は埋めるように設定
深度によるクリッピングは有効にする
gpipeline.BlendState.AlphaToCoverageEnable = false;
gpipeline.BlendState.IndependentBlendEnable = false;
D3D12_RENDER_TARGET_BLEND_DESC renderTargetBlendDesc = {};
renderTargetBlendDesc.BlendEnable = false;
renderTargetBlendDesc.LogicOpEnable = false;
renderTargetBlendDesc.RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
gpipeline.BlendState.RenderTarget[0] = renderTargetBlendDesc;
レンダーターゲットのブレンドに関する設定を行う部分です。
上記のコードは、ブレンドに関するすべての設定を無効にしています。
アルファテストとアルファブレンドについては、後で調べることにします。
gpipeline.InputLayout.pInputElementDescs = inputLayout;
gpipeline.InputLayout.NumElements = _countof(inputLayout);
先ほど作成したインプットレイアウトを設定します。
gpipeline.IBStripCutValue = D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLED;
gpipeline.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
D3D12_INDEX_BUFFER_STRIP_CUT_VALUE_DISABLEDは、インデックスバッファで特定の値に遭遇しても三角形ストリップが途切れないようにします。以前は三角形を描くためにインデックス値を使用して三角形を形成すると言いました。しかし、インデックスバッファである値に遭遇すると、描画を停止して新しい三角形の描画を始めることができますが、この値はそのような処理をしない設定です。
D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLEは、レンダリングする基本図形を三角形とする設定です。
gpipeline.NumRenderTargets = 1;
gpipeline.RTVFormats[0] = DXGI_FORMAT_B8G8R8A8_UNORM;
レンダーターゲットの数を設定し、フォーマットを指定します。
1ピクセルあたり4バイトのRGBAチャンネルを持つレンダーターゲットなので、DXGI_FORMAT_B8G8R8A8_UNORMにします。
gpipeline.SampleDesc.Count = 1;
gpipeline.SampleDesc.Quality = 0;
アンチエイリアシングに関する設定ですが、特に使用しないので上記のようにします。
必要な設定が終わったので、パイプラインステートオブジェクトを作成しましょう。
ID3D12PipelineState* mPipelinestate = nullptr;
result = mDev->CreateGraphicsPipelineState(&gpipeline, IID_PPV_ARGS(&mPipelinestate));
ビューポートとシーザーラクト
ビューポートとシーザーラクトが必要です。
ビューポートはレンダリングの結果を画面にどのように表示するかを設定します。
D3D12_VIEWPORT viewport = {};
viewport.Width = window_width;
viewport.Height = window_height;
viewport.TopLeftX = 0;
viewport.TopLeftY = 0;
viewport.MaxDepth = 1.0f;
viewport.MinDepth = 0.0f;
当然画面全体に表示させるため、最初に設定したウィンドウのサイズに設定します。
シーザーラクトは、ビューポートに表示されるシーンをどこからどこまで実際に表示するかを設定します。
これも同様にビューポートを画面いっぱいに表示するので、ビューポートのように設定すればよいです。
D3D12_RECT scissorrect = {};
scissorrect.top = 0;
scissorrect.left = 0;
scissorrect.right = scissorrect.left + window_width;
scissorrect.bottom = scissorrect.top + window_height;
描画の準備がすべて整いました。
描画
前の章で作成した色をクリアする命令の後に続けて作成します。
mCmdList->SetPipelineState(mPipelinestate);
mCmdList->SetGraphicsRootSignature(mRootsignature);
mCmdList->RSSetViewports(1, &viewport);
mCmdList->RSSetScissorRects(1, &scissorrect);
mCmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
mCmdList->IASetVertexBuffers(0, 1, &vbView);
mCmdList->IASetIndexBuffer(&ibView);
mCmdList->DrawIndexedInstanced(6, 1, 0, 0, 0);
パイプラインステートを設定
ビューポートとシザー矩形を設定
プリミティブを三角形リストに設定
頂点バッファビューとインデックスバッファビューを設定
DrawIndexedInstancedで描画命令
ここまでしてビルドして確認してみましょう。
なぜ白くなるのかと思ったら、ピクセルシェーダーで出力色を変えてみてください。
float4 BasicPS(Output input) : SV_TARGET
{
return float4(1, 0, 0, 1);
}
私は赤に変えました。
ちゃんと赤色で表示されます。
今は四角形を望む位置に置いたことがないので、画面いっぱいにレンダリングされているのでしょう。
ピクセルシェーダーの色を変更するとちゃんと色が変わるので、正常に描画されていることは間違いありません。
DirectXTex インストール
今回はテクスチャを読み込めましょう。
もちろん、PNGやJPGのような画像をそのまま使用するのではなく、これらの画像ファイルをDirectXで使用できるように読み込む必要があります。
しかし、直接画像を読み込むコードを準備するのは非常に面倒です。
そこで、マイクロソフトが提供するDirectXTexライブラリを使用します。
DirectXTexは、さまざまな形式の画像ファイルをロードしたり、フィルタリング、ミップマップ生成などを簡単に行うことができます。
https://github.com/microsoft/DirectXTex
DirectXTex GitHubページで「Clone or download」ボタンをクリックし、「Download ZIP」を選択して圧縮ファイルをダウンロードします。
受け取った圧縮ファイルの圧縮を解凍し、環境に合ったVisualStudioプロジェクトを開きます。
私はDirectXTex_Desktop_2019_Win10を使用します。
VisualStudioが開いたら、ソリューションをビルドしてください。
私はDebug x64でビルドしました。
ライブラリディレクトリを追加しましょう。
Visual Studioで、プロジェクト → プロパティ → 構成プロパティ → リンカー → 全般を選択してください。
上記のように追加ライブラリディレクトリのDirectXTexのビルドされたフォルダパスを入力します。
私の場合はDirectXTex\DirectXTex\Bin\Desktop_2019_Win10\x64\Debugでした。
ヘッダーファイルも使用できるように、インクルードディレクトリも追加しましょう。
プロパティウィンドウでC++を選択し、追加インクルードディレクトリにDirectXTexヘッダーファイルがあるパスを追加します。
ここまで完了すると、DirectXTexを使用できるようになります。
ヘッダーを追加してライブラリをリンクしてください。
#include <DirectXTex.h>
#pragma comment(lib, "DirectXTex.lib")
テクスチャバッファの生成
画像を読み込んでテクスチャバッファを生成しましょう。
TexMetadata metadata = {};
ScratchImage scratchImg = {};
result = LoadFromWICFile(L"img/textest.png", WIC_FLAGS_NONE, &metadata, scratchImg);
私はプロジェクトフォルダのimgフォルダにあるtextest.pngを読み込みました。
ScratchImageは画像の実際のデータを持っており、
TexMetadataはテクスチャのサイズ、ミップマップレベル、テクスチャの次元数といった情報を持っています。
これで実際に使用するテクスチャバッファオブジェクトを作成しましょう。
頂点バッファと同じように、CreateCommittedResourceを使用してバッファオブジェクトを作成します。
D3D12_HEAP_PROPERTIES textureHeapprop = {};
textureHeapprop.Type = D3D12_HEAP_TYPE_CUSTOM;
textureHeapprop.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_WRITE_BACK;
textureHeapprop.MemoryPoolPreference = D3D12_MEMORY_POOL_L0;
textureHeapprop.CreationNodeMask = 0;
textureHeapprop.VisibleNodeMask = 0;
D3D12_HEAP_TYPE_CUSTOMを使用すると、何か特別な感じがします。
まず、この点を考慮してください。私たちは画像ファイルを読み込んだのですが、テクスチャバッファを生成したら、何らかの方法で読み込んだ画像の情報をテクスチャバッファに入れる必要があります。
そうなると、TypeをD3D12_HEAP_TYPE_DEFAULTに設定できないことは確かです。
なぜなら、D3D12_HEAP_TYPE_DEFAULTはCPUからアクセスできないため、読み込んだ画像情報をGPUバッファに入れることができないからです。
D3D12_HEAP_TYPE_UPLOADは頻繁にデータを更新する必要がある状況に適しています。
一度読み込んでバッファに画像データを入れるのであれば、わざわざ再度更新する理由はありません。
D3D12_HEAP_TYPE_READBACKはバンド幅が狭いです。テクスチャというデータは思ったより大きいデータなので、適していません。
D3D12_HEAP_TYPE_CUSTOMに設定し、CPUPagePropertyとMemoryPoolPreferenceを設定しましょう。
D3D12_CPU_PAGE_PROPERTY_WRITE_BACKは、CPUが書き込んだデータをシステムメモリに正しくフラッシュし、そのデータをGPUが即座に使用できるようにします。
D3D12_MEMORY_POOL_L0は、システムメモリにリソースを割り当て、CPUがアクセスできるようにします。D3D12_MEMORY_POOL_L1もありますが、これはGPUメモリにリソースを割り当てるため、CPUがアクセスすることはできません。
D3D12_RESOURCE_DESC resDesc = {};
resDesc.Format = metadata.format;
resDesc.Width = metadata.width;
resDesc.Height = metadata.height;
resDesc.DepthOrArraySize = metadata.arraySize;
resDesc.SampleDesc.Count = 1;
resDesc.SampleDesc.Quality = 0;
resDesc.MipLevels = metadata.mipLevels;
resDesc.Dimension = static_cast<D3D12_RESOURCE_DIMENSION>(metadata.dimension);
resDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
resDesc.Flags = D3D12_RESOURCE_FLAG_NONE;
上で画像をロードするときに取得したTexMetadataを使用してD3D12_RESOURCE_DESCを埋めます。
ID3D12Resource* texbuff = nullptr;
result = _dev->CreateCommittedResource(
&textureHeapprop,
D3D12_HEAP_FLAG_NONE,
&resDesc,
D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE,
nullptr,
IID_PPV_ARGS(&texbuff)
);
テクスチャバッファオブジェクトを生成しましょう。
ピクセルシェーダーで使用できるように D3D12_RESOURCE_STATE_PIXEL_SHADER_RESOURCE に設定します。
これで作成されたバッファに画像データを書き込みましょう。
私たちはWriteToSubresourceを使用します。この方法は非常に簡単ですが、もしテクスチャデータを頻繁に更新する必要がある場合は効率が悪い方法です。
今回はプログラムの初期に画像を読み込み、その後はデータを更新する予定がないため、この方法でも問題ありません。
const Image* img = scratchImg.GetImage(0, 0, 0);
result = texbuff->WriteToSubresource(0,
nullptr,
img->pixels,
img->rowPitch,
img->slicePitch);
ScratchImageでGetImageからサブリソースを取得できます。
1つの画像しか読み込んでいないため、最初に該当するサブリソースだけを取得すればいいでしょう。
WriteToSubresourceで最初のパラメーターはミップレベルです。ミップマップを使用するわけではないので0にします。
2番目のパラメーターは書き込み範囲ですが、全体のサブリソースを使用するのでnullptrで大丈夫です。
3番目は実際のデータ
4番目は行のバイト数
5番目は各スライスのバイト数
Image構造体のパラメータとほとんど一致するため、使用するのは難しくないでしょう。
テクスチャバッファが準備されました。GPUメモリには今、読み込んだtextestイメージがテクスチャリソースとして存在している状態です。
これを使用できるようにする必要があります。そのためには、ディスクリプタヒープとルートシグネチャにルートパラメータとサンプラーを設定する必要があります。
ディスクリプタ ヒープ
前回の記事でレンダーターゲットを使用するためにディスクリプタヒープを作成したことを覚えていますか?
今回はシェーダーで使用するシェーダーリソースのためのディスクリプタヒープを生成してみましょう。
ID3D12DescriptorHeap* basicDescHeap = nullptr;
D3D12_DESCRIPTOR_HEAP_DESC descHeapDesc = {};
descHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE;
descHeapDesc.NodeMask = 0;
descHeapDesc.NumDescriptors = 1;
descHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV;
result = _dev->CreateDescriptorHeap(&descHeapDesc, IID_PPV_ARGS(&basicDescHeap));
前回と異なる部分はFlagsとTypeです。
レンダーターゲットとは異なり、シェーダーで使用する必要があるため、D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLEでFlagsを設定します。
D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAVは、このディスクリプターヒープにシェーダーリソースビュー(SRV)、定数バッファービュー(CBV)を入れることができます。
では、シェーダーリソースビューを作りましょう。
D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
srvDesc.Format = metadata.format;
srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING;
srvDesc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D;
srvDesc.Texture2D.MipLevels = 1;
auto basicHeapHandle = basicDescHeap->GetCPUDescriptorHandleForHeapStart();
_dev->CreateShaderResourceView(texbuff, &srvDesc, basicHeapHandle);
ディスクリプタ ヒープのハンドルを使用してビューを作成する部分は以前と変わりません。
異なる部分はレンダーターゲットではないため、D3D12_SHADER_RESOURCE_VIEW_DESCを使用してCreateShaderResourceViewでビューを作成することです。
フォーマットはTexMetadataから取得し
Shader4ComponentMappingはD3D12_DEFAULT_SHADER_4_COMPONENT_MAPPINGマクロを使用します。特別な場合でない限り、これで問題ありません。
ViewDimensionは2次元テクスチャを使用するため、D3D12_SRV_DIMENSION_TEXTURE2Dに設定します。
ルートパラメータとサンプラー
前の記事では、空のルートシグネチャを作成しました。
今回は、テクスチャをシェーダーで参照できるようにルートパラメータを追加し、テクスチャをサンプリングできるようにサンプラーをルートシグネチャに追加してみましょう。
ルートパラメーターは、GPUがシェーダーで使用するデータを渡すための方法です。
1つのリソースだけを渡す方法もありますが、複数のリソースを渡せるようにディスクリプタテーブルを使用する方法で進めます。
テクスチャをGPUが使用するためのルートパラメーターコードは次のように記述します。
D3D12_DESCRIPTOR_RANGE textureDescriptorRange = {};
textureDescriptorRange.NumDescriptors = 1;
textureDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
textureDescriptorRange.BaseShaderRegister = 0;
textureDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
D3D12_ROOT_PARAMETER rootparam[1] = {};
rootparam[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootparam[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
rootparam[0].DescriptorTable.pDescriptorRanges = &textureDescriptorRange;
rootparam[0].DescriptorTable.NumDescriptorRanges = 1;
実際のところ、ディスクリプタテーブルを使うことは複数のリソースグループを扱うのにより適した方法ですが、今回は1つのリソースだけを設定する方法を見ていきましょう。
D3D12_DESCRIPTOR_RANGE パラメーターから見てみましょう。
NumDescriptorsはテーブルにいくつのディスクリプターが含まれるかを設定します。今回はシェーダーリソースビューが1つなので1にします。
RangeTypeはこのディスクリプターテーブルがシェーダーリソースビューであることを知らせます。
BaseShaderRegisterは0に設定します。これはシェーダーで使用するレジスタの番号ですが、後で再度説明します。
OffsetInDescriptorsFromTableStartはD3D12_DESCRIPTOR_RANGE_OFFSET_APPENDにします。以前にも似たようなものを見たことがありますね。ディスクリプターのオフセットを自動的に続けるようにするマクロです。
今ルートパラメータを見てみましょう。
ParameterTypeはディスクリプタテーブルを使用するようにD3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE
ShaderVisibilityはピクセルシェーダーで使用するため、D3D12_SHADER_VISIBILITY_PIXEL
DescriptorTable.pDescriptorRangesにはtextureDescriptorRangeのアドレスを渡します。
DescriptorTable.NumDescriptorRangesはtextureDescriptorRangeを1つだけ使用するように1にします。
もしtextureDescriptorRangeが配列で複数のディスクリプタテーブルを使用したい場合は、使用したい数に変更すれば良いでしょう。
後に複数のディスクリプタテーブルを使用する状況が来るので、複数の状況については後で見てみましょう。
作成したルートパラメータをルートシグネチャに追加します。
D3D12_ROOT_SIGNATURE_DESC rootSignatureDesc = {};
rootSignatureDesc.Flags = D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT;
rootSignatureDesc.pParameters = rootparam;
rootSignatureDesc.NumParameters = 1;
まだルートパラメータが1つだけなので、NumParametersは1にします。
さて、サンプラーを追加しましょう。
サンプラーは、テクスチャをどのように読み取り処理するかを定義するオブジェクトです。
シェーダーでテクスチャをサンプリングするためには必須です。
D3D12_STATIC_SAMPLER_DESC samplerDesc = {};
samplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplerDesc.BorderColor = D3D12_STATIC_BORDER_COLOR_TRANSPARENT_BLACK;
samplerDesc.Filter = D3D12_FILTER_MAXIMUM_MIN_MAG_MIP_LINEAR;
samplerDesc.MaxLOD = D3D12_FLOAT32_MAX;
samplerDesc.MinLOD = 0.0f;
samplerDesc.ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
samplerDesc.ComparisonFunc = D3D12_COMPARISON_FUNC_NEVER;
samplerDesc.RegisterSpace = 0;
AddressU、AddressV、AddressW、BorderColorについては、UVの話をするときにまた説明します。
Filter、MaxLOD、MinLODについても、ミップマップの話をするときにまた説明します。
ShaderVisibilityはピクセルシェーダーで使用するためにD3D12_SHADER_VISIBILITY_PIXELに設定してください。
ComparisonFuncは、テクスチャをサンプリングするときにサンプリングされた値と特定の参照値を比較するために使用される関数設定です。基本的なサンプラーを使用する場合は、D3D12_COMPARISON_FUNC_NEVERに設定して比較を行わないようにします。後でシャドウマッピングについて扱う予定です。
RegisterSpaceは、上記でD3D12_DESCRIPTOR_RANGEを記述するときに見たBaseShaderRegisterと似ています。今回も0に設定します。
作成が終わったら、ルートシグネチャに追加します。
NumStaticSamplers パラメータについては、もう一度話す必要はないでしょう。
rootSignatureDesc.pStaticSamplers = &samplerDesc;
rootSignatureDesc.NumStaticSamplers = 1;
コマンドを作成する前に、まだシェーダーを修正していません。
シェーダー修正
ピクセルシェーダーでテクスチャを使用するように修正しましょう。
今回はHLSLヘッダーファイルを作成することにしましょう。
新しい項目でHLSLヘッダーファイルを選択して追加してください。
生成されたファイルを開いて次のように記述してください。
Texture2D<float4> tex : register(t0);
SamplerState smp : register(s0);
struct Output
{
float4 svpos : SV_POSITION;
float2 uv : TEXCOORD;
};
texとsmpの後ろにregister(t0)とregister(s0)が見えますか。
これは先ほど設定したD3D12_DESCRIPTOR_RANGEのBaseShaderRegister、D3D12_STATIC_SAMPLER_DESCのRegisterSpaceです。
シェーダーレジスターは、GPUのシェーディングでリソースを特定のスロットにバインドするために設定します。
簡単に考えれば、使用するデータに番号を付けると考えればいいでしょう。
コンスタントバッファーは b
テクスチャーは t
サンプラーは s
UAVは u
を使用します。
このレジスタは最大使用可能な数があります。私の記事では最大数に達するほど使用しないため、特に問題はありませんが、気になる方は調べてみてください。
struct Outputはテクスチャをサンプリングする必要があるため、uvをピクセルシェーダーに渡す必要があり、頂点シェーダーの戻り値をOutputに変更するために作成しました。
頂点シェーダーを修正しましょう。
#include "BasicShaderHeader.hlsli"
Output BasicVS (float4 pos : POSITION, float2 uv : TEXCOORD)
{
Output output;
output.svpos = pos;
output.uv = uv;
return output;
}
上で作成したHLSLヘッダーを追加し、戻り値をOutputに変更してください。
そして内部ではuvを渡せるようにしてください。
ピクセルシェーダーも修正しましょう。
#include "BasicShaderHeader.hlsli"
float4 BasicPS(Output input) : SV_TARGET
{
return float4(tex.Sample(smp, input.uv));
}
ここにもヘッダーを追加し、今度はテクスチャをサンプリングしてその色を出力するようにしましょう。
サンプルでサンプラーを使用することを見てください。
両方のシェーダーにヘッダーを追加しましたが、以前にシェーダーをコンパイルしたことをもう一度思い出してください。
result = D3DCompileFromFile(L"BasicVertexShader.hlsl",
nullptr,
D3D_COMPILE_STANDARD_FILE_INCLUDE,
"BasicVS",
"vs_5_0",
D3DCOMPILE_DEBUG | D3DCOMPILE_SKIP_OPTIMIZATION,
0,
&vsBlob,
&errorBlob);
pIncludeをD3D_COMPILE_STANDARD_FILE_INCLUDEに設定しました。このように設定しないと、ヘッダーファイルを使用できません。
テクスチャを適用してレンダリング
では、描画を実行してみましょう。
DrawIndexedInstancedで描画する前に、これだけ追加してください。
mCmdList->SetDescriptorHeaps(1, &basicDescHeap);
mCmdList->SetGraphicsRootDescriptorTable(0, basicDescHeap->GetGPUDescriptorHandleForHeapStart());SetDescriptorHeaps
SetDescriptorHeapsでGPUにどのディスクリプタヒープを参照するかを知らせます。
そして、0番ルートパラメータにはbasicDescHeapディスクリプタヒープでシェーダーリソースビューを作成したハンドルの位置をバインドします。
ビルドして確認してみましょう。
私は適切な画像をロードしました。
ロードした画像が正しくテクスチャとして使用されていることを確認できます。
しかし、作った四角形が画面いっぱいに表示されています。今度は、好きな場所に配置してみましょう。
コンスタントバッファを作って、位置情報をGPUに渡しましょう。
コンスタントバッファー
まず、コンスタントバッファーを作成する前に、バッファーに渡す行列を作成します。
XMMATRIX matrix = XMMatrixIdentity();
XMMATRIX worldMatrix = XMMatrixIdentity();
XMFLOAT3 eye(0, 0, -5);
XMFLOAT3 target(0, 0, 0);
XMFLOAT3 up(0, 1, 0);
XMMATRIX lookMatrix = XMMatrixLookAtLH(XMLoadFloat3(&eye), XMLoadFloat3(&target), XMLoadFloat3(&up));
XMMATRIX projectionMatrix = XMMatrixPerspectiveFovLH(XM_PIDIV2, static_cast<float>(window_width) / static_cast<float>(window_height), 1.0f, 10.0f);
matrix *= worldMatrix;
matrix *= lookMatrix;
matrix *= projectionMatrix;
3Dプログラミングを経験された方や行列変換に関する知識がある方は、上記のコードを説明しなくても何をしているのかすでにご存じでしょう。
しかし、行列変換にまったく初めて触れる方にはわからないかもしれません。
行列変換については別の記事でまとめることにします。
今回はよくわからない方は、単に位置情報だと思ってください。
これからコンスタントバッファを生成します。
ID3D12Resource* constBuff = nullptr;
mDev->CreateCommittedResource(
& CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
& CD3DX12_RESOURCE_DESC::Buffer((sizeof(matrix) + 0xff) & ~0xff),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(&constBuff)
);
以前、CreateCommittedResourceを使用する際にはD3D12_HEAP_PROPERTIESとD3D12_RESOURCE_DESCを直接作成して渡しましたが、今回は少し違いますね。
CD3DX12_HEAP_PROPERTIESとCD3DX12_RESOURCE_DESCを使用します。
これらを使用するには、まずヘッダーを追加する必要があります。
#include <d3dx12.h>
DirectXTexを追加しながら一緒に使えるようになったものです。
CD3DX12_HEAP_PROPERTIESとCD3DX12_RESOURCE_DESCは一種のユーティリティ関数です。
特殊な場合でない限り、D3D12_HEAP_PROPERTIESとD3D12_RESOURCE_DESCはほとんど同じ設定を使用することになります。しかし、毎回パラメータを直接入力するのはコードが長くなり面倒です。
だからこそ、ユーティリティ関数で簡単に使用するのです。
CD3DX12_HEAP_PROPERTIESはヒープのタイプだけを渡せば適切なD3D12_HEAP_PROPERTIESを返してくれますし、
CD3DX12_RESOURCE_DESC::Bufferはバッファのサイズだけを渡せば適切なD3D12_RESOURCE_DESCを返してくれます。
ここでバッファのサイズを伝えるときに何か変わったことがありますね。
(sizeof(matrix) + 0xff) & ~0xff)
こうしています。
これは、DirectX の定数バッファが 256 バイトのアラインメントを要求するためです。256 バイトでアラインメントされていると、GPU のメモリアクセスが最も効率的だと言われています。
もし matrix のサイズが 68 バイトなら、上記の演算を通じて 256 にバイトを返します。
上記のコードがなぜ 256 バイト単位の値を返すのか理解できない場合は、ビットマスクについて調べてください。
とにかくコンスタントバッファを使用する場合は、256 バイト単位でアラインメントが必要であることを覚えておいてください。
バッファを作ったので、Map を使って行列の値をマッピングしましょう。
XMMATRIX* mapMatrix;
result = constBuff->Map(0, nullptr, (void**)&mapMatrix);
*mapMatrix = matrix;
これでシェーダーで使用できるようにディスクリプタヒープにビューを作成する必要があります。
先ほどテクスチャを作成したときに使用したディスクリプタビューに追加すればよいです。
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc = {};
cbvDesc.BufferLocation = constBuff->GetGPUVirtualAddress();
cbvDesc.SizeInBytes = constBuff->GetDesc().Width;
basicHeapHandle.ptr += mDev->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
mDev->CreateConstantBufferView(&cbvDesc, basicHeapHandle);
D3D12_CONSTANT_BUFFER_VIEW_DESC のコードを見れば、難しくなく理解できると思います。
重要なのは、コンスタントバッファービューを生成する部分です。
先ほど、シェーダーリソースビューはディスクリプタヒープの開始位置に生成しました。
したがって、コンスタントバッファービューはシェーダーリソースビューのサイズ分だけ移動した位置に生成される必要があります。
GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV)でそのサイズを取得し、ハンドルの位置を移動させます。
さて、ルートパラメータを追加して、コンスタントビューをシェーダーにバインドできるようにしましょう。
D3D12_DESCRIPTOR_RANGE textureDescriptorRange = {};
textureDescriptorRange.NumDescriptors = 1;
textureDescriptorRange.RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_SRV;
textureDescriptorRange.BaseShaderRegister = 0;
textureDescriptorRange.OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
D3D12_DESCRIPTOR_RANGE constantBufferDescriptorRange = {};
constantBufferDescriptorRange .NumDescriptors = 1;
constantBufferDescriptorRange .RangeType = D3D12_DESCRIPTOR_RANGE_TYPE_CBV;
constantBufferDescriptorRange .BaseShaderRegister = 0;
constantBufferDescriptorRange .OffsetInDescriptorsFromTableStart = D3D12_DESCRIPTOR_RANGE_OFFSET_APPEND;
D3D12_ROOT_PARAMETER rootparam[2] = {};
rootparam[0].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootparam[0].ShaderVisibility = D3D12_SHADER_VISIBILITY_PIXEL;
rootparam[0].DescriptorTable.pDescriptorRanges = &textureDescriptorRange;
rootparam[0].DescriptorTable.NumDescriptorRanges = 1;
rootparam[1].ParameterType = D3D12_ROOT_PARAMETER_TYPE_DESCRIPTOR_TABLE;
rootparam[1].ShaderVisibility = D3D12_SHADER_VISIBILITY_VERTEX;
rootparam[1].DescriptorTable.pDescriptorRanges = &constantBufferDescriptorRange;
rootparam[1].DescriptorTable.NumDescriptorRanges = 1;
D3D12_DESCRIPTOR_RANGE を追加し、D3D12_DESCRIPTOR_RANGE_TYPE_CBV に設定します。
b0 レジスタを使用するために BaseShaderRegister は 0 にします。
そしてルートパラメーターを追加します。
行列変換はバーテックスシェーダーでのみ行うため、D3D12_SHADER_VISIBILITY_VERTEX にします。
シェーダーヘッダーにコンスタントバッファーに関する内容を追加します。
Texture2D<float4> tex : register(t0);
SamplerState smp : register(s0);
struct Output
{
float4 svpos : SV_POSITION;
float2 uv : TEXCOORD;
};
cbuffer cbuff0 : register(b0)
{
matrix mat;
};
バーテックスシェーダーでは、行列変換を行うようにコードを修正します。
#include "BasicShaderHeader.hlsli"
Output BasicVS (float4 pos : POSITION, float2 uv : TEXCOORD)
{
Output output;
output.svpos = mul(mat, pos);
output.uv = uv;
return output;
}
では、描画しましょう。
好きな位置に描画
テクスチャをバインドする部分にこのように追加してください。
mCmdList->SetDescriptorHeaps(1, &basicDescHeap);
mCmdList->SetGraphicsRootDescriptorTable(0, basicDescHeap->GetGPUDescriptorHandleForHeapStart());
auto heapHandle = basicDescHeap->GetGPUDescriptorHandleForHeapStart();
heapHandle.ptr += mDev->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
mCmdList->SetGraphicsRootDescriptorTable(1, heapHandle);
コンスタント バッファ ビューは、ディスクリプタ ヒープの開始点からシェーダー リソース ビューのサイズ分離れた場所に生成しました。
バインドするときもハンドルの位置をその分移動させます。
コンスタント バッファのルート パラメータ インデックスは1でした。SetGraphicsRootDescriptorTableに1を渡します。
さて、ビルドして確認してみましょう。
何か正しい位置に置かれた感じがします。
まだ行列について知らない方には、なぜこうなるのか理解できないかもしれませんが、今回はここまでにして締めくくりたいと思います。
UV
終わる前にUVについて話そうと思います。
すでに知っている方はスルーしてください。
UVはテクスチャでの座標です。
テクスチャは1次元的に見ると画像1枚です。その画像を任意の3Dモデルに貼り付けるための座標です。
バーテックスデータはほとんどそのUV値を持っています。
この記事では直接バーテックスを定義したため、私たちがUV値を入れましたが、通常はモデリングツールで作成されたモデルファイルを読み込んでレンダリングします。モデルを作る人が「このバーテックスの色はこの色だから、UV座標はこれだ」と設定した結果物です。
Uは横軸で、Vは縦軸です。
Zがある場合もありますが、これは3Dテクスチャを使用する際に見られます。
上の図はDirectXの場合ですが、左上が0,0で右下が1,1です。
これは必ずしもこのようなものではなく、プラットフォームによって規則が異なる場合があります。
OpenGLでは上の図とは異なります。
では、サンプラー設定で扱っていなかったパラメーターを再度見てみましょう。
samplerDesc.AddressU = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplerDesc.AddressV = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
samplerDesc.AddressW = D3D12_TEXTURE_ADDRESS_MODE_WRAP;
これは、UV座標がテクスチャ座標範囲を超える場合にどのように処理するかに関する設定です。
上の図のように、テクスチャ座標は最小が0,0、最大が1,1ですね。
もしこの範囲を超える値が入ってきたらどうすればいいでしょうか?
それのための設定です。
D3D12_TEXTURE_ADDRESS_MODE_WRAPはテクスチャ座標を越えると繰り返される設定です。
U値がもし1.2だとしたら0.2に置換されるようにするのです。
D3D12_TEXTURE_ADDRESS_MODE_CLAMPを使用すると、U値が0.0以下の場合は0.0、1.0以上の場合は1.0となり、値が座標を超えないようにする設定です。
この2つ以外にもいくつかあるので、必要な方はドキュメントを見つけて必要な設定を行ってください。
samplerDesc.Filter = D3D12_FILTER_MAXIMUM_MIN_MAG_MIP_LINEAR;
これは、サンプリングするテクスチャとレンダリングされる画面の解像度が異なる場合にどのようにサンプリング(補間)するかを決定するパラメータです。
ここで疑問を持つ方がいるかもしれません。
なぜなら、さっきバーテックスデータにUVデータがあると言いました。
でも、テクスチャサンプリングはピクセルシェーダーで行うのでは? そして、三角形はバーテックスが3つなのに、UV3つで三角形全体をどうやって塗るの?と思うかもしれません。
そこで、シェーダーがどのように動作するのかについても触れていこうと思います。
シェーダーの構造
この部分も3Dグラフィックスに関する知識がある方なら、すでに知っている内容でしょう。
そのような方はスキップしてください。
この部分では、グラフィックパイプラインについて簡単に説明します。
本当に簡略化して説明することをご理解ください。
上の図は、グラフィックスパイプラインを簡単に表現したものです。
三角形を描くとき、頂点が3つ必要でしたよね。
では、三角形を描くためにはバーテックスシェーダーが3回呼び出されるでしょう。
では、ピクセルシェーダーはどうでしょうか?
3回でしょうか?もちろん違います。画面に表示される三角形が占めるピクセルの数だけピクセルシェーダーが呼び出されます。
渡された頂点位置で三角形が構成されると、その三角形内にいくつのピクセルが入るかを見つけ出すのがラスタライザーです。
ピクセルシェーダーはラスタライザーが見つけたピクセルの数だけ呼び出されることになります。
UV値も同様です。ラスタライザーがピクセルシェーダーに渡されるピクセルの位置を計算するときに、UV値もそれに合わせて計算されます。
まとめ
さて、まとめましょう。
今回の記事では、ポリゴンを描くために頂点バッファとインデックスバッファを作成し、シェーダーを記述して描画を行いました。
描画した四角形にテクスチャを貼るために、テクスチャをロードし、テクスチャをシェーダーで使用するために、シェーダーリソースビューとコンスタントバッファビューを持つディスクリプタヒープを作成しました。
ここまでで、特別な場合を除いて、描画に必要な基本的な事項をすべて見ました。
今後はこの方法を使って、新しいモデルをロードしたり、テクスチャを追加するためにシェーダーリソースビューを追加したり、コンスタントバッファービューを追加することの繰り返しになります。
望むシーンを描画するために、素材を調達する方法を学んだと言えるでしょう。
行列変換、UV、グラフィックスパイプラインのように説明を簡素化した部分がありますが、これらを正しく説明しようとすると記事が非常に長くなります。
今回はUVとグラフィックスパイプラインについて簡略に扱いました。
行列変換は別の記事で改めて取り上げてみようと思います。
次回