DX Understand the Direct3D 11 rendering pipeline - yoshimune/LearningDirectX11 GitHub Wiki

https://docs.microsoft.com/ja-jp/windows/desktop/direct3dgetstarted/understand-the-directx-11-2-graphics-pipeline

前回までで、DirectX device resouces を使って描画ができるウインドウをどうやって作成するか、を学びました。今回は、どうやってグラフィックスパイプラインを構築するか、グラフィックスパイプラインに接続する場所について学びます。

グラフィックスパイプラインを定義する2つのDirect3Dインターフェイスがあったことを思い出すでしょう。ID3D11DeviceはGPUとリソースの仮想表現を提供します。ID3D11DeviceContextはパイプラインのグラフィックスプロセス提供します。典型的には、ID3D11Deviceのインスタンスは、シーン上のグラフィックスプロセスの開始時に必要なGPUリソースの設定と取得に使います。また、ID3D11DeviceContextは、グラフィックスパイプライン上のそれぞれ適切なシェーダーステージのときにこれらのリソースの処理するためにつかいます。通常、シーンのセットアップ時、またはデバイスが変更されたときのみID3D11Deviceメソッドを呼びます。一方で、ID3DDeviceContextはフレームを表示するたび毎回呼ばれます。

この例では、簡単なvartex-shaded cubeの回転表示に合わせて最小のグラフィックスパイプラインの作成と設定をしています。表示に必要なリソースをおよそ最小のセットで例示します。ここでこの情報を呼んでいるときに、レンダリング死体シーンをサポートするためにそれを拡張しなければならない場合の例題の制限に注意してください。

この例は2つのグラフィックスC++クラスをカバーしています。デバイスリソース管理クラスと、3Dシーンレンダリングクラスです。このトピックは特に3Dシーンレンダラに焦点をあてています。

What does the cube renderer do?

グラフィックスパイプラインは3Dシーンレンダラークラスによって定義されます。シーンレンダラーは以下のことが可能です。

  • Uniform dataを保持するためのコンスタントバッファを定義する
  • 頂点シェーダが三角形を正しく走査できるようにするために、オブジェクトの頂点情報と対応するインデックスバッファを保持しておくためのvertex buffer を定義する。
  • テクスチャリソースとリソースビューを作成する
  • シェーダーオブジェクトをロードする
  • それぞれのフレームを表示するためにグラフィックスデータを更新する
  • スワップチェーンにグラフィックスを描画する

最初の4つのプロセスは典型的にはID3D11Deviceインターフェイスメソッドをグラフィックリソースの初期化と管理のために使用します。そして、最後の2つはID3D11DeviceContextインターフェイスメソッドをグラフィックスパイプラインの管理と実行のために使います。

Rendererクラスのインスタンスはメインプロジェクトクラスのメンバー変数として作成され管理されます。DeviceResourcesインスタンスはいくつかのクラス(メインプロジェクトクラス、App viewprovider クラス、Renderer)と共有するポインターとして管理されます。もし、Rendererを自作のクラスと交換した場合、共有ポインタメンバーとしてDeviceResourcesインスタンスの宣言とアサインを考慮してください。

std::shared_ptr<DX::DeviceResources> m_deviceResources;

DeviceResourcesインスタンスがAppクラスのInitializeメソッド内で作成された後に、クラスコンストラクタ(または初期化メソッド)にポインタを渡してください。もし代わりにメインクラスが完全にDeviceResourcesインスタンスを所有したい場合は、weak_ptr参照を渡すこともできます。

Create the cube renderer

この例では、シーンレンダラークラスを以下のメソッドを使って整理します。

  • CreateDeviceDependentResources: シーンが初期化されるまたは再起動する必要があるときに呼び出されます。このメソッドはinitial vertexデータ、テクスチャ、シェーダー、他のリソースをロードし、initial constantとvertex バッファを構築します。典型的には、ここでのほとんどの作業はID3DDeviceメソッドを用いて完了しています。(ID3D11DeviceContextではなく)
  • CreateWindowSizeDependentResources: ウィンドウステートが変更されたときに呼ばれます。リサイズの発生時、回転の変更時のようなときです。このメソッドは変換行列を再構築します。これらはカメラのようなものです。
  • Update: 典型的には、即時的なゲームステートを管理するプログラムの一部から呼ばれます。この例では、Main クラスからのみ呼ばれます。このメソッドには、いくつかのレンダリングに影響を与えるゲームステート情報(オブジェクトの位置、アニメーションフレームの更新、ライトレベルやゲーム物理の変更などのグローバルなゲームデータなど)を読みこませます。これの入力はフレームごとのコンスタントバッファとオブジェクトデータの更新に使われます。
  • Render: 典型的には、ゲームループを管理するプログラムの一部から呼ばれます。このケースでは、Mainクラスから呼ばれます。このメソッドはグラフィックスパイプラインを構築します。シェーダーのバインド、バッファーとリソースをシェーダーステージにバインド、現在のフレームに描画を発生させる、などです。

これらのメソッドはアセットを使用したDirect3Dでシーンをレンダリングするためのビヘイビア本体を含みます。もしこの例を新しいレンダリングクラスに拡張する場合は、メインプロジェクトクラスに以下を宣言してください。

std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;

を以下に変更してください

std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;

もう一度、この例はメソッドが同じシグネチャを実装していることを前提としています。もしシグネチャが変わっていた場合、Mainループを確認し、それに応じて変更します。

Create device dependent resources

CreateDeviceDependentResources は、ID3D11Device呼び出しを使ってシーンとリソース初期化のための全てのオペレーションを統合します。このメソッドは、Direct3Dデバイスがシーンの初期化がされた(または再生成された)ことを前提としています。全てのシーンの特定のグラフィックリソースを再生成または再ロードします。vertex,pixelシェーダー、頂点・インデックスバッファ、その他のリソース(例えば、テクスチャとそれに対応するビュー)などです。

CreateDeviceDependentResourcesを見てみましょう

void Renderer::CreateDeviceDependentResources()
{
    // Compile shaders using the Effects library.
    auto CreateShadersTask = Concurrency::create_task(
            [this]( )
            {
                CreateShaders();
            }
        );

    // Load the geometry for the spinning cube.
    auto CreateCubeTask = CreateShadersTask.then(
            [this]()
            {
                CreateCube();
            }
        );
}

void Renderer::CreateWindowSizeDependentResources()
{
    // Create the view matrix and the perspective matrix.
    CreateViewAndPerspective();
}

ディスクからリソース(リソースはコンパイルされたシェーダーオブジェクト(CSOまたは.cso)かテクスチャのようなもの)をロードするときは非同期で行われます。これは(別のタスクセットアップのような)別の仕事を同じ時間にすることを許可します。メインループはブロックされません。なにかおもしろいもの(たとえばゲームのローディングアニメーション)をユーザーに対して表示し続けることが可能です。
Concurrency::Tasks はWindows8から利用が可能なAPIです。非同期ローディングタスクのカプセル化に使われるラムダ構文に注意してください。これらのラムダ式はオフスレッドと呼ばれる関数を代表します。よって、現在のクラスオブジェクト(this)のポインタは明示的に取得されます。

以下は、バイトコードで書かれたシェーダーをロード方法を示したサンプルです。

HRESULT hr = S_OK;

// Use the Direct3D device to load resources into graphics memory.
ID3D11Device* device = m_deviceResources->GetDevice();

// You'll need to use a file loader to load the shader bytecode. In this
// example, we just use the standard library.
FILE* vShader, * pShader;
BYTE* bytes;

size_t destSize = 4096;
size_t bytesRead = 0;
bytes = new BYTE[destSize];

fopen_s(&vShader, "CubeVertexShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, vShader);
hr = device->CreateVertexShader(
    bytes,
    bytesRead,
    nullptr,
    &m_pVertexShader
    );

D3D11_INPUT_ELEMENT_DESC iaDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },

    { "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT,
    0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};

hr = device->CreateInputLayout(
    iaDesc,
    ARRAYSIZE(iaDesc),
    bytes,
    bytesRead,
    &m_pInputLayout
    );

delete bytes;


bytes = new BYTE[destSize];
bytesRead = 0;
fopen_s(&pShader, "CubePixelShader.cso", "rb");
bytesRead = fread_s(bytes, destSize, 1, 4096, pShader);
hr = device->CreatePixelShader(
    bytes,
    bytesRead,
    nullptr,
    m_pPixelShader.GetAddressOf()
    );

delete bytes;

CD3D11_BUFFER_DESC cbDesc(
    sizeof(ConstantBufferStruct),
    D3D11_BIND_CONSTANT_BUFFER
    );

hr = device->CreateBuffer(
    &cbDesc,
    nullptr,
    m_pConstantBuffer.GetAddressOf()
    );

fclose(vShader);
fclose(pShader);

vertexとindexバッファの作り方の例です

HRESULT Renderer::CreateCube()
{
    HRESULT hr = S_OK;

    // Use the Direct3D device to load resources into graphics memory.
    ID3D11Device* device = m_deviceResources->GetDevice();

    // Create cube geometry.
    VertexPositionColor CubeVertices[] =
    {
        {DirectX::XMFLOAT3(-0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3(  0,   0,   0),},
        {DirectX::XMFLOAT3(-0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3(  0,   0,   1),},
        {DirectX::XMFLOAT3(-0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3(  0,   1,   0),},
        {DirectX::XMFLOAT3(-0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(  0,   1,   1),},

        {DirectX::XMFLOAT3( 0.5f,-0.5f,-0.5f), DirectX::XMFLOAT3(  1,   0,   0),},
        {DirectX::XMFLOAT3( 0.5f,-0.5f, 0.5f), DirectX::XMFLOAT3(  1,   0,   1),},
        {DirectX::XMFLOAT3( 0.5f, 0.5f,-0.5f), DirectX::XMFLOAT3(  1,   1,   0),},
        {DirectX::XMFLOAT3( 0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(  1,   1,   1),},
    };
    
    // Create vertex buffer:
    
    CD3D11_BUFFER_DESC vDesc(
        sizeof(CubeVertices),
        D3D11_BIND_VERTEX_BUFFER
        );

    D3D11_SUBRESOURCE_DATA vData;
    ZeroMemory(&vData, sizeof(D3D11_SUBRESOURCE_DATA));
    vData.pSysMem = CubeVertices;
    vData.SysMemPitch = 0;
    vData.SysMemSlicePitch = 0;

    hr = device->CreateBuffer(
        &vDesc,
        &vData,
        &m_pVertexBuffer
        );

    // Create index buffer:
    unsigned short CubeIndices [] = 
    {
        0,2,1, // -x
        1,2,3,

        4,5,6, // +x
        5,7,6,

        0,1,5, // -y
        0,5,4,

        2,6,7, // +y
        2,7,3,

        0,4,6, // -z
        0,6,2,

        1,3,7, // +z
        1,7,5,
    };

    m_indexCount = ARRAYSIZE(CubeIndices);

    CD3D11_BUFFER_DESC iDesc(
        sizeof(CubeIndices),
        D3D11_BIND_INDEX_BUFFER
        );

    D3D11_SUBRESOURCE_DATA iData;
    ZeroMemory(&iData, sizeof(D3D11_SUBRESOURCE_DATA));
    iData.pSysMem = CubeIndices;
    iData.SysMemPitch = 0;
    iData.SysMemSlicePitch = 0;
    
    hr = device->CreateBuffer(
        &iDesc,
        &iData,
        &m_pIndexBuffer
        );

    return hr;
}

この例ではメッシュまたはテクスチャをロードしていません。ゲーム固有のメッシュとテクスチャタイプをロードするためのメソッドを作成し、非同期で呼び出す必要があります。

また、シーンごとのconstant bufferの初期値もここに設定してください。この例でのシーンごとのconstant buffer は固定のライトまたは別の静的シーンの要素とデータを含みます。

Implement the CreateWindowSizeDependentResources method

CreateWindowSizeDependentResources メソッドは、ウィンドウサイズ・回転・解像度が変更されたときに毎時呼ばれます。

ウィンドウサイズリソースは次のように更新されます。_static message proc_がウインドウステートの変更を支持するイベントのうち実行可能な一つを取得します。メインループは次にイベントについて知らされ、メインクラスのインスタンスでCreateWindowSizeDependentResourcesを呼びます。次に、シーンレンダラクラスでCreateWindowSizeDependentResourcesの実装を呼びます。

このメソッドの最初の仕事は、ウィンドウプロパティの変更によってビジュアルが混乱、または使用不可となっていないことを確認することです。この例では、リサイズ・回転したウィンドウに応じてプロジェクション行列を新しいFOVで更新します。

既に、DeviceResources内のウィンドウリソースの作成コードを見ています。それはスワップチェーン(とバックバッファ)とレンダーターゲットビューです。これはどうやってレンダラがアスペクト比に応じた変更を行うかという例です。

void Renderer::CreateViewAndPerspective()
{
    // Use DirectXMath to create view and perspective matrices.

    DirectX::XMVECTOR eye = DirectX::XMVectorSet(0.0f, 0.7f, 1.5f, 0.f);
    DirectX::XMVECTOR at  = DirectX::XMVectorSet(0.0f,-0.1f, 0.0f, 0.f);
    DirectX::XMVECTOR up  = DirectX::XMVectorSet(0.0f, 1.0f, 0.0f, 0.f);

    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.view,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixLookAtRH(
                eye,
                at,
                up
                )
            )
        );

    float aspectRatio = m_deviceResources->GetAspectRatio();

    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.projection,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixPerspectiveFovRH(
                DirectX::XMConvertToRadians(70),
                aspectRatio,
                0.01f,
                100.0f
                )
            )
        );
}

もしシーンが特定のアスペクト比に依存するコンポーネントのレイアウト持っていた場合、これはそれらをアスペクト比に合わせて並べ替える場所となります。ここでまた、ポストプロセッシング動作の設定を変更することもできます。

Implement the Update method

今回のケースでは、Updateメソッドはゲームループ1回ごとに呼ばれます。同じ名前のメインクラスメソッドから呼ばれます。前回のフレームからの経過時間(または経過ステップ数)を元にジオメトリとゲームステートを更新する、という単純な目的があります。この例では、単純にキューブを回転させています。実際のゲームシーンでは、このメソッドは、ゲームステートのチェック、フレームごと(または別の動的要因)にコンスタントバッファ・ジオメトリバッファ、その他のメモリ上のアセットを更新するための多くのコードを含みます。CPUとGPU間やり取りはオーバーヘッドを含んでいるので、実際に最後のフレームから変更があるバッファのみ更新されているということを確認してください。コンスタントバッファは効率よくするため必要に応じてまとめたり分割したりできます。

void Renderer::Update()
{
    // Rotate the cube 1 degree per frame.
    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.world,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixRotationY(
                DirectX::XMConvertToRadians(
                    (float) m_frameCount++
                    )
                )
            )
        );

    if (m_frameCount == MAXUINT)  m_frameCount = 0;
}

このケースでは、Rotateがコンスタントバッファを新しい回転行列をキューブに与えて更新しています。この行列は、vertex shaderステージで頂点毎に乗算されます。このメソッドが毎フレームごとに呼ばれるため、動的定数とvertex bufferを更新するメソッドを集約したり、グラフィックスパプラインによる変換のためにシーン内のオブジェクトを準備するその他の走査を実行するのに適しています。

Implenent the Render method

このメソッドは、Updateが終わった後、ゲームループ1回ごとに呼ばれます。Updateのように、Renderメソッドはまたメインクラスから呼ばれます。これは、フレームがID3D11DeviceContextインスタンスでメソッドを使うために、グラフィックスパイプラインが構築され処理されるメソッドです。これによりID3D11DeviceContext::DrawIndexedの最終コールを終了します。この呼出(または別の似たID3D11DeviceContext で定義された Drawコール)が実際にパイプライン上実行することを理解することが重要です。特に、これはDirect3DがドローイングステートをセットするためにGPUとやり取りするとき、それぞれのパイプラインを走り、そしてスワップチェーンによって表示のためのレンダーターゲットバッファにピクセルの結果を書きます。可能であれば、特に死0ンにレンダリングされたオブジェクトがたくさんある場合は、CPUとGPU間の通信にオーバーヘッドがあるため、複数の描画呼び出しを一つに結合します。

void Renderer::Render()
{
    // Use the Direct3D device context to draw.
    ID3D11DeviceContext* context = m_deviceResources->GetDeviceContext();

    ID3D11RenderTargetView* renderTarget = m_deviceResources->GetRenderTarget();
    ID3D11DepthStencilView* depthStencil = m_deviceResources->GetDepthStencil();

    context->UpdateSubresource(
        m_pConstantBuffer.Get(),
        0,
        nullptr,
        &m_constantBufferData,
        0,
        0
        );

    // Clear the render target and the z-buffer.
    const float teal [] = { 0.098f, 0.439f, 0.439f, 1.000f };
    context->ClearRenderTargetView(
        renderTarget,
        teal
        );
    context->ClearDepthStencilView(
        depthStencil,
        D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL,
        1.0f,
        0);

    // Set the render target.
    context->OMSetRenderTargets(
        1,
        &renderTarget,
        depthStencil
        );

    // Set up the IA stage by setting the input topology and layout.
    UINT stride = sizeof(VertexPositionColor);
    UINT offset = 0;

    context->IASetVertexBuffers(
        0,
        1,
        m_pVertexBuffer.GetAddressOf(),
        &stride,
        &offset
        );

    context->IASetIndexBuffer(
        m_pIndexBuffer.Get(),
        DXGI_FORMAT_R16_UINT,
        0
        );
    
    context->IASetPrimitiveTopology(
        D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
        );

    context->IASetInputLayout(m_pInputLayout.Get());

    // Set up the vertex shader stage.
    context->VSSetShader(
        m_pVertexShader.Get(),
        nullptr,
        0
        );

    context->VSSetConstantBuffers(
        0,
        1,
        m_pConstantBuffer.GetAddressOf()
        );

    // Set up the pixel shader stage.
    context->PSSetShader(
        m_pPixelShader.Get(),
        nullptr,
        0
        );

    // Calling Draw tells Direct3D to start sending commands to the graphics device.
    context->DrawIndexed(
        m_indexCount,
        0,
        0
        );
}

グラフィックスパイプラインステージの変数をコンテキスト上に並べてセットするの良いプラクティスです。典型的には、以下のように並びます。

  • コンスタントバッファリソースを必要に応じて(Updateで使ったデータ)新しいデータにリフレッシュする。
  • Input assembly(IA): これはシーンジオメトリで定義されたvertex bufferとindex butterをアタッチする場所です。それぞれのvertex buffer、index bufferをシーン上のそれぞれのオブジェクトにアタッチする必要があります。この例はキューブだけであるため、とてもシンプルです。
  • Vertex shader(VS): vertex bufferのデータが変更されたvertex shaderをアタッチしてください。そしてvertex shader のためのコンスタントバッファもアタッチしてください。
  • Pixel shader(PS): ピクセルシェーダーをアタッチしてください。ラスタライズされたシーン上のピクセル毎に命令を実行します。そしてピクセルシェーダのためのデバイスリソース(コンスタントバッファ、テクスチャ、他)をアタッチしてください
  • Output merger(OM): これはピクセルがブレンドされるステージです。シェーダー処理が完了した後に実行されます。デプス・ステンシルとレンダーターゲットをその他のステージを設定する前にアタッチしているため、このステージは例外です。シャドウマップ・ハイとマップ・その他のサンプリングテクニックのようなテクスチャを生成する追加のバーテックス・ピクセルシェーダーを持っている場合、複数のステンシルとターゲットを持っているかもしれません。このケースでは、ドロー関数を呼ぶ前に、描画パスはターゲットセットの充填を必要とします。