三角形を描画するだけのような、DirectX11 を使った簡単なプログラムを作成する際に必要な処理を解説します。サンプルプログラムを見よう見まねで実装するだけでなく、3DGC描画の仕組みを把握してプログラミングしたい場合に役立ててもらえたら幸いです。
実際にプログラミングする際の手順は DirectX11 プログラミング事始め を、プログラム全体のコードに関しては GitHubのリポジトリ の DrawPrimitive を参考にしてください。本記事で記載するコードは DrawPrimitive から抜粋したものです。
大まかな枠組み
描画処理を説明すると以下の様になります。
- CPU で必要な変数の初期化と頂点情報の読み込みと加工そして描画方法の設定を行い、これら種々のデータを GPU に渡す。
(開発者側が頑張ってコードを書く部分) - GPU は受け取った描画方法に従って頂点情報を処理して画像を描画する。
(DirectX11とグラフィックドライバが頑張ってくれる部分)
~~これでもう完全に理解したかと思います。~~さすがにもう少し解説を加えます。
CPU側ではレンダリングの際に使用する変数の初期化、入力データの加工とGPUに渡す際のレイアウトの定義を行います。また、描画設定とデータの処理の仕方を指示するシェーダープログラムもGPUに渡します。
描画設定とシェーダープログラムに従って、GPUは受け取った頂点情報を画像として描画します。GPU が行う描画処理は複数のステージに分割されていて、ステージでは設定された処理によって頂点情報が加工され、その処理が完了すると次のステージに移行する、といったように多段階の処理が施されて最終的に描画データが生成されます。抽象的なデータ構造を持つ頂点情報からディスプレイに表示される画像を作成する一連の過程全体をグラフィックスパイプライン(またはレンダリングパイプライン)と呼びます。
上記の仕組みを把握しておくと見通しよくプログラミングできると思います。それでは 3DCG の用語も交えて実装の詳細部分に踏み込みます。
実装内容
3DCG の描画を行う際は以下のように DirectX11 のインターフェイスの初期化を行い、描画の際に必要な情報の設定を行います。
初期化処理と描画設定
- デバイスの作成
- デバイスコンテキストの作成
- スワップチェーンの作成
- レンダーターゲットの作成
- レンダーターゲットビューの作成
- ラスタライザステートの作成
- 深度・ステンシルの設定
- 深度ステンシルバッファの作成
- ビューポートの設定
- シェーダーの作成
- 入力レイアウトの定義
- 定数バッファの作成
やるべきことが多いですが、各項目を解説していきます。カッコ内に使用する DirectX11 のインターフェイスを記しますので、プログラミングする際の参考にしていただければ幸いです。
余計なおせっかいですが、一度に全て読む必要はありません。
概要を復習したくなった時、意味を忘れた時などにこの記事を思い出して読んでいただければよいのかなと思います。まあ、リンク先のほうが役立つと思うんですけどね。
デバイス(ID3D11Device)の作成
グラフィックドライバ・ビデオカードを表す仮想的なデバイスを表します。また、後ほど紹介するリソースとオブジェクトの生成と管理を行います。
デバイスコンテキスト(ID3D11DeviceContext)の作成
デバイス使用時の環境や設定を表します。レンダリングに関する命令の生成を行います。
スワップチェーン(IDXGISwapChain)の作成
スワップチェーンはフロントバッファ(画面に表示されている画像)と
バックバッファ(グラフィックパイプラインの処理が完了し、画面に表示する準備ができた画像情報)を入れ替える(swap)ことで画面を更新する枠組みを意味します。
レンダーターゲット(ID3D11Texture2D)の作成
描画対象であるバックバッファを指定するためにレンダーターゲットを作成する必要があります。Texture2D と書かれているように、インターフェイスそのものはレンダーターゲット専用のものではありません。GetBuffer メソッドのドキュメントの通り、リソースを表すインターフェイスであれば何でもよいです。しかし、デバイスコンテキスト等、リソースを正確にレンダーターゲットと認識する必要がある立場から見るとこれだけでは困るので次に紹介するレンダーターゲットビューをバックバッファとバインドさせます。
レンダーターゲットビュー(ID3D11RenderTargetView)の作成
指定したバックバッファをレンダーターゲットとして認識させるためにレンダーターゲットビューを用います。
今回の例のように、ビューをリソースにバインドさせることでレンダリングを行う対象であるかテクスチャとして参照するか等々を定めることができます。
また、1つのリソースに複数のビューをバインドさせることも可能です。
ラスタライザーステート(ID3D11RasterizerState)の作成
グラフィックパイプラインのラスタライザーステージにおける処理をラスタライザーステートで設定します。ラスタライザーステージでは
ポリゴン(プリミティブとも言います)を2次元のピクセル情報に変換します。
以下の様にラスタライザーステートの設定を行うことができ、今回は反時計回りを表面と定義し、裏面は描画しないように設定しました。
bool CDxGraphic::CreateDefaultRasterizerState()
{
D3D11_RASTERIZER_DESC desc =
{
D3D11_FILL_SOLID, D3D11_CULL_BACK, TRUE, 0, 0.0f, 0.0f,
TRUE, FALSE, FALSE, FALSE
};
if (FAILED(device->CreateRasterizerState(&desc, &rs))) return false;
return true;
}
深度・ステンシルステート(ID3D11DepthStencilState)の作成
2次元画像に落とし込んだ時にオブジェクトの前後関係からどのピクセルを描画するかしないかを深度・ステンシルテストと呼ばれる処理で判定します。その際に必要な設定を以下の様に深度・ステンシルステートで定義します。
bool CDxGraphic::CreateDepthStencilState()
{
D3D11_DEPTH_STENCIL_DESC desc =
{
TRUE, D3D11_DEPTH_WRITE_MASK_ALL, D3D11_COMPARISON_LESS,
FALSE, D3D11_DEFAULT_STENCIL_READ_MASK, D3D11_DEFAULT_STENCIL_WRITE_MASK,
D3D11_STENCIL_OP_KEEP, D3D11_STENCIL_OP_KEEP, D3D11_STENCIL_OP_KEEP, D3D11_COMPARISON_ALWAYS
};
if (FAILED(device->CreateDepthStencilState(&desc, &dss))) return false;
return true;
}
深度・ステンシルバッファの作成
ステートを定義したので、バッファとビューを以下のように作成します。
bool CDxGraphic::CreateStencilBuffer(int w, int h)
{
D3D11_TEXTURE2D_DESC texdesc =
{
static_cast<UINT>(w), static_cast<UINT>(h), 1, 1,
DXGI_FORMAT_R24G8_TYPELESS, sampledesc, D3D11_USAGE_DEFAULT, D3D11_BIND_DEPTH_STENCIL | D3D11_BIND_SHADER_RESOURCE
};
if (FAILED(device->CreateTexture2D(&texdesc, nullptr, &depthtex))) return false;
D3D11_DEPTH_STENCIL_VIEW_DESC dsvdesc =
{
DXGI_FORMAT_D24_UNORM_S8_UINT, D3D11_DSV_DIMENSION_TEXTURE2D
};
if (FAILED(device->CreateDepthStencilView(depthtex, &dsvdesc, &dsv))) return false;
return true;
}
ビューポートの作成
描画先の領域内で実際に描画する範囲をビューポートによって指定します。
// ビューポートの設定
D3D11_VIEWPORT vp[] =
{
{ 0, 0, static_cast<FLOAT>(w), static_cast<FLOAT>(h), 0, 1.0f }
};
context->RSSetViewports(1, vp);
シェーダー(ID3D11VertexShader, ID3D11GeometryShader, ID3D11PixelShader)の作成
DirectX11 では柔軟なアプリケーション開発が出来るよう、GPU が行う描画処理をシェーダプログラムで記述します。HLSL(High Level Shader Language)と呼ばれる言語で命令内容を記述しコンパイルします。これを基にしてシェーダのインターフェイスを作成します。今回のサンプルプログラムでは頂点シェーダーとジオメトリシェーダー、ピクセルシェーダーを作成しています。
他のシェーダーも含めた詳しい説明は公式ドキュメントを参考にして下さい。
入力レイアウトの作成
シェーダーに渡す頂点情報のレイアウトを定義する必要があります。
D3D11_INPUT_ELEMENT_DESC 構造体で以下の例の様にレイアウトを定義し、デバイスでグラフィックパイプラインに渡すための入力レイアウトを作成します。
D3D11_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT , 0, sizeof(float) * 3, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
UINT num = ARRAYSIZE(layout);
if (FAILED(device->CreateInputLayout(layout, num, &csodata.front(), csosize, &inputlayout.p)))
return false;
"POSITION"や"COLOR"の様に文字列としてセマンティクスを定義し、座標値と色情報がデータに含まれていることをシェーダーに知らせます。
定数バッファ(ID3D11Buffer)の作成
この用語だけだと何のバッファだと疑問を持つと思います。3次元の頂点情報を2次元に変換する際に用いる変換行列や光源等々、GPU 側からみると定数として扱われる数値を定数バッファと呼びます。乱暴な言い方をすると、頂点情報を受け取ったけどそれ以外の値もCPU側から貰いたいので、それらを定数バッファとして扱います。サンプルプログラムでは変換行列(ワールド行列、ビュー行列、プロジェクション行列)を定義し、それらを定数バッファとしてシェーダーに渡す処理を書いています。
以上で描画の準備が整ったので、画面への表示を行う処理を書いていきます。
表示するデータの作成から画面への表示まで
- データの読み込み
- 変換行列の設定
- 定義したフォーマットに沿って頂点情報を整理する
- 作成したシェーダーのセット
- 定数バッファの値を設定
- シェーダーに頂点データを渡す
- バックバッファに描画するデータを渡す
- 描画データをバックバッファからフロントバッファにスワップする
これらの処理をサンプルプログラムでは LoadSampleData() メソッド及び Render() メソッドで行っています。LoadSampleData() メソッド内ではデータの読み込みとビュー行列・プロジェクション行列の値の設定を行います。
(2020/08/12 追記 : 範囲 for 文の箇所を修正しました。)
void CDxGraphic::LoadSampleData(int w, int h)
{
float nearz = 1 / 1000.0f;
float farz = 10.0f;
d3dprojmatrix = DirectX::XMMatrixPerspectiveFovRH(M_PI / 4.0f, 1.0f * w / h, nearz, farz);
DirectX::XMVECTOR eye = DirectX::XMVectorSet(0.0f, -5.0f, 0.57735f, 0.0f);
DirectX::XMVECTOR focus = DirectX::XMVectorSet(0.0f, 0.0f, 0.57735f, 0.0f);
DirectX::XMVECTOR up = DirectX::XMVectorSet(0.0f, 0.0f, 1.0f, 0.0f);
d3dviewmatrix = DirectX::XMMatrixLookAtRH(eye, focus, up);
std::vector<float> vertexarray;
for (const Vertex& v : InputData)
{
vertexarray.push_back(v.position[0]);
vertexarray.push_back(v.position[1]);
vertexarray.push_back(v.position[2]);
vertexarray.push_back(v.color[0]);
vertexarray.push_back(v.color[1]);
vertexarray.push_back(v.color[2]);
vertexarray.push_back(v.color[3]);
}
numindices = static_cast<UINT>(vertexarray.size()) / 7;
vertexbuffer.Release();
D3D11_BUFFER_DESC bdvertex =
{
static_cast<UINT>(sizeof(float) * vertexarray.size()),
D3D11_USAGE_DEFAULT,
D3D11_BIND_VERTEX_BUFFER
};
D3D11_SUBRESOURCE_DATA srdv = { &vertexarray.front() };
device->CreateBuffer(&bdvertex, &srdv, &vertexbuffer.p);
Render();
}
変換行列に関してさらっと説明しますと、1つのオブジェクトのみを置いた座標系から複数のオブジェクトを置くワールド座標系への変換行列をワールド行列、ワールド座標系からカメラを中心としたビュー座標系への変換行列をビュー行列、ビュー行列から2次元のスクリーン座標への変換行列をプロジェクション行列と呼びます。今回は読み込んだ座標値をそのままワールド座標に置くので、ワールド行列は単位行列のままにします。読み込むデータは1枚の三角形に関する頂点情報のみなので、カメラ座標を決め打ちしてビュー行列を設定します。また、ウィンドウの幅と高さをもとにプロジェクション座標を設定します。
上記のように頂点情報の読み込みと行列の値のセットを終えたのち、ID3D11DeviceContext インターフェスのメソッドを用いて GPU に頂点バッファと定数バッファを渡します。そして、Draw() メソッドで描画命令を送り、 ID3D11SwapChain インターフェイスの Present() メソッドでグラフィックスパイプラインの処理が完了したバッファをスワップすることで画面にポリゴンを移すことができます。サンプルプログラムでは以下のようにして Render()メソッドを実装しました。
void CDxGraphic::Render()
{
UINT strides = sizeof(CoordColor);
UINT offset = 0;
if (context == nullptr) return;
// バックバッファと深度バッファのクリア
FLOAT backcolor[4] = { 1.f, 1.f, 1.f, 1.f };
context->ClearRenderTargetView(rtv, backcolor);
context->ClearDepthStencilView(dsv, D3D11_CLEAR_DEPTH, 1.0f, 0);
// 頂点データに渡すデータのレイアウトを設定
context->IASetInputLayout(inputlayout);
// 頂点シェーダー, ジオメトリシェーダー, ピクセルシェーダーの設定
context->VSSetShader(vertexshader, nullptr, 0);
context->GSSetShader(geometryshader, nullptr, 0);
context->PSSetShader(pixelshader, nullptr, 0);
// ラスタライザーステートを設定
context->RSSetState(rs);
MatrixBuffer matrixbuf = {
// シェーダーでは列優先(column_major)で行列データを保持するため, 転置を行う
DirectX::XMMatrixTranspose(d3dprojmatrix),
DirectX::XMMatrixTranspose(d3dviewmatrix),
DirectX::XMMatrixTranspose(d3dworldmatrix)
};
// マトリックスバッファの設定
context->UpdateSubresource(matrixbuffer, 0, nullptr, &matrixbuf, 0, 0);
context->VSSetConstantBuffers(0, 1, &matrixbuffer.p);
context->GSSetConstantBuffers(0, 1, &matrixbuffer.p);
// 深度・ステンシルバッファの使用方法を設定
context->OMSetDepthStencilState(dss, 0);
context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
context->IASetVertexBuffers(0, 1, &vertexbuffer.p, &strides, &offset);
context->Draw(numindices, 0);
// 作成したプリミティブをウィンドウへ描画
if (swapchain != nullptr)
swapchain->Present(0, 0);
}
最後に
本記事と前回書いた記事(DirectX11 プログラミング事始め) を参考にすれば画面に三角形が映し出されるプログラムができると思います。シェーダーや面の裏表、変換行列に関しては十分に説明できなかったので、機会があればまた記事にしようかと思います。~~いつになるかわからないのでリンク先を辿った方が早いです。~~各箇所に出てきたメソッドと構造体等々の詳細に関しては公式ドキュメントの定義をご覧いただければと思います。私も全ての処理に精通しているわけではないので、構造体に変な値を入れていることがあるかもしれません。その際にはご連絡いただければ幸いです。
参考
[Direct3D 11 Graphics(マイクロソフト公式)]
(https://docs.microsoft.com/en-us/windows/win32/api/_direct3d11/index)
グラフィックパイプラインの概要(マイクロソフト公式)
DirectX11シェーダー入門 | ZeroGram
☆PROJECT ASURA☆ [Direct3D 11] 『Direct3D 11 再入門』
[いまさらDirect3D11入門]
(https://tositeru.github.io/ImasaraDX11/)