渲染框架 I:渲染简介

注释

本主题是 使用 DirectX 教程系列创建简单的通用 Windows 平台(UWP)游戏的一部分。 该链接中的主题设置序列的上下文。

到目前为止,我们介绍了如何构建通用 Windows 平台(UWP)游戏,以及如何定义状态机来处理游戏流。 现在是时候了解如何开发呈现框架了。 让我们看看示例游戏如何使用 Direct3D 11 呈现游戏场景。

Direct3D 11 包含一组 API,这些 API 提供对高性能图形硬件的高级功能的访问权限,这些硬件可用于为图形密集型应用程序(如游戏)创建 3D 图形。

在屏幕上呈现游戏图形意味着基本上在屏幕上呈现一系列帧。 在每个帧中,必须基于视图呈现场景中可见的对象。

为了呈现帧,必须将所需的场景信息传递给硬件,以便它可以在屏幕上显示。 如果要在屏幕上显示任何内容,那么在游戏开始运行时,你需要立即开始渲染。

目标

设置基本呈现框架以显示 UWP DirectX 游戏的图形输出。 你可以松散地将其分解为这三个步骤。

  1. 建立与图形接口的连接。
  2. 创建绘制图形所需的资源。
  3. 通过呈现帧来显示图形内容。

本主题介绍如何呈现图形,涵盖步骤 1 和 3。

呈现框架 II:游戏呈现 介绍了步骤 2 — 如何设置呈现框架,以及如何在呈现发生之前准备数据。

开始吧

最好熟悉基本的图形和呈现概念。 如果你不熟悉 Direct3D 和呈现,请参阅 术语和概念,了解本主题中使用的图形和呈现术语的简要说明。

对于此游戏,GameRenderer 类表示此示例游戏的呈现器。 它负责创建和维护用于生成游戏视觉对象的所有 Direct3D 11 和 Direct2D 对象。 它还维护对 Simple3DGame 对象的引用,该对象用于检索要呈现的对象列表,以及提供给抬头显示器(HUD)的游戏状态。

在本教程的这一部分中,我们将重点介绍在游戏中呈现 3D 对象。

建立与图形接口的连接

有关如何访问用于渲染的硬件的信息,请参阅 定义游戏的 UWP 应用框架 主题。

App:Initialize方法

std::make_shared 函数用于创建一个指向 DX::DeviceResourcesshared_ptr,它还提供对设备的访问。

在 Direct3D 11 中,设备 用于分配和销毁对象、呈现基元,并通过图形驱动程序与图形卡通信。

void Initialize(CoreApplicationView const& applicationView)
{
    ...

    // At this point we have access to the device. 
    // We can create the device-dependent resources.
    m_deviceResources = std::make_shared<DX::DeviceResources>();
}

通过渲染帧显示图形

游戏场景需要在游戏启动时呈现。 呈现的指令从 GameMain::Run 方法开始,如下所示。

简单的流程是这样的。

  1. 更新
  2. 呈现
  3. 演示

GameMain 的 Run 方法

void GameMain::Run()
{
    while (!m_windowClosed)
    {
        if (m_visible) // if the window is visible
        {
            switch (m_updateState)
            {
            ...
            default:
                CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessAllIfPresent);
                Update();
                m_renderer->Render();
                m_deviceResources->Present();
                m_renderNeeded = false;
            }
        }
        else
        {
            CoreWindow::GetForCurrentThread().Dispatcher().ProcessEvents(CoreProcessEventsOption::ProcessOneAndAllPending);
        }
    }
    m_game->OnSuspending();  // Exiting due to window close, so save state.
}

更新

有关 gameMain::Update 方法中的游戏状态更新方式的详细信息,请参阅 游戏流管理 主题。

渲染

通过从 GameMain::Run调用 GameRenderer::Render 方法来实现呈现。

如果启用 立体声渲染,则有两个呈现通道 - 一个用于左眼,一个用于右眼。 在每个渲染过程中,我们将渲染目标和深度模板视图绑定到设备。 之后,我们还清除深度模板视图。

注释

可以使用其他方法实现立体渲染,例如,通过顶点实例化或几何着色器进行单次立体声渲染。 双渲染传递方法是实现立体声渲染的较慢但更方便的方法。

游戏运行并加载资源后,我们将在每次渲染过程中更新投影矩阵 。 对象与每个视图略有不同。 接下来,我们将建立 图形渲染管道

注释

有关如何加载资源的详细信息,请参阅 创建和加载 DirectX 图形资源

在此示例游戏中,呈现器旨在在所有对象中使用标准顶点布局。 这简化了着色器设计,并允许在着色器之间轻松更改,与对象的几何图形无关。

GameRenderer::Render 方法

我们将 Direct3D 上下文设置为使用输入顶点布局。 输入布局对象描述如何将顶点缓冲区数据传递到 渲染流水线

接下来,我们将 Direct3D 上下文设置为使用前面定义的常量缓冲区,这些缓冲区由 顶点着色器 管道阶段和 像素着色器 管道阶段使用。

注释

有关常量缓冲区定义的详细信息,请参阅 呈现框架 II:游戏呈现

由于同一输入布局和常量缓冲区集用于管道中的所有着色器,因此每帧只需设置一次。

void GameRenderer::Render()
{
    bool stereoEnabled{ m_deviceResources->GetStereoState() };

    auto d3dContext{ m_deviceResources->GetD3DDeviceContext() };
    auto d2dContext{ m_deviceResources->GetD2DDeviceContext() };

    int renderingPasses = 1;
    if (stereoEnabled)
    {
        renderingPasses = 2;
    }

    for (int i = 0; i < renderingPasses; i++)
    {
        // Iterate through the number of rendering passes to be completed.
        // 2 rendering passes if stereo is enabled.
        if (i > 0)
        {
            // Doing the Right Eye View.
            ID3D11RenderTargetView* const targets[1] = { m_deviceResources->GetBackBufferRenderTargetViewRight() };

            // Resets render targets to the screen.
            // OMSetRenderTargets binds 2 things to the device.
            // 1. Binds one render target atomically to the device.
            // 2. Binds the depth-stencil view, as returned by the GetDepthStencilView method, to the device.
            // For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-omsetrendertargets

            d3dContext->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());

            // Clears the depth stencil view.
            // A depth stencil view contains the format and buffer to hold depth and stencil info.
            // For more info about depth stencil view, go to: 
            // https://learn.microsoft.com/windows/uwp/graphics-concepts/depth-stencil-view--dsv-
            // A depth buffer is used to store depth information to control which areas of 
            // polygons are rendered rather than hidden from view. To learn more about a depth buffer,
            // go to: https://learn.microsoft.com/windows/uwp/graphics-concepts/depth-buffers
            // A stencil buffer is used to mask pixels in an image, to produce special effects. 
            // The mask determines whether a pixel is drawn or not,
            // by setting the bit to a 1 or 0. To learn more about a stencil buffer,
            // go to: https://learn.microsoft.com/windows/uwp/graphics-concepts/stencil-buffers

            d3dContext->ClearDepthStencilView(m_deviceResources->GetDepthStencilView(), D3D11_CLEAR_DEPTH, 1.0f, 0);

            // Direct2D -- discussed later
            d2dContext->SetTarget(m_deviceResources->GetD2DTargetBitmapRight());
        }
        else
        {
            // Doing the Mono or Left Eye View.
            // As compared to the right eye:
            // m_deviceResources->GetBackBufferRenderTargetView instead of GetBackBufferRenderTargetViewRight
            ID3D11RenderTargetView* const targets[1] = { m_deviceResources->GetBackBufferRenderTargetView() };

            // Same as the Right Eye View.
            d3dContext->OMSetRenderTargets(1, targets, m_deviceResources->GetDepthStencilView());
            d3dContext->ClearDepthStencilView(m_deviceResources->GetDepthStencilView(), D3D11_CLEAR_DEPTH, 1.0f, 0);

            // d2d -- Discussed later under Adding UI
            d2dContext->SetTarget(m_deviceResources->GetD2DTargetBitmap());
        }

        const float clearColor[4] = { 0.5f, 0.5f, 0.8f, 1.0f };

        // Only need to clear the background when not rendering the full 3D scene since
        // the 3D world is a fully enclosed box and the dynamics prevents the camera from
        // moving outside this space.
        if (i > 0)
        {
            // Doing the Right Eye View.
            d3dContext->ClearRenderTargetView(m_deviceResources->GetBackBufferRenderTargetViewRight(), clearColor);
        }
        else
        {
            // Doing the Mono or Left Eye View.
            d3dContext->ClearRenderTargetView(m_deviceResources->GetBackBufferRenderTargetView(), clearColor);
        }

        // Render the scene objects
        if (m_game != nullptr && m_gameResourcesLoaded && m_levelResourcesLoaded)
        {
            // This section is only used after the game state has been initialized and all device
            // resources needed for the game have been created and associated with the game objects.
            if (stereoEnabled)
            {
                // When doing stereo, it is necessary to update the projection matrix once per rendering pass.

                auto orientation = m_deviceResources->GetOrientationTransform3D();

                ConstantBufferChangeOnResize changesOnResize;
                // Apply either a left or right eye projection, which is an offset from the middle
                XMStoreFloat4x4(
                    &changesOnResize.projection,
                    XMMatrixMultiply(
                        XMMatrixTranspose(
                            i == 0 ?
                            m_game->GameCamera().LeftEyeProjection() :
                            m_game->GameCamera().RightEyeProjection()
                            ),
                        XMMatrixTranspose(XMLoadFloat4x4(&orientation))
                        )
                    );

                d3dContext->UpdateSubresource(
                    m_constantBufferChangeOnResize.get(),
                    0,
                    nullptr,
                    &changesOnResize,
                    0,
                    0
                    );
            }

            // Update variables that change once per frame.
            ConstantBufferChangesEveryFrame constantBufferChangesEveryFrameValue;
            XMStoreFloat4x4(
                &constantBufferChangesEveryFrameValue.view,
                XMMatrixTranspose(m_game->GameCamera().View())
                );
            d3dContext->UpdateSubresource(
                m_constantBufferChangesEveryFrame.get(),
                0,
                nullptr,
                &constantBufferChangesEveryFrameValue,
                0,
                0
                );

            // Set up the graphics pipeline. This sample uses the same InputLayout and set of
            // constant buffers for all shaders, so they only need to be set once per frame.
            // For more info about the graphics or rendering pipeline, see
            // https://learn.microsoft.com/windows/win32/direct3d11/overviews-direct3d-11-graphics-pipeline

            // IASetInputLayout binds an input-layout object to the input-assembler (IA) stage. 
            // Input-layout objects describe how vertex buffer data is streamed into the IA pipeline stage.
            // Set up the Direct3D context to use this vertex layout. For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetinputlayout
            d3dContext->IASetInputLayout(m_vertexLayout.get());

            // VSSetConstantBuffers sets the constant buffers used by the vertex shader pipeline stage.
            // Set up the Direct3D context to use these constant buffers. For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-vssetconstantbuffers

            ID3D11Buffer* constantBufferNeverChanges{ m_constantBufferNeverChanges.get() };
            d3dContext->VSSetConstantBuffers(0, 1, &constantBufferNeverChanges);
            ID3D11Buffer* constantBufferChangeOnResize{ m_constantBufferChangeOnResize.get() };
            d3dContext->VSSetConstantBuffers(1, 1, &constantBufferChangeOnResize);
            ID3D11Buffer* constantBufferChangesEveryFrame{ m_constantBufferChangesEveryFrame.get() };
            d3dContext->VSSetConstantBuffers(2, 1, &constantBufferChangesEveryFrame);
            ID3D11Buffer* constantBufferChangesEveryPrim{ m_constantBufferChangesEveryPrim.get() };
            d3dContext->VSSetConstantBuffers(3, 1, &constantBufferChangesEveryPrim);

            // Sets the constant buffers used by the pixel shader pipeline stage. 
            // For more info, see
            // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-pssetconstantbuffers

            d3dContext->PSSetConstantBuffers(2, 1, &constantBufferChangesEveryFrame);
            d3dContext->PSSetConstantBuffers(3, 1, &constantBufferChangesEveryPrim);
            ID3D11SamplerState* samplerLinear{ m_samplerLinear.get() };
            d3dContext->PSSetSamplers(0, 1, &samplerLinear);

            for (auto&& object : m_game->RenderObjects())
            {
                // The 3D object render method handles the rendering.
                // For more info, see Primitive rendering below.
                object->Render(d3dContext, m_constantBufferChangesEveryPrim.get());
            }
        }

        // Start of 2D rendering
        ...
    }
}

基本渲染

渲染场景时,遍历需要渲染的所有对象。 对于每个对象(基元)重复以下步骤。

  • 使用模型的 世界转换矩阵 和材料信息更新常量缓冲区(m_constantBufferChangesEveryPrim)。
  • m_constantBufferChangesEveryPrim 包含每个对象的参数。 它包括对象到世界转换矩阵以及用于照明计算的颜色和反射指数等材料属性。
  • 将 Direct3D 上下文设置为应用网格对象数据的输入顶点布局,从而将数据流送到 渲染管道的输入汇编器(IA)阶段。
  • 将 Direct3D 上下文设置为在 IA 阶段中使用 索引缓冲区。 提供基元信息:类型、数据顺序。
  • 提交绘图调用以绘制索引的非实例化基元。 GameObject::Render 方法将特定于给定基元的数据更新到基元 常量缓冲区。 这会在上下文中调用 DrawIndexed,以绘制每个基元的几何图形。 具体而言,此绘图调用会根据常量缓冲区数据排队命令和数据,并传输到图形处理单元(GPU)。 每个绘图调用为每个顶点执行一次顶点着色器,然后为每个基元中每个三角形的每个像素执行一次 像素着色器。 纹理是像素着色器进行渲染时所使用的状态的一部分。

下面是使用多个常量缓冲区的原因。

  • 游戏使用多个常量缓冲区,但它只需要为每个基元更新一次这些缓冲区。 如前所述,常量缓冲区类似于针对每个基元运行的着色器的输入。 某些数据是静态的(m_constantBufferNeverChanges);某些数据在帧(m_constantBufferChangesEveryFrame)上是恒定的,例如相机的位置;和某些数据特定于基元,例如其颜色和纹理(m_constantBufferChangesEveryPrim)。
  • 游戏呈现器将这些输入分成不同的常量缓冲区,以优化 CPU 和 GPU 使用的内存带宽。 此方法还有助于最大程度地减少 GPU 需要跟踪的数据量。 GPU 有一个巨大的命令队列,每次游戏调用 Draw时,该命令和相关的数据都会被加入队列中。 当游戏更新基元常量缓冲区并发出下一 Draw 命令时,图形驱动程序会将下一个命令和关联的数据添加到队列。 如果游戏绘制了 100 个基元,那么队列中可能会有 100 个常量缓冲区数据的副本。 为了最大程度地减少游戏发送到 GPU 的数据量,游戏使用单独的基元常量缓冲区,该缓冲区仅包含每个基元的更新。

GameObject::Render 方法

void GameObject::Render(
    _In_ ID3D11DeviceContext* context,
    _In_ ID3D11Buffer* primitiveConstantBuffer
    )
{
    if (!m_active || (m_mesh == nullptr) || (m_normalMaterial == nullptr))
    {
        return;
    }

    ConstantBufferChangesEveryPrim constantBuffer;

    // Put the model matrix info into a constant buffer, in world matrix.
    XMStoreFloat4x4(
        &constantBuffer.worldMatrix,
        XMMatrixTranspose(ModelMatrix())
        );

    // Check to see which material to use on the object.
    // If a collision (a hit) is detected, GameObject::Render checks the current context, which 
    // indicates whether the target has been hit by an ammo sphere. If the target has been hit, 
    // this method applies a hit material, which reverses the colors of the rings of the target to 
    // indicate a successful hit to the player. Otherwise, it applies the default material 
    // with the same method. In both cases, it sets the material by calling Material::RenderSetup, 
    // which sets the appropriate constants into the constant buffer. Then, it calls 
    // ID3D11DeviceContext::PSSetShaderResources to set the corresponding texture resource for the 
    // pixel shader, and ID3D11DeviceContext::VSSetShader and ID3D11DeviceContext::PSSetShader 
    // to set the vertex shader and pixel shader objects themselves, respectively.

    if (m_hit && m_hitMaterial != nullptr)
    {
        m_hitMaterial->RenderSetup(context, &constantBuffer);
    }
    else
    {
        m_normalMaterial->RenderSetup(context, &constantBuffer);
    }

    // Update the primitive constant buffer with the object model's info.
    context->UpdateSubresource(primitiveConstantBuffer, 0, nullptr, &constantBuffer, 0, 0);

    // Render the mesh.
    // See MeshObject::Render method below.
    m_mesh->Render(context);
}

MeshObject::Render 方法

void MeshObject::Render(_In_ ID3D11DeviceContext* context)
{
    // PNTVertex is a struct. stride provides us the size required for all the mesh data
    // struct PNTVertex
    //{
    //  DirectX::XMFLOAT3 position;
    //  DirectX::XMFLOAT3 normal;
    //  DirectX::XMFLOAT2 textureCoordinate;
    //};
    uint32_t stride{ sizeof(PNTVertex) };
    uint32_t offset{ 0 };

    // Similar to the main render loop.
    // Input-layout objects describe how vertex buffer data is streamed into the IA pipeline stage.
    ID3D11Buffer* vertexBuffer{ m_vertexBuffer.get() };
    context->IASetVertexBuffers(0, 1, &vertexBuffer, &stride, &offset);

    // IASetIndexBuffer binds an index buffer to the input-assembler stage.
    // For more info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetindexbuffer.
    context->IASetIndexBuffer(m_indexBuffer.get(), DXGI_FORMAT_R16_UINT, 0);

    // Binds information about the primitive type, and data order that describes input data for the input assembler stage.
    // For more info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-iasetprimitivetopology.
    context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

    // Draw indexed, non-instanced primitives. A draw API submits work to the rendering pipeline.
    // For more info, see
    // https://learn.microsoft.com/windows/win32/api/d3d11/nf-d3d11-id3d11devicecontext-drawindexed.
    context->DrawIndexed(m_indexCount, 0, 0);
}

DeviceResources::Present 方法

我们调用 DeviceResources::Present 方法,以显示我们在缓冲区中放置的内容。

我们使用术语“交换链”来指代用于向用户显示帧的缓冲区集合。 每当应用程序显示新帧以显示时,交换链中的第一个缓冲区将取代显示的缓冲区。 此过程称为交换或翻转。 有关详细信息,请参阅 交换链

  • IDXGISwapChain1 接口的 Present 方法指示 DXGI 等待,直到发生垂直同步(VSync),将应用程序暂时休眠,直到下一个 VSync。 这确保不会浪费处理不显示到屏幕上的帧的任何处理周期。
  • ID3D11DeviceContext3 接口的 DiscardView 方法丢弃了 渲染目标的内容。 仅当将完全覆盖现有内容时,此操作才有效。 如果使用脏矩形或滚动矩形,则应删除此调用。
  • 使用相同的 DiscardView 方法,丢弃 深度模板的内容。
  • HandleDeviceLost 方法用于管理要删除 设备 的方案。 如果设备被断开连接或驱动程序升级删除,则必须重新创建所有设备资源。 有关详细信息,请参阅 处理 Direct3D 11中设备移除的情形。

小窍门

若要实现流畅的帧速率,必须确保呈现帧的工作量符合 VSync 之间的时间。

// Present the contents of the swap chain to the screen.
void DX::DeviceResources::Present()
{
    // The first argument instructs DXGI to block until VSync, putting the application
    // to sleep until the next VSync. This ensures we don't waste any cycles rendering
    // frames that will never be displayed to the screen.
    HRESULT hr = m_swapChain->Present(1, 0);

    // Discard the contents of the render target.
    // This is a valid operation only when the existing contents will be entirely
    // overwritten. If dirty or scroll rects are used, this call should be removed.
    m_d3dContext->DiscardView(m_d3dRenderTargetView.get());

    // Discard the contents of the depth stencil.
    m_d3dContext->DiscardView(m_d3dDepthStencilView.get());

    // If the device was removed either by a disconnection or a driver upgrade, we 
    // must recreate all device resources.
    if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
    {
        HandleDeviceLost();
    }
    else
    {
        winrt::check_hresult(hr);
    }
}

后续步骤

本主题介绍了图形在显示器上的呈现方式,并提供了一些使用的呈现术语的简短说明(下面)。 详细了解 呈现框架 II 中的呈现:游戏呈现 主题,以及如何在呈现之前准备所需的数据。

术语和概念

简单游戏场景

简单的游戏场景由几个具有多个光源的对象组成。

对象的形状由空间中的一组 X、Y、Z 坐标定义。 游戏世界中的实际呈现位置可以通过将转换矩阵应用于位置 X、Y、Z 坐标来确定。 它还可能有一组纹理坐标(U 和 V),用于指定如何将材料应用到对象上。 这定义了对象的表面属性,并让你能够查看对象是否有粗糙的表面(如网球),还是光滑的光泽表面(如保龄球)。

渲染框架使用场景和对象信息逐帧重新创建场景,使其在显示器上栩栩如生。

渲染管线

呈现管道是将 3D 场景信息转换为屏幕上显示的图像的过程。 在 Direct3D 11 中,此管道是可编程的。 你可以调整阶段以满足你的呈现需求。 使用 HLSL 编程语言可对具有常见着色器核心的阶段进行编程。 它也称为 图形呈现管道,或只是 管道

若要帮助创建此管道,需要熟悉这些详细信息。

有关详细信息,请参阅 了解 Direct3D 11 渲染管线图形管线

HLSL

HLSL 是 DirectX 的高级着色器语言。 使用 HLSL,可以为 Direct3D 管道创建类似 C 的可编程着色器。 有关详细信息,请参阅 HLSL

着色器

着色器可以视为一组指令,用于确定呈现对象图面的方式。 使用 HLSL 编程的着色器被称为 HLSL 着色器。 [HLSL](#hlsl) 着色器的源代码文件具有 .hlsl 文件扩展名。 可以在构建时或运行时编译这些着色器,并在运行时将其设置到适当的管道阶段。 编译的着色器对象具有 .cso 文件扩展名。

可以使用着色器模型 1、着色器模型 2 和着色器模型 3 设计 Direct3D 9 着色器;Direct3D 10 着色器只能在着色器模型 4 上设计。 Direct3D 11 着色器可以在着色器模型 5 上设计。 Direct3D 11.3 和 Direct3D 12 可以在着色器模型 5.1 上设计,Direct3D 12 也可以在着色器模型 6 上设计。

顶点着色器和像素着色器

数据以基元流的形式进入图形管道,并由各种着色器(如顶点着色器和像素着色器)进行处理。

顶点着色器处理顶点,通常执行转换、外观和照明等操作。 像素着色器支持丰富的着色技术,例如每像素照明和后期处理。 它结合了常量、纹理数据、每顶点插值和其他数据,以生成逐像素输出。

着色器阶段

定义用于处理此基元流的这些各种着色器序列称为呈现管道中的着色器阶段。 实际阶段取决于 Direct3D 的版本,但通常包括顶点、几何图形和像素阶段。 还有其他阶段,例如外壳着色器和域着色器用于分割,以及计算着色器。 所有这些阶段都是使用 HLSL完全可编程的。 有关详细信息,请参阅 图形管道

各种着色器文件格式

下面是着色器代码文件扩展名。

  • 扩展名为.hlsl的文件包含 [HLSL](#hlsl) 源代码。
  • 扩展名为 .cso 的文件保存已编译的着色器对象。
  • 扩展名为 .h 的文件是头文件,但在着色器代码上下文中,此头文件定义保存着色器数据的字节数组。
  • 扩展名为 .hlsli 的文件包含常量缓冲区的格式。 在示例游戏中,该文件 着色器>ConstantBuffers.hlsli

注释

通过在运行时加载 .cso 文件或在可执行代码中添加 .h 文件来嵌入着色器。 但是你不会对同一着色器同时使用这两种方法。

更深入地了解 DirectX

Direct3D 11 是一组 API,可帮助我们为图形密集型应用程序(如游戏)创建图形,我们希望有一个很好的图形卡来处理密集型计算。 本部分简要介绍了 Direct3D 11 图形编程概念:资源、子资源、设备和设备上下文。

资源

可以将资源(也称为设备资源)视为有关如何呈现对象的信息,例如纹理、位置或颜色。 资源向管道提供数据,并定义场景中呈现的内容。 可以从游戏媒体加载资源,也可以在运行时动态创建资源。

事实上,资源是内存中的区域,可通过 Direct3D 管道访问。 为了使管道能够高效地访问内存,必须将提供给管道的数据(例如输入几何图形、着色器资源和纹理)存储在资源中。 所有 Direct3D 资源都派生自两种类型的资源:缓冲区或纹理。 每个管道阶段最多可以有 128 个资源处于活动状态。 有关详细信息,请参阅 资源

子资源

术语子资源是指资源的子集。 Direct3D 可以引用整个资源,也可以引用资源的子集。 有关详细信息,请参阅 子资源

深度模板

深度模板资源包含用于保存深度和模板信息的格式和缓冲区。 它是使用纹理资源创建的。 有关如何创建深度模板资源的详细信息,请参阅 配置 Depth-Stencil 功能。 我们通过使用 ID3D11DepthStencilView 接口实现的深度模板视图来访问深度模板资源。

深度信息告诉我们哪些多边形区域位于其他多边形后面,以便我们可以确定哪些区域处于隐藏状态。 模板信息显示我们哪些像素被遮罩。 它可以用于生成特殊效果,因为它决定了像素是否被绘制,并且会将位设置为1或0。

有关详细信息,请参阅 深度模具视图深度缓冲区模具缓冲区

渲染目标

渲染目标是我们可以在渲染过程结束时写入的资源。 它通常使用 ID3D11Device::CreateRenderTargetView 方法创建,方法是使用交换链后缓冲区(这也是资源)作为输入参数。

每个渲染目标还应具有相应的深度模板视图,因为在使用它之前,我们使用OMSetRenderTargets来设置渲染目标时,也需要深度模板视图。 我们通过使用 ID3D11RenderTargetView 接口实现的渲染目标视图来访问渲染目标资源。

装置

可以将设备想象成一种分配和销毁对象、呈现基元并通过图形驱动程序与图形卡通信的方法。

为了更加精确地说明,Direct3D 设备是 Direct3D 的渲染组件。 设备封装并存储渲染状态,执行变换和光照操作,并对图像进行光栅化处理以生成图面。 有关详细信息,请参阅 设备

设备由 ID3D11Device 接口表示。 换句话说,ID3D11Device 接口表示虚拟显示适配器,用于创建设备拥有的资源。

ID3D11Device 有不同版本。 ID3D11Device5 是最新版本,并在 ID3D11Device4的基础上增加了新方法。 有关 Direct3D 如何与基础硬件通信的详细信息,请参阅 Windows 设备驱动程序模型(WDDM)体系结构

每个应用程序必须至少有一台设备;大多数应用程序只创建一个。 通过调用 D3D11CreateDeviceD3D11CreateDeviceAndSwapChain,并使用标志 D3D_DRIVER_TYPE 指定驱动类型,为您的计算机上安装的某个硬件驱动程序创建设备。 每个设备可以使用一个或多个设备上下文,具体取决于所需的功能。 有关详细信息,请参阅 D3D11CreateDevice 函数

设备上下文

设备上下文用于设置 管道 状态,并使用 设备拥有的 资源生成呈现命令。

Direct3D 11 实现两种类型的设备上下文,一种用于即时呈现,另一种用于延迟呈现:这两个上下文都用 ID3D11DeviceContext 接口表示。

ID3D11DeviceContext 接口有不同的版本;ID3D11DeviceContext4ID3D11DeviceContext3的基础上添加了新方法。

在 Windows 10 创意者更新中引入了 ID3D11DeviceContext4,是 ID3D11DeviceContext 接口的最新版本。 面向 Windows 10 创作者更新及更高版本的应用程序应使用此接口,而不是早期版本。 有关详细信息,请参阅 ID3D11DeviceContext4

DX::DeviceResources

DX::D eviceResources 类位于 DeviceResources.cpp/.h 文件中,并控制所有 DirectX 设备资源。

缓冲区

缓冲区资源是由完全类型化的数据组成的集合,这些数据被分组为元素。 可以使用缓冲区来存储各种数据,包括位置向量、普通向量、顶点缓冲区中的纹理坐标、索引在索引缓冲区或设备状态。 缓冲区元素可以包括打包的数据值(如 R8G8B8A8 图面值)、单一 8 位整数或四个 32 位浮点值。

有三种类型的缓冲区可用:顶点缓冲区、索引缓冲区和常量缓冲区。

顶点缓冲区

包含用于定义几何图形的顶点数据。 顶点数据包括位置坐标、颜色数据、纹理坐标数据、普通数据等。

索引缓冲区

包含在顶点缓冲区中的整数偏移量,用于更高效地呈现基元。 索引缓冲区包含一系列顺序排列的 16 位或 32 位索引,每个索引用于标识顶点缓冲区中的一个顶点。

常量缓冲区或着色器常量缓冲区

允许你高效地向管道提供着色器数据。 可以使用常量缓冲区作为针对每个基元运行的着色器的输入,并存储渲染管道的流输出阶段的结果。 从概念上讲,常量缓冲区类似于单元素顶点缓冲区。

缓冲区的设计和实现

可以基于数据类型设计缓冲区,例如,在我们的示例游戏中,为静态数据创建一个缓冲区,另一个缓冲区用于帧上的常量数据,另一个缓冲区用于特定于基元的数据。

所有缓冲区类型都由 ID3D11Buffer 接口封装,可以通过调用 ID3D11Device::CreateBuffer来创建缓冲区资源。 但缓冲区必须先绑定到管道,然后才能访问该缓冲区。 缓冲区可以同时绑定到多个管道阶段进行读取。 缓冲区也可以绑定到单个管道阶段进行写入;但是,无法同时绑定同一缓冲区进行读取和写入。

可以通过这些方式绑定缓冲区。

  • 通过调用 ID3D11DeviceContext 的方法(如 ID3D11DeviceContext::IASetVertexBuffersID3D11DeviceContext::IASetIndexBuffer),将数据传递到输入汇编阶段。
  • 通过调用 ID3D11DeviceContext::SOSetTargets设置流输出阶段的目标。
  • 通过调用着色器方法(例如 ID3D11DeviceContext::VSSetConstantBuffers)到达着色器阶段。

有关详细信息,请参阅 Direct3D 11中的缓冲区简介

DXGI

Microsoft DirectX 图形基础结构(DXGI)是一个子系统,封装 Direct3D 所需的一些低级别任务。 在多线程应用程序中使用 DXGI 时,必须特别小心,以确保不会发生死锁。 有关详细信息,请参阅 多线程和 DXGI

功能级别

功能级别是 Direct3D 11 中引入的概念,用于处理新计算机和现有计算机中的视频卡的多样性。 功能级别是一组定义完善的图形处理单元(GPU)功能。

每个视频卡根据安装的 GPU 实现特定级别的 DirectX 功能。 在早期版本的 Microsoft Direct3D 中,可以找到实现的视频卡的 Direct3D 版本,然后相应地对应用程序进行编程。

使用功能级别,创建设备时,可以尝试为要请求的功能级别创建设备。 如果设备创建成功,则该功能级别存在;如果失败,则硬件不支持该功能级别。 可以尝试在较低功能级别重新创建设备,也可以选择退出应用程序。 例如,12_0 功能级别需要 Direct3D 11.3 或 Direct3D 12,以及着色器模型 5.1。 有关详细信息,请参阅 Direct3D 功能级别:每个功能级别的概述

使用功能级别,可以为 Direct3D 9、Microsoft Direct3D 10 或 Direct3D 11 开发应用程序,然后在 9、10 或 11 硬件上运行它(但有一些例外情况)。 有关详细信息,请参阅 Direct3D 功能级别

立体声渲染

立体声渲染用于增强深度的错觉。 它使用两个图像,一个来自左眼,另一个来自右眼,在显示屏幕上显示场景。

从数学上看,我们应用了一个立体投影矩阵,这相当于在常规单声道投影矩阵上进行向右和向左的轻微水平偏移,以实现此目的。

我们做了两次渲染处理,以实现此示例游戏中的立体渲染。

  • 绑定上右侧呈现目标,应用右侧投影,然后绘制原始图元。
  • 绑定到左侧渲染目标,应用左投影,然后绘制原始对象。

相机和坐标空间

游戏代码旨在根据其自身的坐标系(有时称为世界空间或场景空间)更新游戏世界。 所有对象(包括相机)都定位并面向此空间。 有关详细信息,请参阅 坐标系

顶点着色器执行从模型坐标转换为设备坐标的繁重任务,并使用以下算法(其中 V 是向量,M 是矩阵)。

V(device) = V(model) x M(model-to-world) x M(world-to-view) x M(view-to-device)

  • M(model-to-world) 是模型坐标到世界坐标的转换矩阵,也称为 世界转换矩阵。 这由基元提供。
  • M(world-to-view) 是从世界坐标到视图坐标的转换矩阵,也称为 视图转换矩阵
    • 这由相机的视图矩阵提供。 它由相机的位置以及视线矢量(从相机直接指向场景的 矢量,和垂直向上的 查找 矢量)定义。
    • 在示例游戏中,m_viewMatrix 是视图转换矩阵,并使用 Camera::SetViewParams进行计算。
  • M(view-to-device) 是视图坐标到设备坐标的转换矩阵,也称为 投影转换矩阵
    • 提供这一功能的是相机的投影。 它提供有关最终场景中实际可见的空间量的信息。 视野 (FoV)、纵横比和剪裁平面定义投影转换矩阵。
    • 在示例游戏中,m_projectionMatrix 定义投影坐标的转换,使用 Camera::SetProjParams(对于立体投影,使用两个投影矩阵——一个用于每个眼睛的视图)。

VertexShader.hlsl 中的着色器代码从常量缓冲区加载这些向量和矩阵,并为每个顶点执行此转换。

坐标转换

Direct3D 使用三种转换将三维模型坐标更改为像素坐标(屏幕空间)。 这些转换是世界转换、视图转换和投影转换。 有关详细信息,请参阅 转换概述

世界转换矩阵

世界转换将坐标从模型空间(其中顶点相对于模型的本地原点定义)更改为世界空间,其中顶点相对于场景中所有对象通用的原点进行定义。 从本质上讲,"世界变换"是指将模型放置到世界中,这也是它名字的由来。 有关详细信息,请参阅 世界转换

查看转换矩阵

视图转换将查看器定位到世界空间,将顶点转换为相机空间。 在相机空间中,相机或观察者位于原点,面向正 z 轴方向。 有关详细信息,请访问 视图转换

投影转换矩阵

投影转换将视锥体转换为长方体形状。 视锥是一个三维体,与视区的相机在场景中具有相对定位。 视口是一个用于投影 3D 场景的二维矩形。 有关详细信息,请参阅 视区和剪辑

由于视锥的近端小于远端,因此它具有扩展靠近相机的对象的效果:这是如何将透视应用于场景。 因此,离玩家更近的对象显得更大;更远的对象看起来更小。

从数学上讲,投影转换是一个矩阵,通常既是刻度投影,又是透视投影。 它像相机的镜头一样工作。 有关详细信息,请参阅 投影转换

采样器状态

采样器状态确定如何使用纹理寻址模式、筛选和细节级别对纹理数据采样。 当从纹理中读取纹理像素(或纹素)时,会进行采样。

纹理包含纹素数组。 每个纹素的位置由 (u,v)表示,其中 u 是宽度,v 是高度,并且其位置根据纹理的宽度和高度被映射为介于 0 和 1 之间的值。 在采样纹理时,生成的纹理坐标用于定位纹素。

当纹理坐标低于 0 或高于 1 时,纹理地址模式定义纹理坐标如何寻址纹素位置。 例如,使用 TextureAddressMode.Clamp时,0-1 范围之外的任何坐标将固定到最大值 1,采样前的最小值为 0。

如果纹理对于多边形而言太大或太小,那么会对纹理进行过滤以适应空间。 放大筛选器放大纹理,缩小筛选器可减少纹理以适应较小的区域。 纹理放大在一个或多个地址上重复样本纹素,从而导致图像更加模糊。 纹理缩小更为复杂,因为它需要将多个纹素值组合成单个值。 这可能会导致锯齿现象或不平滑的边缘,具体取决于纹理数据。 进行缩小的最常用方法是使用 mipmap。 mipmap 是多级纹理。 每个级别的大小是上一个级别大小的 1/2,直到缩小到 1x1 的纹理。 在使用缩小处理时,游戏会选择与渲染时所需大小最接近的 mipmap 级别。

BasicLoader 类

BasicLoader 是一个简单的加载程序类,它支持从磁盘上的文件加载着色器、纹理和网格。 它提供同步和异步方法。 在此示例游戏中,BasicLoader.h/.cpp 文件位于 实用工具 文件夹中。

如需更多信息,请查看 基础加载器