Direct3D 11 レンダリング パイプラインについて

以前は、「 DirectX デバイス リソースを操作する」で描画に使用できるウィンドウを作成する方法について説明しました。 次に、グラフィックス パイプラインを構築する方法と、それにフックできる場所について説明します。

グラフィックス パイプラインを定義する Direct3D インターフェイスには、GPU とそのリソースの仮想表現を提供する ID3D11Device という 2 つの Direct3D インターフェイスがあることを思い出します。および ID3D11DeviceContext。パイプラインのグラフィックス処理を表します。 通常、 ID3D11Device のインスタンスを使用して、シーン内のグラフィックスの処理を開始するために必要な GPU リソースを構成して取得し、 ID3D11DeviceContext を 使用して、グラフィックス パイプライン内の適切なシェーダー ステージごとにこれらのリソースを処理します。 通常、 ID3D11Device メソッドを呼び出す頻度は低く、つまり、シーンを設定したとき、またはデバイスが変更された場合にのみ呼び出します。 一方、表示するフレームを処理するたびに ID3D11DeviceContext を呼び出します。

次の使用例は、頂点網掛けの単純な回転キューブを表示するのに適した最小限のグラフィックス パイプラインを作成して構成します。 これは、表示に必要なリソースの最小セットを示しています。 ここでの情報を読んでいるとき、レンダリングするシーンをサポートするために拡張する必要がある場合がある、特定の例の制限事項に注意してください。

この例では、グラフィックス用の 2 つの C++ クラス (デバイス リソース マネージャー クラスと 3D シーン レンダラー クラス) について説明します。 このトピックでは、特に 3D シーン レンダラーについて説明します。

キューブ レンダラーは何を行いますか?

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

  • 均一なデータを格納する定数バッファーを定義します。
  • オブジェクトの頂点データを保持する頂点バッファーと、頂点シェーダーが三角形を正しく歩くために対応するインデックス バッファーを定義します。
  • テクスチャ リソースとリソース ビューを作成します。
  • シェーダー オブジェクトを読み込みます。
  • 各フレームを表示するようにグラフィックス データを更新します。
  • グラフィックスをスワップ チェーンにレンダリング (描画) します。

最初の 4 つのプロセスでは、通常、グラフィックス リソースの初期化と管理に ID3D11Device インターフェイス メソッドを使用し、最後の 2 つのプロセスでは ID3D11DeviceContext インターフェイス メソッドを使用してグラフィックス パイプラインを管理および実行します。

Renderer クラスのインスタンスが作成され、メイン プロジェクト クラスのメンバー変数として管理されます。 DeviceResources インスタンスは、メイン プロジェクト クラス、アプリ ビュー プロバイダー クラス、レンダラーなど、複数のクラス間で共有ポインターとして管理されますRenderer を独自のクラスに置き換える場合は、DeviceResources インスタンスを共有ポインター メンバーとして宣言して割り当てることも検討してください。

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

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

キューブ レンダラーを作成する

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

  • CreateDeviceDependentResources: シーンを初期化または再起動する必要があるときに呼び出されます。 このメソッドは、最初の頂点データ、テクスチャ、シェーダー、およびその他のリソースを読み込み、初期定数バッファーと頂点バッファーを構築します。 通常、ここでの作業のほとんどは 、ID3D11DeviceContext メソッドではなく ID3D11Device メソッドで行われます。
  • CreateWindowSizeDependentResources: サイズ変更が発生したときや向きが変更されたときなど、ウィンドウの状態が変更されるたびに呼び出されます。 このメソッドは、カメラ用の変換マトリックスなど、変換マトリックスを再構築します。
  • 更新: 通常、即時のゲーム状態を管理するプログラムの一部から呼び出されます。この例では、 Main クラスから呼び出します。 このメソッドは、オブジェクトの位置やアニメーション フレームの更新、ライト レベルやゲーム物理の変更などのグローバル ゲーム データなど、レンダリングに影響を与えるゲーム状態情報から読み取るようにします。 これらの入力は、フレームごとの定数バッファーとオブジェクト データを更新するために使用されます。
  • レンダリング: 通常、ゲーム ループを管理するプログラムの一部から呼び出されます。この場合は、 Main クラスから呼び出されます。 このメソッドは、グラフィックス パイプラインを構築します。シェーダーをバインドし、バッファーとリソースをシェーダー ステージにバインドし、現在のフレームの描画を呼び出します。

これらのメソッドは、アセットを使用して Direct3D でシーンをレンダリングするための動作の本体で構成されます。 この例を新しいレンダリング クラスで拡張する場合は、メイン プロジェクト クラスで宣言します。 だからその:

std::unique_ptr<Sample3DSceneRenderer> m_sceneRenderer;

これを次のようにします。

std::unique_ptr<MyAwesomeNewSceneRenderer> m_sceneRenderer;

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

シーン レンダリング メソッドの詳細を見てみましょう。

デバイスに依存するリソースを作成する

CreateDeviceDependentResources、ID3D11Device 呼び出しを使用してシーンとそのリソースを初期化するためのすべての操作を統合します。 このメソッドは、Direct3D デバイスがシーンに対して初期化された (または再作成された) ことを前提としています。 頂点シェーダー、ピクセル シェーダー、オブジェクトの頂点バッファーとインデックス バッファー、その他のリソース (テクスチャおよび対応するビューなど) など、シーン固有のすべてのグラフィックス リソースが再作成または再読み込みされます。

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) ファイルやテクスチャなどのリソースをディスクから読み込むたびに、非同期的に実行されます。 これにより、他の作業 (他のセットアップ タスクなど) を同時に実行し続けることができます。また、メイン ループがブロックされないため、ユーザーに視覚的に興味深いもの (ゲームの読み込みアニメーションなど) を表示し続けることができます。 この例では、Windows 8 以降で使用できる Concurrency::Tasks API を使用します。非同期読み込みタスクのカプセル化に使用されるラムダ構文に注意してください。 これらのラムダは、off-thread と呼ばれる関数を表しているため、現在のクラス オブジェクト (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);

頂点バッファーとインデックス バッファーを作成する方法の例を次に示します。

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;
}

この例では、メッシュやテクスチャは読み込まれません。 ゲームに固有のメッシュとテクスチャの種類を読み込むためのメソッドを作成し、非同期で呼び出す必要があります。

シーンごとの定数バッファーの初期値もここに設定します。 シーンごとの定数バッファーの例には、固定ライト、またはその他の静的シーン要素とデータが含まれます。

CreateWindowSizeDependentResources メソッドを実装する

CreateWindowSizeDependentResources メソッドは、ウィンドウサイズ、向き、または解像度が変更されるたびに呼び出されます。

ウィンドウ サイズ リソースは次のように更新されます。静的メッセージ プロシージャは、ウィンドウの状態の変化を示す可能性のあるいくつかのイベントの 1 つを取得します。 その後、メイン ループにイベントが通知され、メイン クラス インスタンスで 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 aspectRatioX = m_deviceResources->GetAspectRatio();
    float aspectRatioY = aspectRatioX < (16.0f / 9.0f) ? aspectRatioX / (16.0f / 9.0f) : 1.0f;

    DirectX::XMStoreFloat4x4(
        &m_constantBufferData.projection,
        DirectX::XMMatrixTranspose(
            DirectX::XMMatrixPerspectiveFovRH(
                2.0f * std::atan(std::tan(DirectX::XMConvertToRadians(70) * 0.5f) / aspectRatioY),
                aspectRatioX,
                0.01f,
                100.0f
                )
            )
        );
}

シーンに縦横比に依存するコンポーネントの特定のレイアウトがある場合は、その縦横比に合わせて再配置する場所です。 ここでも、処理後の動作の構成を変更することもできます。

Update メソッドを実装する

Update メソッドはゲーム ループごとに 1 回呼び出されます。この例では、同じ名前の メイン クラスの メソッドによって呼び出されます。 単純な目的があります。前のフレーム以降の経過時間 (または経過時間ステップ) に基づいて、シーンのジオメトリとゲームの状態を更新します。 この例では、キューブをフレームごとに 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 は、キューブの新しい変換マトリックスを使用して定数バッファーを更新します。 マトリックスは、頂点シェーダー ステージ中に頂点ごとに乗算されます。 このメソッドはフレームごとに呼び出されるため、動的定数バッファーと頂点バッファーを更新するメソッドを集計したり、グラフィックス パイプラインによる変換のためにシーン内のオブジェクトを準備するその他の操作を実行したりするのに適しています。

Render メソッドを実装する

このメソッドは、 Update を呼び出した後、ゲーム ループごとに 1 回呼び出されます。 Update と同様に、Render メソッドも メイン クラスから呼び出されます。 これは、 ID3D11DeviceContext インスタンスのメソッドを使用して、フレームに対してグラフィックス パイプラインを構築して処理するメソッドです。 これにより、 ID3D11DeviceContext::D rawIndexed の最終呼び出しが終わる。 この呼び出し (または ID3D11DeviceContext で定義されている他の同様の Draw* 呼び出し) が実際にパイプラインを実行することを理解しておくことが重要です。 具体的には、Direct3D が GPU と通信して描画状態を設定し、各パイプライン ステージを実行し、ピクセル結果をレンダー ターゲット バッファー リソースに書き込み、スワップ チェーンで表示する場合です。 CPU と GPU の間の通信にはオーバーヘッドが発生するため、可能であれば複数の描画呼び出しを 1 つに結合します。特に、シーンに多数のレンダリング オブジェクトがある場合です。

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 のデータを使用)。
  • 入力アセンブリ (IA): シーン ジオメトリを定義する頂点バッファーとインデックス バッファーをアタッチします。 シーン内の各オブジェクトの各頂点とインデックス バッファーをアタッチする必要があります。 この例にはキューブだけが含まれているため、非常に単純です。
  • 頂点シェーダー (VS): 頂点バッファー内のデータを変換する頂点シェーダーをアタッチし、頂点シェーダーの定数バッファーをアタッチします。
  • ピクセル シェーダー (PS): ラスター化されたシーンでピクセル単位の操作を実行するピクセル シェーダーをアタッチし、ピクセル シェーダー (定数バッファー、テクスチャなど) のデバイス リソースをアタッチします。
  • 出力マージ (OM): これは、シェーダーの完了後にピクセルがブレンドされるステージです。 他のステージを設定する前に深度ステンシルとレンダー ターゲットをアタッチするため、これはルールの例外です。 シャドウ マップ、高さマップ、その他のサンプリング手法などのテクスチャを生成する頂点シェーダーとピクセル シェーダーが追加されている場合、複数のステンシルとターゲットが存在する場合があります。この場合、描画関数を呼び出す前に、各描画パスに適切なターゲットを設定する必要があります。

次に、最後のセクション (シェーダーとシェーダー リソースを操作する) で、シェーダーを見て、Direct3D で実行する方法について説明します。

次へ

シェーダーとシェーダー リソースを操作する