将视觉内容添加到 Marble Maze 示例

本文档介绍了 Marble Maze 游戏如何在通用 Windows 平台(UWP)应用环境中使用 Direct3D 和 Direct2D,以便在使用自己的游戏内容时了解模式并对其进行调整。 若要了解视觉游戏组件如何适应 Marble Maze 的整体应用程序结构,请参阅 Marble Maze 应用程序结构

在开发 Marble Maze 的视觉方面时,我们遵循了以下基本步骤:

  1. 创建初始化 Direct3D 和 Direct2D 环境的基本框架。
  2. 使用图像和模型编辑程序设计游戏中显示的 2D 和 3D 资产。
  3. 确保 2D 和 3D 资产正确加载并显示在游戏中。
  4. 集成可增强游戏资产视觉质量的顶点和像素着色器。
  5. 集成游戏逻辑,例如动画和用户输入。

我们还首先专注于添加 3D 资产,然后再添加 2D 资产。 例如,我们在添加菜单系统和计时器之前专注于核心游戏逻辑。

在开发过程中,我们还需要多次重复执行其中一些步骤。 例如,当我们对网格和弹珠模型进行更改时,还必须更改支持这些模型的一些着色器代码。

注释

DirectX Marble Maze 游戏示例中找到与此文档对应的示例代码。

  以下是本文档讨论的一些要点,用于处理 DirectX 和视觉游戏内容时,即初始化 DirectX 图形库、加载场景资源以及更新和呈现场景时:

  • 添加游戏内容通常涉及许多步骤。 这些步骤通常需要迭代。 游戏开发人员通常首先专注于添加 3D 游戏内容,然后添加 2D 内容。
  • 通过尽可能支持最广泛的图形硬件来获得更多客户并为他们提供出色的体验。
  • 明确地分隔设计时和运行时格式。 构建设计时资产,以最大程度地提高灵活性,并在内容上实现快速迭代。 格式化和压缩资产,以在运行时尽可能高效地加载和呈现。
  • 在 UWP 应用中创建 Direct3D 和 Direct2D 设备,就像在经典 Windows 桌面应用中所做的那样。 一个重要区别是交换链与输出窗口的关联方式。
  • 在设计您的游戏时,请确保您选择的网格格式支持您的关键方案。 例如,如果你的游戏需要碰撞,请确保可以从网格中获取碰撞数据。
  • 通过在呈现之前先更新所有场景对象,将游戏逻辑与呈现逻辑分开。
  • 你通常会首先绘制 3D 场景对象,然后绘制在场景前面显示的任何 2D 对象。
  • 将绘图同步到垂直空白,以确保你的游戏不会浪费时间绘制不会在显示器上显示的帧。 垂直空白 是一帧在显示器上完成绘制和下一帧开始之间的时间。

DirectX 图形入门

当我们计划 Marble Maze 通用 Windows 平台 (UWP) 游戏时,我们选择了 C++ 和 Direct3D 11.1,因为它们非常适合用于创建需要对呈现和高性能进行最大控制的 3D 游戏。 DirectX 11.1 支持从 DirectX 9 到 DirectX 11 的硬件,因此可帮助你更高效地接触更多客户,因为无需为每个早期 DirectX 版本重写代码。

Marble Maze 使用 Direct3D 11.1 呈现 3D 游戏资产,即弹珠和迷宫。 Marble Maze 还使用 Direct2D、DirectWrite 和 Windows 图像处理组件(WIC)绘制 2D 游戏资产,例如菜单和计时器。

游戏开发需要规划。 如果你不熟悉 DirectX 图形,建议阅读 DirectX:开始 熟悉创建 UWP DirectX 游戏的基本概念。 阅读本文档并浏览 Marble Maze 源代码时,可以参考以下资源了解有关 DirectX 图形的更深入的信息:

  • Direct3D 11 图形:介绍 Direct3D 11,这是一种功能强大的硬件加速 3D 图形 API,用于在 Windows 平台上呈现 3D 几何图形。
  • Direct2D:介绍 Direct2D,这是一个硬件加速的 2D 图形 API,它为 2D 几何图形、位图和文本提供高性能和高质量呈现。
  • DirectWrite:介绍支持高质量文本呈现的 DirectWrite。
  • Windows 映像组件:介绍 WIC,这是一个可扩展平台,为数字图像提供低级别 API。

功能级别

Direct3D 11 引入了一个名为 功能级别的概念。 功能级别是一组定义完善的 GPU 功能。 使用功能级别将游戏定位于较早版本的 Direct3D 硬件上运行。 Marble Maze 支持功能级别 9.1,因为它不需要更高级别的高级功能。 我们建议你支持尽可能广泛的硬件,并缩放游戏内容,以便拥有高或低端计算机的客户都有很好的体验。 有关功能级别的详细信息,请参阅 下层硬件上的 Direct3D 11

初始化 Direct3D 和 Direct2D

设备表示显示适配器。 在 UWP 应用中创建 Direct3D 和 Direct2D 设备,就像在经典 Windows 桌面应用中所做的那样。 主要区别在于如何将 Direct3D 交换链连接到窗口系统。

DeviceResources 类是管理 Direct3D 和 Direct2D 的基础。 此类处理常规基础结构,而不是特定于游戏的资产。 Marble Maze 定义 MarbleMazeMain 类来处理特定于游戏的资产,该资产具有对 DeviceResources 对象的引用,以授予它对 Direct3D 和 Direct2D 的访问权限。

初始化期间, DeviceResources 构造函数将创建独立于设备的资源和 Direct3D 和 Direct2D 设备。

// Initialize the Direct3D resources required to run. 
DX::DeviceResources::DeviceResources() :
    m_screenViewport(),
    m_d3dFeatureLevel(D3D_FEATURE_LEVEL_9_1),
    m_d3dRenderTargetSize(),
    m_outputSize(),
    m_logicalSize(),
    m_nativeOrientation(DisplayOrientations::None),
    m_currentOrientation(DisplayOrientations::None),
    m_dpi(-1.0f),
    m_deviceNotify(nullptr)
{
    CreateDeviceIndependentResources();
    CreateDeviceResources();
}

DeviceResources 类分隔此功能,以便在环境发生更改时更轻松地响应。 例如,当窗口大小发生更改时,它会调用 CreateWindowSizeDependentResources 方法。

初始化 Direct2D、DirectWrite 和 WIC 工厂

DeviceResources::CreateDeviceIndependentResources 方法为 Direct2D、DirectWrite 和 WIC 创建工厂。 在 DirectX 图形中,工厂是创建图形资源的起点。 Marble Maze 指定 D2D1_FACTORY_TYPE_SINGLE_THREADED,因为它在主线程上执行所有绘图。

// These are the resources required independent of hardware. 
void DX::DeviceResources::CreateDeviceIndependentResources()
{
    // Initialize Direct2D resources.
    D2D1_FACTORY_OPTIONS options;
    ZeroMemory(&options, sizeof(D2D1_FACTORY_OPTIONS));

#if defined(_DEBUG)
    // If the project is in a debug build, enable Direct2D debugging via SDK Layers.
    options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif

    // Initialize the Direct2D Factory.
    DX::ThrowIfFailed(
        D2D1CreateFactory(
            D2D1_FACTORY_TYPE_SINGLE_THREADED,
            __uuidof(ID2D1Factory2),
            &options,
            &m_d2dFactory
            )
        );

    // Initialize the DirectWrite Factory.
    DX::ThrowIfFailed(
        DWriteCreateFactory(
            DWRITE_FACTORY_TYPE_SHARED,
            __uuidof(IDWriteFactory2),
            &m_dwriteFactory
            )
        );

    // Initialize the Windows Imaging Component (WIC) Factory.
    DX::ThrowIfFailed(
        CoCreateInstance(
            CLSID_WICImagingFactory2,
            nullptr,
            CLSCTX_INPROC_SERVER,
            IID_PPV_ARGS(&m_wicFactory)
            )
        );
}

创建 Direct3D 和 Direct2D 设备

DeviceResources::CreateDeviceResources 方法调用 D3D11CreateDevice 来创建表示 Direct3D 显示适配器的设备对象。 由于 Marble Maze 支持功能级别 9.1 及更高版本,DeviceResources::CreateDeviceResources 方法在 featureLevels 数组中指定了级别 9.1 到 11.1。 Direct3D 按顺序排列列表,并为应用提供可用的第一个功能级别。 因此 ,D3D_FEATURE_LEVEL 数组条目从高到低列出,以便应用获得可用的最高功能级别。 DeviceResources::CreateDeviceResources 方法通过查询从 D3D11CreateDevice返回的 Direct3D 11 设备来获取 Direct3D 11.1 设备。

// This flag adds support for surfaces with a different color channel ordering
// than the API default. It is required for compatibility with Direct2D.
UINT creationFlags = D3D11_CREATE_DEVICE_BGRA_SUPPORT;

#if defined(_DEBUG)
    if (DX::SdkLayersAvailable())
    {
        // If the project is in a debug build, enable debugging via SDK Layers 
        // with this flag.
        creationFlags |= D3D11_CREATE_DEVICE_DEBUG;
    }
#endif

// This array defines the set of DirectX hardware feature levels this app will support.
// Note the ordering should be preserved.
// Don't forget to declare your application's minimum required feature level in its
// description.  All applications are assumed to support 9.1 unless otherwise stated.
D3D_FEATURE_LEVEL featureLevels[] =
{
    D3D_FEATURE_LEVEL_11_1,
    D3D_FEATURE_LEVEL_11_0,
    D3D_FEATURE_LEVEL_10_1,
    D3D_FEATURE_LEVEL_10_0,
    D3D_FEATURE_LEVEL_9_3,
    D3D_FEATURE_LEVEL_9_2,
    D3D_FEATURE_LEVEL_9_1
};

// Create the Direct3D 11 API device object and a corresponding context.
ComPtr<ID3D11Device> device;
ComPtr<ID3D11DeviceContext> context;

HRESULT hr = D3D11CreateDevice(
    nullptr,                    // Specify nullptr to use the default adapter.
    D3D_DRIVER_TYPE_HARDWARE,   // Create a device using the hardware graphics driver.
    0,                          // Should be 0 unless the driver is D3D_DRIVER_TYPE_SOFTWARE.
    creationFlags,              // Set debug and Direct2D compatibility flags.
    featureLevels,              // List of feature levels this app can support.
    ARRAYSIZE(featureLevels),   // Size of the list above.
    D3D11_SDK_VERSION,          // Always set this to D3D11_SDK_VERSION for UWP apps.
    &device,                    // Returns the Direct3D device created.
    &m_d3dFeatureLevel,         // Returns feature level of device created.
    &context                    // Returns the device immediate context.
    );

if (FAILED(hr))
{
    // If the initialization fails, fall back to the WARP device.
    // For more information on WARP, see:
    // https://go.microsoft.com/fwlink/?LinkId=286690
    DX::ThrowIfFailed(
        D3D11CreateDevice(
            nullptr,
            D3D_DRIVER_TYPE_WARP, // Create a WARP device instead of a hardware device.
            0,
            creationFlags,
            featureLevels,
            ARRAYSIZE(featureLevels),
            D3D11_SDK_VERSION,
            &device,
            &m_d3dFeatureLevel,
            &context
            )
        );
}

// Store pointers to the Direct3D 11.1 API device and immediate context.
DX::ThrowIfFailed(
    device.As(&m_d3dDevice)
    );

DX::ThrowIfFailed(
    context.As(&m_d3dContext)
    );

DeviceResources::CreateDeviceResources 方法接着创建 Direct2D 设备。 Direct2D 使用 Microsoft DirectX 图形基础结构(DXGI)与 Direct3D 互作。 DXGI 允许在图形运行时之间共享视频内存图面。 Marble Maze 使用来自 Direct2D 工厂的 Direct3D 设备的底层 DXGI 设备来创建 Direct2D 设备。

// Create the Direct2D device object and a corresponding context.
ComPtr<IDXGIDevice3> dxgiDevice;
DX::ThrowIfFailed(
    m_d3dDevice.As(&dxgiDevice)
    );

DX::ThrowIfFailed(
    m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice)
    );

DX::ThrowIfFailed(
    m_d2dDevice->CreateDeviceContext(
        D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
        &m_d2dContext
        )
    );

有关 DXGI 和 Direct2D 与 Direct3D 之间的互作性的详细信息,请参阅 DXGI 概述Direct2D 和 Direct3D 互作性概述

将 Direct3D 与视图关联

DeviceResources::CreateWindowSizeDependentResources 方法创建依赖于给定窗口大小的图形资源,例如交换链和 Direct3D 和 Direct2D 的渲染目标。 DirectX UWP 应用不同于桌面应用的一个重要方式是交换链与输出窗口的关联方式。 交换链负责显示设备在监视器上呈现的缓冲区。 Marble Maze 应用程序结构 描述 UWP 应用的窗口化系统与桌面应用有何不同。 由于 UWP 应用不适用于 HWND 对象,Marble Maze 必须使用 IDXGIFactory2::CreateSwapChainForCoreWindow 方法将设备输出关联到视图。 以下示例演示创建交换链的 DeviceResources::CreateWindowSizeDependentResources 方法的一部分。

// Obtain the final swap chain for this window from the DXGI factory.
DX::ThrowIfFailed(
    dxgiFactory->CreateSwapChainForCoreWindow(
        m_d3dDevice.Get(),
        reinterpret_cast<IUnknown*>(m_window.Get()),
        &swapChainDesc,
        nullptr,
        &m_swapChain
        )
    );

为了最大程度地减少耗电量,在笔记本电脑和平板电脑等电池供电的设备上,DeviceResources::CreateWindowSizeDependentResources 方法调用 IDXGIDevice1::SetMaximumFrameLatency 方法,以确保游戏仅在垂直空白后呈现。 本文档的“展示场景”部分更详细地描述了与垂直空白同步的过程。

// Ensure that DXGI does not queue more than one frame at a time. This both 
// reduces latency and ensures that the application will only render after each
// VSync, minimizing power consumption.
DX::ThrowIfFailed(
    dxgiDevice->SetMaximumFrameLatency(1)
    );

DeviceResources::CreateWindowSizeDependentResources 方法以适用于大多数游戏的方式初始化图形资源。

注释

术语 视图 在 Windows 运行时中的含义与 Direct3D 中的含义不同。 在 Windows 运行时中,视图是指应用的用户界面设置集合,包括显示区域和输入行为,以及用于处理的线程。 可以在创建视图时指定所需的配置和设置。 Marble Maze 应用程序结构中介绍了设置应用视图的过程。 在 Direct3D 中,术语视图具有多个含义。 资源视图定义资源可以访问的子资源。 例如,当纹理对象与着色器资源视图相关联时,着色器稍后可以访问纹理。 资源视图的一个优点是,可以在呈现管道的不同阶段以不同方式解释数据。 有关资源视图的详细信息,请参阅 资源视图。 在视图转换或视图转换矩阵的上下文中使用时,视图是指相机的位置和方向。 视图变换是根据相机的位置和方向调整物体在世界中的位置。 有关视图转换的详细信息,请参阅视图转换(Direct3D 9)。 本主题更详细地介绍了 Marble Maze 如何使用资源和矩阵视图。

 

加载场景资源

Marble Maze 使用 BasicLoader 类(在 BasicLoader.h 中声明)加载纹理和着色器。 Marble Maze 使用 SDKMesh 类来加载迷宫和弹珠的 3D 网格。

为了确保响应式应用,Marble Maze 以异步方式或在后台加载场景资源。 当资产在后台加载时,游戏可以响应窗口事件。 本指南的 背景加载游戏资产 部分中,更详细地介绍了此过程。

加载 2D 叠加层和用户界面

在 Marble Maze 中,覆盖层是显示在屏幕顶部的图像。 叠加层始终显示在场景前面。 在 Marble Maze 中,叠加层包含 Windows 徽标和文本字符串 DirectX Marble Maze 游戏示例。 覆盖的管理由 SampleOverlay 类执行,该类在 SampleOverlay.h 中定义。 尽管我们将覆盖层用作 Direct3D 示例的一部分,但你可以调整此代码以显示在场景前面显示的任何图像。

覆盖的一个重要方面是,由于其内容不会更改, SampleOverlay 类会在初始化期间将其内容绘制或缓存到 ID2D1Bitmap1 对象。 在绘图时,SampleOverlay 类只需要将位图绘制到屏幕上。 这样,无需为每个帧执行昂贵的例程(如文本绘制)。

用户界面(UI)由 2D 组件组成,如菜单和抬头显示器(HUD),显示在场景前。 Marble Maze 定义了以下 UI 元素:

  • 允许用户启动游戏或查看高分的菜单项。
  • 一个计时器,在开始播放前倒计时 3 秒。
  • 跟踪已用游戏时间的计时器。
  • 列出最快完成时间的表。
  • 游戏暂停时显示“暂停”的文本。

Marble Maze 在 UserInterface.h 中定义特定于游戏的 UI 元素。 Marble Maze 将 ElementBase 类定义为所有 UI 元素的基类型。 ElementBase 类定义属性,例如 UI 元素的大小、位置、对齐和可见性。 它还控制元素的更新和呈现方式。

class ElementBase
{
public:
    virtual void Initialize() { }
    virtual void Update(float timeTotal, float timeDelta) { }
    virtual void Render() { }

    void SetAlignment(AlignType horizontal, AlignType vertical);
    virtual void SetContainer(const D2D1_RECT_F& container);
    void SetVisible(bool visible);

    D2D1_RECT_F GetBounds();

    bool IsVisible() const { return m_visible; }

protected:
    ElementBase();

    virtual void CalculateSize() { }

    Alignment       m_alignment;
    D2D1_RECT_F     m_container;
    D2D1_SIZE_F     m_size;
    bool            m_visible;
};

通过为 UI 元素提供通用基类,管理用户界面的 UserInterface 类只需保存 ElementBase 对象的集合,从而简化 UI 管理和提供可重用的用户界面管理器。 Marble Maze 定义派生自 ElementBase 的类型, 实现特定于游戏的行为。 例如,HighScoreTable 定义了高分排行榜的行为。 有关这些类型的详细信息,请参阅源代码。

注释

由于 XAML 使你能够更轻松地创建复杂的用户界面,就像在模拟和策略游戏中找到的用户界面一样,请考虑是否使用 XAML 来定义 UI。 有关如何在 DirectX UWP 游戏中在 XAML 中开发用户界面的信息,请参阅 扩展游戏示例,该示例指的是 DirectX 3D 射击游戏示例。

 

加载着色器

Marble Maze 使用 BasicLoader::LoadShader 方法从文件加载着色器。

着色器是当今游戏中 GPU 编程的基本单元。 几乎所有的 3D 图形处理都是由着色器驱动的,无论是模型转换和场景照明,还是更复杂的几何处理,从角色蒙皮到细分。 有关着色器编程模型的详细信息,请参阅 HLSL

Marble Maze 使用顶点和像素着色器。 顶点着色器始终在一个输入顶点上运行,并生成一个顶点作为输出。 像素着色器采用数值、纹理数据、内插每顶点值和其他数据,以生成像素颜色作为输出。 由于着色器一次转换一个元素,因此提供多个着色器管道的图形硬件可以并行处理元素集。 GPU 可用的并行管道数可以大大大于 CPU 可用的数目。 因此,即使是基本着色器也可以极大地提高吞吐量。

MarbleMazeMain::LoadDeferredResources 方法首先加载一个顶点着色器,然后在加载覆盖层后再加载一个像素着色器。 这些着色器的设计时版本分别在 BasicVertexShader.hlslBasicPixelShader.hlsl 中定义。 Marble Maze 在呈现阶段将这些着色器应用于球和迷宫。

Marble Maze 项目包括着色器文件的 .hlsl(设计时格式)和 .cso (运行时格式) 版本。 在生成时,Visual Studio 使用 fxc.exe 效果编译器将 .hlsl 源文件编译为 .cso 二进制着色器。 有关效果编译器工具的详细信息,请参阅 Effect-Compiler 工具

顶点着色器使用提供的模型、视图和投影矩阵来转换输入几何图形。 输入几何体的位置数据经过转换并输出两次:一次是在屏幕空间中,这是渲染所必需的;另一次是在世界空间中,这样像素着色器就可以执行光照计算。 表面法向量将转换为世界空间,像素着色器也使用该空间进行照明。 纹理坐标不变地直接传递给像素着色器。

sPSInput main(sVSInput input)
{
    sPSInput output;
    float4 temp = float4(input.pos, 1.0f);
    temp = mul(temp, model);
    output.worldPos = temp.xyz / temp.w;
    temp = mul(temp, view);
    temp = mul(temp, projection);
    output.pos = temp;
    output.tex = input.tex;
    output.norm = mul(float4(input.norm, 0.0f), model).xyz;
    return output;
}

像素着色器接收顶点着色器的输出作为输入。 此着色器执行照明计算,以模拟一个软边缘聚光灯在迷宫上方悬浮,这个聚光灯与弹珠的位置对齐。 对于直接指向光的表面,照明最强。 随着表面法线逐渐接近与光线垂直,漫射分量会减少到零;当法线朝离光方向指时,环境光项会减弱。 指向大理石更近的点(因此更接近聚光中心)被点亮得更强烈。 然而,大理石下方的几点光源被调节,以模拟柔和的阴影。 在实际环境中,像白色大理石这样的对象会漫射地将聚光灯反射到场景中的其他对象上。 这是对大理石明亮半部分的表面的近似值。 额外的照明因子与大理石的相对角度和距离有关。 生成的像素颜色是采样纹理与照明计算结果的组合。

float4 main(sPSInput input) : SV_TARGET
{
    float3 lightDirection = float3(0, 0, -1);
    float3 ambientColor = float3(0.43, 0.31, 0.24);
    float3 lightColor = 1 - ambientColor;
    float spotRadius = 50;

    // Basic ambient (Ka) and diffuse (Kd) lighting from above.
    float3 N = normalize(input.norm);
    float NdotL = dot(N, lightDirection);
    float Ka = saturate(NdotL + 1);
    float Kd = saturate(NdotL);

    // Spotlight.
    float3 vec = input.worldPos - marblePosition;
    float dist2D = sqrt(dot(vec.xy, vec.xy));
    Kd = Kd * saturate(spotRadius / dist2D);

    // Shadowing from ball.
    if (input.worldPos.z > marblePosition.z)
        Kd = Kd * saturate(dist2D / (marbleRadius * 1.5));

    // Diffuse reflection of light off ball.
    float dist3D = sqrt(dot(vec, vec));
    float3 V = normalize(vec);
    Kd += saturate(dot(-V, N)) * saturate(dot(V, lightDirection))
        * saturate(marbleRadius / dist3D);

    // Final composite.
    float4 diffuseTexture = Texture.Sample(Sampler, input.tex);
    float3 color = diffuseTexture.rgb * ((ambientColor * Ka) + (lightColor * Kd));
    return float4(color * lightStrength, diffuseTexture.a);
}

警告

编译的像素着色器包含 32 个算术指令和 1 个纹理指令。 此着色器应在台式计算机或高性能平板电脑上表现良好。 但是,某些计算机可能无法处理此着色器,但仍提供交互式帧速率。 考虑目标受众的典型硬件,并设计着色器以满足该硬件的功能。

 

MarbleMazeMain::LoadDeferredResources 方法使用 BasicLoader::LoadShader 方法加载着色器。 以下示例加载顶点着色器。 此着色器的运行时格式为 BasicVertexShader.csom_vertexShader成员变量是 ID3D11VertexShader 对象。

BasicLoader^ loader = ref new BasicLoader(m_deviceResources->GetD3DDevice());

D3D11_INPUT_ELEMENT_DESC layoutDesc [] =
{
    { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "NORMAL", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 24, D3D11_INPUT_PER_VERTEX_DATA, 0 },
    { "TANGENT", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 32, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
m_vertexStride = 44; // must set this to match the size of layoutDesc above

Platform::String^ vertexShaderName = L"BasicVertexShader.cso";
loader->LoadShader(
    vertexShaderName,
    layoutDesc,
    ARRAYSIZE(layoutDesc),
    &m_vertexShader,
    &m_inputLayout
    );

m_inputLayout 成员变量是 ID3D11InputLayout 对象。 输入布局对象封装输入汇编程序 (IA) 阶段的输入状态。 IA 阶段的一项工作是使用系统生成的值(也称为 语义)提高着色器的效率,以便仅处理尚未处理的基元或顶点。

使用 ID3D11Device::CreateInputLayout 方法从输入元素描述数组创建输入布局。 数组包含一个或多个输入元素;每个输入元素描述一个顶点缓冲区中的一个顶点数据元素。 整个输入元素说明集描述了将绑定到 IA 阶段的所有顶点缓冲区中的所有顶点数据元素。

上述代码片段中的 layoutDesc 显示了 Marble Maze 使用的布局说明。 布局说明描述包含四个顶点数据元素的顶点缓冲区。 数组中每个条目的重要部分是语义名称、数据格式和字节偏移量。 例如, POSITION 元素指定对象空间中的顶点位置。 它从字节偏移量 0 开始,并包含三个浮点组件(总共 12 个字节)。 NORMAL 元素指定法向量。 它从字节偏移 12 开始,因为它直接出现在布局中的 POSITION 之后,该布局占用了 12 个字节。 NORMAL 元素包含四个分量的 32位无符号整数。

将输入布局与顶点着色器定义的 sVSInput 结构进行比较,如以下示例所示。 sVSInput 结构定义 POSITIONNORMALTEXCOORD0 元素。 DirectX 运行时将布局中的每个元素映射到由着色器定义的输入结构。

struct sVSInput
{
    float3 pos : POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
};

struct sPSInput
{
    float4 pos : SV_POSITION;
    float3 norm : NORMAL;
    float2 tex : TEXCOORD0;
    float3 worldPos : TEXCOORD1;
};

sPSInput main(sVSInput input)
{
    sPSInput output;
    float4 temp = float4(input.pos, 1.0f);
    temp = mul(temp, model);
    output.worldPos = temp.xyz / temp.w;
    temp = mul(temp, view);
    temp = mul(temp, projection);
    output.pos = temp;
    output.tex = input.tex;
    output.norm = mul(float4(input.norm, 0.0f), model).xyz;
    return output;
}

文档 语义 更详细地描述了每个可用的语义。

注释

在布局中,可以指定不用于使多个着色器共享同一布局的其他组件。 例如,着色器不使用 TANGENT 元素。 如果要试验普通映射等技术,可以使用 TANGENT 元素。 通过使用普通映射(也称为凹凸映射),可以创建对象表面的凸起效果。 有关凹凸映射的详细信息,请参阅凹凸映射(Direct3D 9)。

 

有关输入程序集阶段的详细信息,请参阅 Input-Assembler 阶段Input-Assembler 阶段入门。

在本文档后面的部分,描述了使用顶点着色器和像素着色器来渲染场景的过程

创建常量缓冲区

Direct3D 缓冲区对数据收集进行分组。 常量缓冲区是一种缓冲区,可用于将数据传递给着色器。 Marble Maze 使用常量缓冲区来保存模型(或世界)视图,以及活动场景对象的投影矩阵。

以下示例演示 MarbleMazeMain::LoadDeferredResources 方法如何创建一个常量缓冲区,该缓冲区稍后将保存矩阵数据。 该示例创建一个 D3D11_BUFFER_DESC 结构,该结构使用 D3D11_BIND_CONSTANT_BUFFER 标志将用法指定为常量缓冲区。 然后,此示例将该结构传递给 ID3D11Device::CreateBuffer 方法。 m_constantBuffer变量是 ID3D11Buffer 对象。

// Create the constant buffer for updating model and camera data.
D3D11_BUFFER_DESC constantBufferDesc = {0};

// Multiple of 16 bytes
constantBufferDesc.ByteWidth = ((sizeof(ConstantBuffer) + 15) / 16) * 16;

constantBufferDesc.Usage               = D3D11_USAGE_DEFAULT;
constantBufferDesc.BindFlags           = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDesc.CPUAccessFlags      = 0;
constantBufferDesc.MiscFlags           = 0;

// This will not be used as a structured buffer, so this parameter is ignored.
constantBufferDesc.StructureByteStride = 0;

DX::ThrowIfFailed(
    m_deviceResources->GetD3DDevice()->CreateBuffer(
        &constantBufferDesc,
        nullptr,    // leave the buffer uninitialized
        &m_constantBuffer
        )
    );

MarbleMazeMain::Update 方法稍后会更新 ConstantBuffer 对象,一个用于迷宫,一个用于弹珠。 在渲染每个对象之前,MarbleMazeMain::Render 方法将每个 ConstantBuffer 对象绑定到常量缓冲区。 以下示例演示 MarbleMazeMain.h 中的 ConstantBuffer 结构。

// Describes the constant buffer that draws the meshes.
struct ConstantBuffer
{
    XMFLOAT4X4 model;
    XMFLOAT4X4 view;
    XMFLOAT4X4 projection;

    XMFLOAT3 marblePosition;
    float marbleRadius;
    float lightStrength;
};

若要更好地了解常量缓冲区如何映射到着色器代码,请将 MarbleMazeMain.h 中的 ConstantBuffer 结构与 BasicVertexShader.hlsl 中顶点着色器定义的 ConstantBuffer 常量缓冲区进行比较:

cbuffer ConstantBuffer : register(b0)
{
    matrix model;
    matrix view;
    matrix projection;
    float3 marblePosition;
    float marbleRadius;
    float lightStrength;
};

ConstantBuffer 结构的布局与 cbuffer 对象匹配。 cbuffer 变量指定寄存器 b0,这意味着常量缓冲区数据存储在寄存器 0 中。 MarbleMazeMain::Render 方法在激活常量缓冲区时指定寄存器 0。 本文档稍后将更详细地介绍此过程。

有关常量缓冲区的详细信息,请参阅 Direct3D 11 中的缓冲区简介。 有关 register 关键字的详细信息,请参阅 register

加载网格

Marble Maze 使用 SDK-Mesh 作为运行时格式,因为此格式提供了为示例应用程序加载网格数据的基本方法。 对于生产用途,应使用符合游戏特定要求的网格格式。

MarbleMazeMain::LoadDeferredResources 方法在加载顶点和像素着色器后加载网格数据。 网格是顶点数据的集合,通常包括位置、普通数据、颜色、材料和纹理坐标等信息。 网格通常在 3D 创作软件中创建,并在独立于应用程序代码的文件中维护。 石材和迷宫是游戏使用的两个网格模型示例。

Marble Maze 使用 SDKMesh 类来管理网格。 此类在 SDKMesh.h中声明。 SDKMesh 提供加载、呈现和销毁网格数据的方法。

重要

Marble Maze 使用 SDK-Mesh 格式,并提供 SDKMesh 类,仅作示例之用。 尽管 SDK-Mesh 格式可用于学习和创建原型,但它是一种非常基本的格式,可能无法满足大多数游戏开发的要求。 建议使用符合游戏特定要求的网格格式。

 

以下示例演示 MarbleMazeMain::LoadDeferredResources 方法如何使用 SDKMesh::Create 方法加载迷宫和球的网格数据。

// Load the meshes.
DX::ThrowIfFailed(
    m_mazeMesh.Create(
        m_deviceResources->GetD3DDevice(),
        L"Media\\Models\\maze1.sdkmesh",
        false
        )
    );

DX::ThrowIfFailed(
    m_marbleMesh.Create(
        m_deviceResources->GetD3DDevice(),
        L"Media\\Models\\marble2.sdkmesh",
        false
        )
    );

加载碰撞数据

虽然本部分并不侧重于 Marble Maze 如何实现弹珠和迷宫之间的物理模拟,但请注意,加载网格时会读取供物理系统使用的网格几何体。

// Extract mesh geometry for the physics system.
DX::ThrowIfFailed(
    ExtractTrianglesFromMesh(
        m_mazeMesh,
        "Mesh_walls",
        m_collision.m_wallTriList
        )
    );

DX::ThrowIfFailed(
    ExtractTrianglesFromMesh(
        m_mazeMesh,
        "Mesh_Floor",
        m_collision.m_groundTriList
        )
    );

DX::ThrowIfFailed(
    ExtractTrianglesFromMesh(
        m_mazeMesh,
        "Mesh_floorSides",
        m_collision.m_floorTriList
        )
    );

m_physics.SetCollision(&m_collision);
float radius = m_marbleMesh.GetMeshBoundingBoxExtents(0).x / 2;
m_physics.SetRadius(radius);

加载碰撞数据的方式在很大程度上取决于使用的运行时格式。 有关如何从 SDK-Mesh 文件中加载冲突几何图形的详细信息,请参阅源代码中的 MarbleMazeMain::ExtractTrianglesFromMesh 方法。

更新游戏状态

Marble Maze 通过在呈现之前先更新所有场景对象,将游戏逻辑与呈现逻辑分开。

Marble Maze 应用程序结构 描述主要游戏循环。 作为游戏循环的一部分,更新场景是在处理 Windows 事件和输入之后、呈现场景之前进行的。 MarbleMazeMain::Update 方法处理 UI 和游戏的更新。

更新用户界面

MarbleMazeMain::Update 方法调用 UserInterface::Update 方法以更新 UI 的状态。

UserInterface::GetInstance().Update(
    static_cast<float>(m_timer.GetTotalSeconds()), 
    static_cast<float>(m_timer.GetElapsedSeconds()));

UserInterface::Update 方法更新 UI 集合中的每个元素。

void UserInterface::Update(float timeTotal, float timeDelta)
{
    for (auto iter = m_elements.begin(); iter != m_elements.end(); ++iter)
    {
        (*iter)->Update(timeTotal, timeDelta);
    }
}

派生自 ElementBase 的类(在 UserInterface.h 中定义)实现 Update 方法来执行特定行为。 例如,StopwatchTimer::Update 方法按提供的数量更新已用时间,并更新稍后显示的文本。

void StopwatchTimer::Update(float timeTotal, float timeDelta)
{
    if (m_active)
    {
        m_elapsedTime += timeDelta;

        WCHAR buffer[16];
        GetFormattedTime(buffer);
        SetText(buffer);
    }

    TextElement::Update(timeTotal, timeDelta);
}

更新场景

MarbleMazeMain::Update 方法根据状态机的当前状态(存储在 m_gameState中的 GameState)更新游戏。 当游戏处于活动状态(GameState::InGameActive)时,Marble Maze 更新相机以跟踪弹珠,更新常量缓冲的视图矩阵部分,并更新物理模拟。

以下示例演示 MarbleMazeMain::Update 方法如何更新相机的位置。 Marble Maze 使用 m_resetCamera 变量来指示相机必须重置,确保其正好位于弹珠的上方。 当游戏开始或弹珠掉入迷宫时,镜头将重置。 当主菜单或高分显示屏幕处于活动状态时,相机将设置为固定位置。 否则,Marble Maze 使用 timeDelta 参数在摄像机的当前位置和目标位置之间插值计算其位置。 目标位置在弹珠的前方并稍高一些。 利用帧时间的流逝,使相机可以逐渐跟踪追随弹珠。

static float eyeDistance = 200.0f;
static XMFLOAT3A eyePosition = XMFLOAT3A(0, 0, 0);

// Gradually move the camera above the marble.
XMFLOAT3A targetEyePosition;
XMStoreFloat3A(
    &targetEyePosition, 
    XMLoadFloat3A(&marblePosition) - (XMLoadFloat3A(&g) * eyeDistance));

if (m_resetCamera)
{
    eyePosition = targetEyePosition;
    m_resetCamera = false;
}
else
{
    XMStoreFloat3A(
        &eyePosition, 
        XMLoadFloat3A(&eyePosition) 
            + ((XMLoadFloat3A(&targetEyePosition) - XMLoadFloat3A(&eyePosition)) 
                * min(1, static_cast<float>(m_timer.GetElapsedSeconds()) * 8)
            )
    );
}

// Look at the marble. 
if ((m_gameState == GameState::MainMenu) || (m_gameState == GameState::HighScoreDisplay))
{
    // Override camera position for menus.
    XMStoreFloat3A(
        &eyePosition, 
        XMLoadFloat3A(&marblePosition) + XMVectorSet(75.0f, -150.0f, -75.0f, 0.0f));

    m_camera->SetViewParameters(
        eyePosition, 
        marblePosition, 
        XMFLOAT3(0.0f, 0.0f, -1.0f));
}
else
{
    m_camera->SetViewParameters(eyePosition, marblePosition, XMFLOAT3(0.0f, 1.0f, 0.0f));
}

以下示例演示 MarbleMazeMain::Update 方法如何更新弹珠和迷宫的常量缓冲区。 迷宫的模型或世界矩阵始终是标识矩阵。 除了主对角线的元素全为1之外,单位矩阵是一个由其他元素全为零的方阵。 弹珠的模型矩阵基于其位置矩阵乘以其旋转矩阵。

// Update the model matrices based on the simulation.
XMStoreFloat4x4(&m_mazeConstantBufferData.model, XMMatrixIdentity());

XMStoreFloat4x4(
    &m_marbleConstantBufferData.model, 
    XMMatrixTranspose(
        XMMatrixMultiply(
            marbleRotationMatrix, 
            XMMatrixTranslationFromVector(XMLoadFloat3A(&marblePosition))
        )
    )
);

// Update the view matrix based on the camera.
XMFLOAT4X4 view;
m_camera->GetViewMatrix(&view);
m_mazeConstantBufferData.view = view;
m_marbleConstantBufferData.view = view;

有关 MarbleMazeMain::Update 方法如何读取用户输入和模拟弹珠运动的信息,请参阅 为 Marble Maze 示例添加输入和交互性

渲染场景

呈现场景时,通常包括这些步骤。

  1. 设置当前渲染目标深度模板缓冲区。
  2. 清除渲染和模板视图。
  3. 准备好用于绘图的顶点和像素着色器。
  4. 在场景中呈现 3D 对象。
  5. 呈现要在场景前显示的任何 2D 对象。
  6. 将呈现的图像显示在监视器上。

MarbleMazeMain::Render 方法首先绑定渲染目标和深度模板视图,清除这些视图,然后绘制场景,最后绘制覆盖层。

准备渲染目标

在呈现场景之前,必须设置当前呈现目标深度-模板缓冲区。 如果无法保证场景覆盖屏幕上的每个像素,还应清除渲染和模板视图。 Marble Maze 清除每一帧上的渲染视图和模板视图,以确保上一帧中没有可见的伪影。

以下示例演示了 MarbleMazeMain::Render 方法如何调用 ID3D11DeviceContext::OMSetRenderTargets 方法将渲染目标和深度模板缓冲区设置为当前的对象。

auto context = m_deviceResources->GetD3DDeviceContext();

// Reset the viewport to target the whole screen.
auto viewport = m_deviceResources->GetScreenViewport();
context->RSSetViewports(1, &viewport);

// Reset render targets to the screen.
ID3D11RenderTargetView *const targets[1] = 
    { m_deviceResources->GetBackBufferRenderTargetView() };

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

// Clear the back buffer and depth stencil view.
context->ClearRenderTargetView(
    m_deviceResources->GetBackBufferRenderTargetView(), 
    DirectX::Colors::Black);

context->ClearDepthStencilView(
    m_deviceResources->GetDepthStencilView(), 
    D3D11_CLEAR_DEPTH | D3D11_CLEAR_STENCIL, 
    1.0f, 
    0);

ID3D11RenderTargetViewID3D11DepthStencilView 接口支持 Direct3D 10 及更高版本提供的纹理视图机制。 有关纹理视图的详细信息,请参阅纹理视图(Direct3D 10)。 OMSetRenderTargets 方法准备 Direct3D 管道的输出合并阶段。 有关输出合并阶段的详细信息,请参阅 Output-Merger 阶段

准备顶点和像素着色器

在呈现场景对象之前,请执行以下步骤来准备用于绘图的顶点和像素着色器:

  1. 将着色器输入布局设置为当前布局。
  2. 将顶点着色器和像素着色器设置为当前着色器。
  3. 请用您必须传递给着色器的数据更新常量缓冲区。

重要

Marble Maze 对所有 3D 对象使用一对顶点和像素着色器。 如果你的游戏使用多个着色器对,则每次绘制使用不同着色器的对象时,都必须执行这些步骤。 为了减少与更改着色器状态相关的开销,我们建议对使用同一着色器的所有对象进行呈现调用分组。

 

本文档 加载着色器 部分介绍了在创建顶点着色器时如何创建输入布局。 以下示例演示 MarbleMazeMain::Render 方法如何使用 ID3D11DeviceContext::IASetInputLayout 方法将此布局设置为当前布局。

m_deviceResources->GetD3DDeviceContext()->IASetInputLayout(m_inputLayout.Get());

以下示例演示 MarbleMazeMain::Render 方法如何使用 ID3D11DeviceContext::VSSetShaderID3D11DeviceContext::P SSetShader 方法将顶点和像素着色器分别设置为当前着色器。

// Set the vertex shader stage state.
m_deviceResources->GetD3DDeviceContext()->VSSetShader(
    m_vertexShader.Get(),   // use this vertex shader
    nullptr,                // don't use shader linkage
    0);                     // don't use shader linkage

m_deviceResources->GetD3DDeviceContext()->PSSetShader(
    m_pixelShader.Get(),    // use this pixel shader
    nullptr,                // don't use shader linkage
    0);                     // don't use shader linkage

m_deviceResources->GetD3DDeviceContext()->PSSetSamplers(
    0,                          // starting at the first sampler slot
    1,                          // set one sampler binding
    m_sampler.GetAddressOf());  // to use this sampler

MarbleMazeMain::Render 设置着色器及其输入布局后,它使用 ID3D11DeviceContext::UpdateSubresource 方法使用迷宫的模型、视图和投影矩阵更新常量缓冲区。 UpdateSubresource 方法将矩阵数据从 CPU 内存复制到 GPU 内存。 回想一下,MarbleMazeMain::Update 方法中更新了 ConstantBuffer 结构的模型和视图组件。 然后,MarbleMazeMain::Render 方法调用 ID3D11DeviceContext::VSSetConstantBuffersID3D11DeviceContext::PSSetConstantBuffers 方法将此常量缓冲区设置为当前使用的缓冲区。

// Update the constant buffer with the new data.
m_deviceResources->GetD3DDeviceContext()->UpdateSubresource(
    m_constantBuffer.Get(),
    0,
    nullptr,
    &m_mazeConstantBufferData,
    0,
    0);

m_deviceResources->GetD3DDeviceContext()->VSSetConstantBuffers(
    0,                                  // starting at the first constant buffer slot
    1,                                  // set one constant buffer binding
    m_constantBuffer.GetAddressOf());   // to use this buffer

m_deviceResources->GetD3DDeviceContext()->PSSetConstantBuffers(
    0,                                  // starting at the first constant buffer slot
    1,                                  // set one constant buffer binding
    m_constantBuffer.GetAddressOf());   // to use this buffer

MarbleMazeMain::Render 方法执行类似的步骤来准备弹珠进行渲染。

渲染迷宫和小弹珠

激活当前着色器后,可以绘制场景对象。 MarbleMazeMain::Render 方法调用 SDKMesh::Render 方法来渲染迷宫网格。

m_mazeMesh.Render(
    m_deviceResources->GetD3DDeviceContext(), 
    0, 
    INVALID_SAMPLER_SLOT, 
    INVALID_SAMPLER_SLOT);

MarbleMazeMain::Render 方法执行相似步骤以渲染弹珠。

如本文档前面所述,提供了 SDKMesh 类以供演示目的,但我们不建议将其用于生产质量的游戏。 但是,请注意,SDKMesh::RenderMesh 方法是由 SDKMesh::Render调用的,它使用 ID3D11DeviceContext::IASetVertexBuffersID3D11DeviceContext::IASetIndexBuffer 方法来设置定义网格的当前顶点和索引缓冲区,并使用 ID3D11DeviceContext::DrawIndexed 方法来绘制这些缓冲区。 有关如何使用顶点和索引缓冲区的详细信息,请参阅 Direct3D 11 中的缓冲区简介

绘制用户界面和覆盖层

绘制 3D 场景对象后,Marble Maze 绘制显示在场景前面的 2D UI 元素。

MarbleMazeMain::Render 方法通过绘制用户界面和覆盖层来结束。

// Draw the user interface and the overlay.
UserInterface::GetInstance().Render(m_deviceResources->GetOrientationTransform2D());

m_deviceResources->GetD3DDeviceContext()->BeginEventInt(L"Render Overlay", 0);
m_sampleOverlay->Render();
m_deviceResources->GetD3DDeviceContext()->EndEvent();

UserInterface::Render 方法使用 ID2D1DeviceContext 对象来绘制 UI 元素。 此方法设置绘图状态,绘制所有活动 UI 元素,然后还原以前的绘图状态。

void UserInterface::Render(D2D1::Matrix3x2F orientation2D)
{
    m_d2dContext->SaveDrawingState(m_stateBlock.Get());
    m_d2dContext->BeginDraw();
    m_d2dContext->SetTransform(orientation2D);

    m_d2dContext->SetTextAntialiasMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);

    for (auto iter = m_elements.begin(); iter != m_elements.end(); ++iter)
    {
        if ((*iter)->IsVisible())
            (*iter)->Render();
    }

    // We ignore D2DERR_RECREATE_TARGET here. This error indicates that the device
    // is lost. It will be handled during the next call to Present.
    HRESULT hr = m_d2dContext->EndDraw();
    if (hr != D2DERR_RECREATE_TARGET)
    {
        DX::ThrowIfFailed(hr);
    }

    m_d2dContext->RestoreDrawingState(m_stateBlock.Get());
}

SampleOverlay::Render 方法使用类似的技术绘制叠加位图。

演示场景

绘制所有 2D 和 3D 场景对象后,Marble Maze 会将呈现的图像呈现给监视器。 它将绘图同步到垂直空白,以确保不会耗费时间来绘制永远不会实际显示在显示器上的帧。 Marble Maze 在呈现场景的同时,也会处理设备的更改。

MarbleMazeMain::Render 方法返回后,游戏循环调用 DX::DeviceResources::Present 方法将渲染的图像发送到监视器或显示器。 DX::DeviceResources::Present 方法调用 IDXGISwapChain::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);

在此示例中, m_swapChainIDXGISwapChain1 对象。 本文档的 “初始化 Direct3D”和“Direct2D ”部分中介绍了此对象的初始化。

IDXGISwapChain::Present的第一个参数,SyncInterval,指定在显示帧之前要等待的垂直空白数。 Marble Maze 指定 1,以便等待下一个垂直空白。

IDXGISwapChain::Present 方法返回一个错误代码,该代码指示设备被移除或出现其他故障。 在这种情况下,Marble Maze 重新初始化设备。

// 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)
{
    HandleDeviceLost();
}
else
{
    DX::ThrowIfFailed(hr);
}

后续步骤

阅读 向 Marble Maze 示例添加输入和交互性,以了解在使用输入设备时需要牢记的一些关键做法。 本文档讨论 Marble Maze 如何支持触摸、加速计、游戏控制器和鼠标输入。