了解 Direct3D 11 呈现管道

之前,你已了解如何在 “使用 DirectX 设备资源”中创建可用于绘图的窗口。 现在,你将了解如何生成图形管道,以及可以连接到该管道的位置。

你会记得有两个 Direct3D 接口定义图形管道: ID3D11Device,它提供 GPU 及其资源的虚拟表示形式;和 ID3D11DeviceContext,表示管道的图形处理。 通常,使用 ID3D11Device 实例来配置和获取开始在场景中处理图形所需的 GPU 资源,并使用 ID3D11DeviceContext 在图形管道中的每个适当着色器阶段处理这些资源。 通常不经常调用 ID3D11Device 方法,也就是说,仅当你设置场景或设备更改时。 另一方面,每次处理要显示的帧时,都会调用 ID3D11DeviceContext

此示例创建并配置一个适用于显示简单旋转的顶点着色立方体的最低图形管道。 它演示显示所需的大约最小资源集。 阅读此处的信息时,请注意给定示例的限制,可能需要扩展该示例以支持要呈现的场景。

此示例介绍图形的两个 C++ 类:设备资源管理器类和 3D 场景呈现器类。 本主题重点介绍 3D 场景呈现器。

多维数据集呈现器的作用是什么?

图形管道由 3D 场景呈现器类定义。 场景呈现器能够:

  • 定义常量缓冲区以存储统一数据。
  • 定义顶点缓冲区以保存对象顶点数据,并定义相应的索引缓冲区,使顶点着色器能够正确行走三角形。
  • 创建纹理资源和资源视图。
  • 加载着色器对象。
  • 更新图形数据以显示每个帧。
  • 呈现 (绘制) 图形到交换链。

前四个进程通常使用 ID3D11Device 接口方法来初始化和管理图形资源,最后两个进程使用 ID3D11DeviceContext 接口方法来管理和执行图形管道。

Renderer 类的实例作为main项目类的成员变量创建和管理。 DeviceResources 实例作为多个类(包括 main 项目类、应用视图提供程序类和 Renderer)的共享指针进行管理。 如果将 Renderer 替换为自己的类,请考虑声明 DeviceResources 实例并将其分配为共享指针成员:

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

App 类的 Initialize 方法中创建 DeviceResources 实例后,只需将指针传递到类构造函数 (或其他初始化方法) 。 如果希望main类完全拥有 DeviceResources 实例,还可以传递weak_ptr引用。

创建多维数据集呈现器

在此示例中,我们使用以下方法组织场景呈现器类:

  • CreateDeviceDependentResources:每当必须初始化或重启场景时调用。 此方法加载初始顶点数据、纹理、着色器和其他资源,并构造初始常量和顶点缓冲区。 通常,此处的大部分工作都是使用 ID3D11Device 方法完成的,而不是 ID3D11DeviceContext 方法。
  • CreateWindowSizeDependentResources:每当窗口状态更改时调用,例如,在重设大小或方向更改时调用。 此方法重新生成转换矩阵,例如相机的转换矩阵。
  • 更新:通常从管理即时游戏状态的程序部分调用;在此示例中,我们只是从 Main 类调用它。 让此方法从任何影响呈现的游戏状态信息(例如对象位置或动画帧的更新)以及任何全局游戏数据(如光线级别或对游戏物理的更改)中读取。 这些输入用于更新每帧常量缓冲区和对象数据。
  • 呈现:通常从管理游戏循环的程序部分调用;在本例中,它从 Main 类调用。 此方法构造图形管道:它将着色器绑定、将缓冲区和资源绑定到着色器阶段,并为当前帧调用绘图。

这些方法构成使用资产使用 Direct3D 呈现场景的行为主体。 如果使用新的呈现类扩展此示例,请在main项目类上声明它。 因此,此为:

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) 文件或纹理)时,异步执行此操作。 这样,你可以像) 其他设置任务一样 (同时进行其他工作,并且由于main循环不受阻止,因此你可以继续向用户显示视觉上有趣的内容, (例如为游戏) 加载动画。 此示例使用从 Windows 8 开始可用的 Concurrency::Tasks API;请注意用于封装异步加载任务的 lambda 语法。 这些 lambda 表示称为“线程外”的函数,因此显式捕获指向 ) (当前类对象的指针。

下面是如何加载着色器字节码的示例:

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 方法。

窗口大小资源按如下所示进行更新:静态消息运行获取指示窗口状态更改的多个可能事件之一。 然后,main循环将通知事件,并在 main 类实例上调用 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 方法 - 在此示例中,它由同名main类的 方法调用。 它有一个简单的用途:根据自上一帧 (或已用时间步长) 来更新场景几何图形和游戏状态。 在此示例中,我们只需每帧旋转一次立方体。 在实际游戏场景中,此方法包含更多代码,用于检查游戏状态、更新每帧 (或其他动态) 常量缓冲区、几何缓冲区和其他内存中资产。 由于 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 后,每个游戏循环调用此方法一次。 与 Update 一样,Render 方法也是从 main 类调用的。 这是使用 ID3D11DeviceContext 实例上的方法为帧构造和处理图形管道的方法。 这最终会调用 ID3D11DeviceContext::D rawIndexed。 请务必了解,此调用 (ID3D11DeviceContext 上定义的其他类似 Draw* 调用) 实际执行管道。 具体而言,当 Direct3D 与 GPU 通信以设置绘图状态、运行每个管道阶段,并将像素结果写入呈现目标缓冲区资源以供交换链显示时。 由于 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
        );
}

最好按顺序在上下文中设置各种图形管道阶段。 通常,顺序为:

  • 根据需要使用 更新) 中的数据 (,使用新数据刷新常量缓冲区资源。
  • 输入程序集 (IA) :这是附加定义场景几何图形的顶点和索引缓冲区的位置。 需要为场景中的每个对象附加每个顶点和索引缓冲区。 由于此示例仅包含多维数据集,因此非常简单。
  • 顶点着色器 (VS) :附加任何将转换顶点缓冲区中的数据的顶点着色器,并为顶点着色器附加常量缓冲区。
  • 像素着色器 (PS) :附加将在光栅化场景中执行每像素操作的任何像素着色器,并附加像素着色器的设备资源 (常量缓冲区、纹理等) 。
  • 输出合并 (OM) :这是着色器完成后混合像素的阶段。 这是规则的例外,因为在设置任何其他阶段之前,需要附加深度模具和呈现目标。 如果具有生成纹理(如阴影贴图、高度贴图或其他采样技术)的其他顶点和像素着色器,则可能有多个模具和目标 - 在本例中,在调用绘图函数之前,每个绘图通道都需要设置适当的目标 () 。

接下来,在 (使用 着色器和着色器资源) 的最后一部分,我们将介绍着色器并讨论 Direct3D 如何执行它们。

下一步

使用着色器和着色器资源