【第1部】シンプルバージョン編
1109.シンプルな三角形の描画(9)
更新描画処理
一般的なゲームのフレームワークでは、各オブジェクトの位置や回転、大きさといった
変化を実装する
Update処理と、各オブジェクトを描画する
Draw処理(もしくはRender処理)を分けて考えます。
ゲーム画面には、大量のオブジェクトが配置される可能性があります。それら一つ一つの
Update処理と
Draw処理を同時い行うと、何かと不便です。つまり
タスク分けのような関係になります。
BaseDx12における
更新描画処理は、まず、
WinMain.cppの以下の部分から始まります。
int MainLoop(HINSTANCE hInstance, HWND hWnd, int nCmdShow, int iClientWidth, int iClientHeight) {
//終了コード
int retCode = 0;
//ウインドウ情報。メッセージボックス表示チェックに使用
WINDOWINFO winInfo;
ZeroMemory(&winInfo, sizeof(winInfo));
//例外処理開始
try {
//COMの初期化
//サウンドなどで使用する
if (FAILED(::CoInitialize(nullptr))) {
// 初期化失敗
throw exception("Com初期化に失敗しました。");
}
basedx12::Scene scene;
basedx12::App::Init(hWnd, &scene, hInstance, nCmdShow, iClientWidth, iClientHeight);
//メッセージループ
MSG msg = { 0 };
while (WM_QUIT != msg.message) {
//キー状態が何もなければウインドウメッセージを得る
if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
//更新描画処理
basedx12::App::UpdateDraw();
}
//msg.wParamには終了コードが入っている
basedx12::App::Destroy();
retCode = (int)msg.wParam;
}
catch (exception & e) {
//STLエラー
//マルチバイトバージョンのメッセージボックスを呼ぶ
if (GetWindowInfo(hWnd, &winInfo)) {
MessageBoxA(hWnd, e.what(), "エラー", MB_OK);
}
else {
MessageBoxA(nullptr, e.what(), "エラー", MB_OK);
}
retCode = 1;
}
catch (...) {
//原因不明失敗した
if (GetWindowInfo(hWnd, &winInfo)) {
MessageBox(hWnd, L"原因不明のエラーです", L"エラー", MB_OK);
}
else {
MessageBox(nullptr, L"原因不明のエラーです", L"エラー", MB_OK);
}
retCode = 1;
}
//例外処理終了
//COMのリリース
::CoUninitialize();
return retCode;
}
赤くなっている部分ですが、アイドリングループごとに
basedx12::App::UpdateDraw()関数が呼ばれます。
その内容は以下です。
void App::UpdateDraw() {
m_timer.Tick();
m_controler.ResetControlerState();
if (m_pSceneBase && m_baseDevice)
{
m_baseDevice->OnUpdate();
m_baseDevice->OnDraw();
}
}
ここでは
タイマーの更新および
コントローラを初期化して、デバイスの
OnUpdate()関数そして
OnDraw()関数を呼び出します。
更新処理
m_baseDevice->OnUpdate();
は仮想関数ですので、
GameDevice::OnUpdate()関数が呼ばれます。以下がその内容です。
void GameDevice::OnUpdate()
{
App::GetSceneBase().OnUpdate();
}
このように
シーンの
OnUpdate()関数を呼び出します。この関数も
SceneBaseクラスを親に持つ仮想関数ですので、結果的に
Scene::OnUpdate()関数が呼ばれます。内容は以下です。
void Scene::OnUpdate() {
}
このように
何もしていません。このサンプルは、できるだけ単純にするために、オブジェクトの移動、などの変化はしない内容となっています。次のサンプルからは、ここに記述が入ります。
描画処理
更新処理は、何もしなくていいのですが、
描画処理は処理が必要です。
m_device->OnUpdate()に続いて
m_device->OnDraw()が呼ばれます。
これは以下の様な処理になります。
void GameDevice::OnDraw()
{
// 描画のためのコマンドリストを集める
PopulateCommandList();
// 描画用コマンドリスト実行
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
// フロントバッファに表示
ThrowIfFailed(GetIDXGISwapChain3()->Present(1, 0));
//次のフレームに移動
MoveToNextFrame();
}
コマンドリストを集める
最初に呼ばれる
は
コマンドリストを集める処理です。以下の様な内容です。
// 描画のためのコマンドリストを集める
void GameDevice::PopulateCommandList()
{
ThrowIfFailed(m_commandAllocators[m_frameIndex]->Reset());
//コマンドリストのリセット(パイプライン指定なし)
CommandList::Reset(m_commandAllocators[m_frameIndex], m_commandList);
// Set necessary state.
m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
m_commandList->RSSetViewports(1, &m_viewport);
m_commandList->RSSetScissorRects(1, &m_scissorRect);
// Indicate that the back buffer will be used as a render target.
m_commandList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET
)
);
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
m_rtvHeap->GetCPUDescriptorHandleForHeapStart(),
m_frameIndex,
m_rtvDescriptorHandleIncrementSize
);
m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
// Record commands.
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
// シーンに個別描画を任せる
App::GetSceneBase().OnDraw();
// Indicate that the back buffer will now be used to present.
m_commandList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT
)
);
ThrowIfFailed(m_commandList->Close());
}
この関数は、基本的に
DirectX-Graphics-Samplesを参考にしてますが、微妙に違います。
DirectX-Graphics-Samplesのほうは
GameDeviceとか
Sceneとかの概念はないので、この関数内で、コマンドリストに、描画処理を集めてますが、
BaseDx12では、個別の描画を
シーンに任せます。
コマンドアロケータとコマンドリストのリセット
ここではまず
ThrowIfFailed(m_commandAllocators[m_frameIndex]->Reset());
//コマンドリストのリセット(パイプライン指定なし)
CommandList::Reset(m_commandAllocators[m_frameIndex], m_commandList);
で、
コマンドアロケータとコマンドリストのリセットを行います。
コマンドリストのリセットは、パイプラインを指定することもできるのですが、個別描画の中で、オブジェクト単位でパイプラインステートを設定することもあるので、この指定はシーン側に任せます。
ルートシグネチャと描画領域
このあと
m_commandList->SetGraphicsRootSignature(m_rootSignature.Get());
m_commandList->RSSetViewports(1, &m_viewport);
m_commandList->RSSetScissorRects(1, &m_scissorRect);
で
ルートシグネチャの設定を行い
ビューポートと
シザー矩形を設定します。
ビューポートはこれまでの
DirectXにあった概念で、
GPUのデバイス座標と
ウインドウのクライアント座標を結びつけるものです。
GameDeviceの親クラスである
BaseDeviceクラスのコンストラクタですでに初期化されています。
デフォルトは、
WinMain.cppによって作成された、ウインドウの幅、高さが設定されています。奥行(depth)は
0.0から1.0に設定されています。
GameDeviceの初期化時に再設定することもできます。
シザー矩形は、ビューポートのうち、どの領域を描画するかを指定します。画面の中を部分的に描画することもできるように、なっています。デフォルトはビューポート同様
BaseDeviceクラスのコンストラクタで初期化されており、ビューポート全体を描画するようになっています。変更する場合はこちらも、
GameDeviceの初期化時などに行います。
バリアを張る
Dx12では、バックバッファに書き込むタイミングで、他からの書き込みを制限する
バリアの実装もプログラマが行います。
その記述は以下です。
m_commandList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET
)
);
このバリアは、バックバッファがレンダーターゲットとして使用される、推移時のバリアです。
レンダーターゲットビューの作成と設定
この後、
レンダーターゲットビューを、ハンドルという形で作成し、コマンドリストに設定します。
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(
m_rtvHeap->GetCPUDescriptorHandleForHeapStart(),
m_frameIndex,
m_rtvDescriptorHandleIncrementSize
);
m_commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
背景色の設定
背景色は
背景色でクリアするという形で設定します。ここで設定するのは
濃い青の背景色です。
ここでも
コマンドリストで行います。先ほど作成した
rtvHandleも再利用します。
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
シーンに個別描画を任せる
ここから先は、
シーンに個別描画を任せます。
App::GetSceneBase().OnDraw();
この呼び出しで、
Scene::OnDraw()関数が呼ばれます。
void Scene::OnDraw() {
auto baseDevice = App::GetBaseDevice();
auto commandList = baseDevice->GetCommandList();
commandList->SetPipelineState(m_pipelineState.Get());
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->IASetVertexBuffers(0, 1, &m_baseMesh->GetVertexBufferView());
commandList->DrawInstanced(3, 1, 0, 0);
}
ここでは、まず
auto baseDevice = App::GetBaseDevice();
auto commandList = baseDevice->GetCommandList();
で、デバイスクラスと、コマンドリストを取り出します。
commandList->SetPipelineState(m_pipelineState.Get());
で、パイプラインステートを設定します。
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
ここで
3角形として描画するという設定を行います。
commandList->IASetVertexBuffers(0, 1, &m_baseMesh->GetVertexBufferView());
で頂点バッファを設定します。
m_baseMesh->GetVertexBufferView()で
D3D12_VERTEX_BUFFER_VIEWの参照が帰りますので、そのアドレスを渡します。
そして
commandList->DrawInstanced(3, 1, 0, 0);
で、描画します。パラメータは
3つの頂点を1つだけの意味です。
Scene::OnDraw()の処理が終わると
GameDevice::PopulateCommandList()関数に制御が戻ります。
GameDevice::PopulateCommandList()に戻る
戻ったら、再びバリアを張ります。
m_commandList->ResourceBarrier(
1,
&CD3DX12_RESOURCE_BARRIER::Transition(
m_renderTargets[m_frameIndex].Get(),
D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT
)
);
これは、バックバッファを表示用に切り替える間のバリアです。
最後に
ThrowIfFailed(m_commandList->Close());
で、コマンドリストをクローズします。
GameDevice::PopulateCommandList()の終了
PopulateCommandList()が終わると
GameDevice::OnDraw()に制御が戻ります。
コマンドリストの実行
GameDevice::OnDraw()では
コマンドリストの実行を行います。
// 描画用コマンドリスト実行
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);
このように
コマンドリストのポインタの配列を作成します。ここでは1つですね。
作成したら
コマンドキューでそのコマンドリストを実行します。
ここまでで
バックバッファへの書き込みは終了です。
// フロントバッファに表示
ThrowIfFailed(GetIDXGISwapChain3()->Present(1, 0));
で、そのバックバッファを
表示用に切り替えます。
これでこのターンでの描画処理は終了です。
次のフレームの準備
その後
//次のフレームに移動
MoveToNextFrame();
で、次のフレームに移動します。
具体的には以下のようになっています。
void BaseDevice::MoveToNextFrame()
{
// Schedule a Signal command in the queue.
const UINT64 currentFenceValue = m_fenceValues[m_frameIndex];
ThrowIfFailed(m_commandQueue->Signal(m_fence.Get(), currentFenceValue));
// Update the frame index.
m_frameIndex = GetIDXGISwapChain3()->GetCurrentBackBufferIndex();
// If the next frame is not ready to be rendered yet, wait until it is ready.
if (m_fence->GetCompletedValue() < m_fenceValues[m_frameIndex])
{
ThrowIfFailed(m_fence->SetEventOnCompletion(m_fenceValues[m_frameIndex], m_fenceEvent));
WaitForSingleObjectEx(m_fenceEvent, INFINITE, FALSE);
}
// Set the fence value for the next frame.
m_fenceValues[m_frameIndex] = currentFenceValue + 1;
}
ここは
DirectX-Graphics-Samplesのものと同じです。
これで
更新、および描画処理の終了です。
次項は、終了時の処理を説明します。