向 Marble Maze 示例添加可视内容

Applies to Windows only

本文介绍 Marble Maze 游戏如何在 Windows 应用商店应用环境中使用 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 内容。
  • 通过尽可能支持更大范围的图形硬件,获得更多客户并为他们提供出色的体验。
  • 将设计时和运行时格式明确分开。构建设计时资产结构,以最大限度提高灵活性并在内容上实现快速迭代。格式化并压缩资产,以在运行时尽可能高效地加载和渲染。
  • 在 Windows 应用商店应用中创建 Direct3D 和 Direct2D 设备,与在经典的 Windows 桌面应用中非常相似。一个重要的区别是交换链与输出窗口的关联方式。
  • 设计游戏时,请确保所选择的网格格式支持你的重要方案。例如,如果游戏需要碰撞,请确保你可从网格获取碰撞数据。
  • 通过在渲染所有场景对象之前首先更新它们,将游戏逻辑与渲染逻辑分开。
  • 通常先绘制 3D 场景对象,然后绘制出现在场景前面的任何 2D 对象。
  • 将图形与垂直空白同步,确保游戏不会花时间绘制从不会在显示器上实际显示的帧。

DirectX 图形入门

我们在计划 Marble Maze Windows 应用商店游戏时,选择了 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 游戏资产,例如菜单和计时器。 最终,Marble Maze 使用 XAML 来提供应用栏并允许你添加 XAML 控件。

游戏开发需要规划。如果不熟悉 DirectX 图形,我们建议你阅读“创建 DirectX 游戏”,熟悉创建 Windows 应用商店 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

一个设备代表显示适配器。在 Windows 应用商店应用中创建 Direct3D 和 Direct2D 设备,与在经典的 Windows 桌面应用中非常相似。主要区别在于将 Direct3D 交换链连接到视窗系统的方式上。

Visual Studio DirectX 应用 (XAML) 模板从特定于游戏的功能中剔除了一些一般性的操作系统和 3D 渲染功能。DeviceResources 类是管理 Direct3D 和 Direct2D 的基础。该类处理一般结构,而不是特定于游戏的资产。Marble Maze 定义 MarbleMaze 类来处理特定于游戏的资产,该类具有对 DeviceResources 对象的引用以使其可以访问 Direct3D 和 Direct2D。

初始化期间,DeviceResources::Initialize 方法创建与设备独立的资源以及 Direct3D 和 Direct2D 设备。


// Initialize the Direct3D resources required to run. 
void DeviceResources::DeviceResources(CoreWindow^ window, float dpi)
{
    m_window = window;

    CreateDeviceIndependentResources();
    CreateDeviceResources();
    CreateWindowSizeDependentResources();
    SetDpi(dpi);
}


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 DeviceResources::CreateDeviceIndependentResources()
{
    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

    DX::ThrowIfFailed(
        D2D1CreateFactory(
            D2D1_FACTORY_TYPE_SINGLE_THREADED,
            __uuidof(ID2D1Factory1),
            &options,
            &m_d2dFactory
            )
        );

    DX::ThrowIfFailed(
        DWriteCreateFactory(
            DWRITE_FACTORY_TYPE_SHARED,
            __uuidof(IDWriteFactory),
            &m_dwriteFactory
            )
        );

    DX::ThrowIfFailed(
        CoCreateInstance(
            CLSID_WICImagingFactory,
            nullptr,
            CLSCTX_INPROC_SERVER,
            IID_PPV_ARGS(&m_wicFactory)
            )
        );
}

创建 Direct3D 和 Direct2D 设备

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


// 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 DX11 API device object, and get a corresponding context.
ComPtr<ID3D11Device> device;
ComPtr<ID3D11DeviceContext> context;
DX::ThrowIfFailed(
    D3D11CreateDevice(
        nullptr,                    // specify null to use the default adapter
        D3D_DRIVER_TYPE_HARDWARE,
        0,                          // leave as 0 unless software device
        creationFlags,              // optionally set debug and Direct2D compatibility flags
        featureLevels,              // list of feature levels this app can support
        ARRAYSIZE(featureLevels),   // number of entries in above list
        D3D11_SDK_VERSION,          // always set this to D3D11_SDK_VERSION for modern
        &device,                    // returns the Direct3D device created
        &m_featureLevel,            // returns feature level of device created
        &context                    // returns the device immediate context
        )
    );    

// Get the Direct3D 11.1 device by querying the Direct3D 11 device.
DX::ThrowIfFailed(
    device.As(&m_d3dDevice)
    );

然后 DeviceResources::CreateDeviceResources 方法创建 Direct2D 设备。Direct2D 使用 Microsoft DirectX 图形基础结构 (DXGI) 来与 Direct3D 互操作。DXGI 支持在图形运行时之间共享视频内存表面。Marble Maze 使用来自 Direct3D 设备的基础 DXGI 设备,通过 Direct2D 工厂创建 Direct2D 设备。


// Obtain the underlying DXGI device of the Direct3D 11.1 device.
DX::ThrowIfFailed(
    m_d3dDevice.As(&dxgiDevice)
    );

// Obtain the Direct2D device for 2-D rendering.
DX::ThrowIfFailed(
    m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice)
    );

// And get its corresponding device context object.
DX::ThrowIfFailed(
    m_d2dDevice->CreateDeviceContext(
        D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
        &m_d2dContext
        )
    );


// Obtain the underlying DXGI device of the Direct3D 11.1 device.
DX::ThrowIfFailed(
    m_d3dDevice.As(&dxgiDevice)
    );

// Obtain the Direct2D device for 2-D rendering.
DX::ThrowIfFailed(
    m_d2dFactory->CreateDevice(dxgiDevice.Get(), &m_d2dDevice)
    );

// And get its corresponding device context object.
DX::ThrowIfFailed(
    m_d2dDevice->CreateDeviceContext(
        D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
        &m_d2dContext
        )
    );


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

将 Direct3D 与视图关联

DeviceResources::CreateWindowSizeDependentResources 方法根据给定的窗口大小创建图形资源,例如交换链及 Direct3D 和 Direct2D 渲染目标。DirectX Windows 应用商店应用与桌面应用的一个重要区别在于交换链与输出窗口的关联方式上。交换链负责显示缓冲区,设备在监视器上要渲染到该缓冲区。文档“Marble Maze 应用结构”介绍了 Windows 应用商店的视窗系统与桌面应用的区别。因为 Windows 应用商店应用不使用 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),
        &swapChainDesc,
        nullptr,    // allow on all displays
        &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 方法以一种适合大部分游戏的方式初始化图形资源。有关展示如何初始化 DirectX Windows 应用商店应用的另一个示例,请参阅如何设置 DirectX Windows 应用商店应用以显示视图

注意  术语视图在 Windows 运行时中的含义与 Direct3D 中的不同。在 Windows 运行时中,视图指的是应用的用户界面设置集合,包括显示区域和输入行为及其用于处理的线程。你在创建视图时指定需要的配置和设置。设置应用视图的过程将在 Marble Maze 应用结构中介绍。 在 Direct3D 中,术语“视图”有多种含义。首先,资源视图定义一种资源可访问的子资源。例如,将一个纹理对象与一个着色器资源视图关联后,该着色器可访问该纹理。资源视图的一个优点是,你可在渲染管道中的不同阶段以不同方式解释数据。有关资源视图的详细信息,请参阅纹理视图 (Direct3D 10)。在视图转换或视图转换矩阵的上下文中使用时,视图指照相机的位置和方向。视图转换会在围绕照相机的位置和方向的世界中重新定位各个对象。有关视图转换的详细信息,请参阅视图转换 (Direct3D 9)。本主题将详细介绍 Marble Maze 如何使用资源和矩阵视图。

加载场景资源

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

为了确保应用能迅速响应,Marble Maze 异步或在后台加载场景资源。资产加载到后台后,游戏即可响应窗口事件。这个过程将在本指南的在后台加载游戏资产中详细介绍。

加载 2D 覆盖图和用户界面

在 Marble Maze 中,覆盖图是显示在屏幕顶层的图像。覆盖图始终显示在场景前面。在 Marble Maze 中,覆盖图包含 Windows 徽标和文本字符串“DirectX Marble Maze game sample”。覆盖图的管理由 SampleOverlay 类执行,该类在 SampleOverlay.h 中定义。此代码类似于 Direct3D Windows 应用商店示例中的代码。我们在 Direct3D 示例中使用了覆盖图,但你可修改此代码以显示任何出现在场景前面的图像。

覆盖图的一个重要方面是,因为它的内容不会更改,所以 SampleOverlay 类在初始化期间将它的内容绘制或缓存到一个 ID2D1Bitmap1 对象。在绘制时,SampleOverlay 类仅需要向屏幕绘制位图。这样,无需为每一帧都执行文本绘制等需要大量资源的例行任务。

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

  • 使用户能够启动游戏或查看高分的菜单项。
  • 在游戏开始前计时 3 秒的计时器。
  • 跟踪已用游戏时间的计时器。
  • 列出最快完成时间的表格。
  • 在游戏暂停时显示 “Paused”的文本。

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。有关如何使用 XAML 在 DirectX Windows 应用商店游戏中开发用户界面的信息,请参阅扩展游戏示例 (Windows)。本文参考了 DirectX 3D 设计游戏示例。

加载着色器

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

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

Marble Maze 使用顶点和像素着色器。顶点着色器始终在一个输入顶点上操作并生成一个顶点作为输出。像素着色器接受数字值、纹理数据、插值的顶点值,以及其他数据,生成一种像素颜色作为输出。因为着色器一次转换一个元素,所以提供多个着色器管道的图形硬件可并行处理多组元素。可供 GPU 使用的并行管道数量可能比可供 CPU 使用的数量要多得多。因此,即使基本着色器也能大大改善吞吐量。

MarbleMaze::LoadDeferredResources 方法在加载覆盖图后加载一个顶点着色器和一个像素着色器。这些着色器的设计时版本分别在 BasicVertexShader.hlsl 和 BasicPixelShader.hlsl 中定义。Marble Maze 在渲染阶段将这些着色器同时应用到球和迷宫。

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

顶点着色器使用提供的模型、视图和投影矩阵来转换输入几何图形。来自输入几何图形的位置数据被转换并输出两次,一次在屏幕空间中(这是渲染所必需的),另一次在世界空间中,让像素着色器能够执行照明计算。表面法线矢量被转换到世界空间,这也由像素着色器用于照明。纹理坐标原封不动地传递到像素着色器。


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


像素着色器接收顶点着色器的输出作为输入。这个着色器执行照明计算来模拟一种虚边聚光效果,该聚光效果悬浮在迷宫上方且与弹珠的位置对齐。直接朝向光源的表面的照明最强。随着法线变为与光线垂直,散射组件逐渐缩小到 0;随着法线点远离光源,环境光逐渐消失。接近弹珠(因此接近聚光效果的中心)的点的照明更强。但是,弹珠下方的点的照明要柔和一些,以模拟虚化的阴影。在真实环境中,一个像白色弹珠这样的物体会将聚光灯的光漫反射到场景中的其他对象上。这非常接近弹珠的明亮部分的表面。其他的照明因素包括相对角和离弹珠的距离。最终的像素颜色包括采样的纹理和照明计算的结果。


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 个纹理指令。这个着色器应能在桌面计算机和高端平板电脑上出色地运行。但是,低端计算机可能无法处理此着色器并仍然提供一种交互式帧速率。考虑你的目标受众的典型硬件,设计你的着色器来满足该硬件的功能。

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


\loader->LoadShader(
    L"BasicVertexShader.cso",
    layoutDesc,
    ARRAYSIZE(layoutDesc),
    &m_vertexShader,
    &m_inputLayout
    );


m_inputLayout 成员变量是一个 ID3D11InputLayout 对象。输入布局对象封装了输入装配器 (IA) 阶段的输入状态。IA 阶段的一个工作是提高着色器的效率,使用系统生成的值(也称为语义)仅处理尚未处理的基元或顶点。使用 ID3D11Device::CreateInputLayout 方法从输入元素描述数组创建一个输入布局。该数组包含一个或多个输入元素;每个输入元素描述来自一个顶点缓冲区的一个矢量数据元素。整个输入元素描述集合描述了来自将绑定到 IA 阶段的所有顶点缓冲区的顶点数据元素。下面的示例展示了 Marble Maze 使用的布局描述。布局描述说明了一个包含 4 个顶点数据元素的顶点缓冲区。数组中每个条目的重要部分是语义名称、数据格式和字节偏移。例如,POSITION 元素指定对象空间中的顶点位置。它从字节偏移 0 开始,包含 3 个浮点组件(共 12 字节)。NORMAL 元素指定法线顶点。它从字节偏移 12 开始,因为它紧挨布局中的 POSITION 后出现,这需要 12 字节。NORMAL 元素包含一个 4 组件、32 位的无符号整数。


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

将输入布局与顶点着色器定义的 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)

有关输入装配阶段状态的详细信息,请参阅输入装配器阶段输入装配器阶段入门

使用顶点和像素着色器渲染场景的过程将在本文后面的渲染场景一节介绍。

创建常量缓冲区

Direct3D 缓冲区将一组对象分组到一起。常量缓冲区是一种可用于将数据传递给着色器的缓冲区。Marble Maze 使用常量缓冲区来持有活动的场景对象的模型(或世界)视图,以及投影指标。

下面的示例展示了 MarbleMaze::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};
constantBufferDesc.ByteWidth           = ((sizeof(ConstantBuffer) + 15) / 16) * 16; // multiple of 16 bytes
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_d3dDevice->CreateBuffer(
        &constantBufferDesc,
        nullptr,             // leave the buffer uninitialized
        &m_constantBuffer
        )
    );

MarbleMaze::Update 方法稍后更新 ConstantBuffer 对象,一个用于迷宫,一个用户弹珠。然后 MarbleMaze::Render 方法在渲染每个对象之前,将每个 ConstantBuffer 对象绑定到常量缓冲区。下面的示例展示了 ConstantBuffer 结构,它位于 MarbleMaze.h 中。


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

    float3 marblePosition;
    float marbleRadius;
    float lightStrength;
};


要更好地理解常量缓冲区如何映射到着色器代码,可比较 ConstantBuffer 结构与 BasicVertexShader.hlsl 中的顶点着色器所定义的 SimpleConstantBuffer 常量缓冲区:


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


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

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

加载网格

Marble Maze 使用 SDK-Mesh 作为运行时格式,因为此格式提供了一种加载示例应用的网格数据的基本方式。对于生产用途,你应使用一种可满足游戏具体要求的网格格式。

MarbleMaze::LoadDeferredResources 方法在加载顶点和像素着色器后加载网格数据。网格是一个顶点数据集合,常常包含位置、法线数据、颜色、材料和纹理坐标等信息。网格通常在 3D 创作软件中创建,在与应用代码分开的文件中维护。弹珠和迷宫是该游戏使用的两个网格示例。

注意  Marble Maze 使用 AutoDesk FBX Interchange File (.fbx) 文件类型作为设计时格式,使用 SDK-Mesh (.sdkmesh) 文件类型作为运行时格式。SDK-Mesh 格式也用在其他 DirectX 示例中。SDK-Mesh 是一种二进制格式,提供了一种加载网格数据的基本方式。有关 SDK Mesh 格式的详细信息,请参阅示例内容导出程序更新

Marble Maze 使用 SDKMesh 类管理网格。这个类在 SDKMesh.h 中声明。SDKMesh 提供了加载、渲染和销毁网格数据的方法。

要点  Marble Maze 使用 SDK-Mesh 格式并仅提供 SDKMesh 类用于演示。尽管 SDK-Mesh 格式对学习和创建原型很有用,但它是一种非常基本的格式,可能无法满足大多数游戏开发的需求。我们建议使用一种可满足游戏具体要求的网格格式。

下面的示例展示了 MarbleMaze::LoadDeferredResources 方法如何使用 SDKMesh::Create 方法加载迷宫和球的网格数据。


// Load the meshes.
DX::ThrowIfFailed(
    m_mazeMesh.Create(
        m_d3dDevice.Get(),
        L"Media\\Models\\maze1.sdkmesh",
        false
        )
    );

DX::ThrowIfFailed(
    m_marbleMesh.Create(
        m_d3dDevice.Get(),
        L"Media\\Models\\marble2.sdkmesh",
        false
        )
    );

加载碰撞数据

尽管本节不会着重介绍 Marble Maze 如何实现弹珠与迷宫之间的力学模拟,但请注意,力学系统的网格几何图形在加载网格时已准备就绪。


// Extract mesh geometry for 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);


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

更新游戏状态

通过在渲染所有场景对象之前首先更新它们,Marble Maze 将游戏逻辑与渲染逻辑分开。

文档“Marble Maze 应用结构”介绍了主要的游戏循环。更新场景(这是游戏循环的一部分)在处理 Windows 事件和输入之后,渲染场景之前执行。MarbleMaze::Update 方法处理 UI 和游戏的更新。

更新用户界面

MarbleMaze::Update 方法调用 UserInterface::Update 方法来更新 UI 的状态。


UserInterface::GetInstance().Update(timeTotal, timeDelta);


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 的类实现 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);
}


更新场景

MarbleMaze::Update 方法根据当前的状态机状态来更新游戏。游戏处于活动状态时,Marble Maze 更新照相机以跟踪弹珠,更新常量缓冲区的视图矩阵部分,以及更新力学模拟。

下面的示例展示了 MarbleMaze::Update 方法如何更新照相机的位置。Marble Maze 使用 m_resetCamera 变量表明必须重置照相机,将其放在弹珠正上方。照相机在游戏开始或弹珠从迷宫掉落时重置。主菜单或高分显示屏幕处于活动状态时,照相机设置为一个固定位置。否则,Marble Maze 使用 timeDelta 参数将照相机的位置插入其当前位置与目标位置之间。目标位置在弹珠的前方稍微靠上的位置。使用已经历的帧时间,照相机能够不断跟踪或追踪弹珠。


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

// Gradually move the camera above the marble.
float3 targetEyePosition = marblePosition - (eyeDistance * float3(g.x, g.y, g.z));
if (m_resetCamera)
{
    eyePosition = targetEyePosition;
    m_resetCamera = false;
}
else
{
    eyePosition = eyePosition + ((targetEyePosition - eyePosition) * min(1, timeDelta * 8));
}

// Look at the marble. 
if ((m_gameState == GameState::MainMenu) || (m_gameState == GameState::HighScoreDisplay))
{
    // Override camera position for menus.
    eyePosition = marblePosition + float3(75.0f, -150.0f, -75.0f);
    m_camera->SetViewParameters(eyePosition, marblePosition, float3(0.0f, 0.0f, -1.0f));
}
else
{
    m_camera->SetViewParameters(eyePosition, marblePosition, float3(0.0f, 1.0f, 0.0f));
}


下面的示例展示了 MarbleMaze::Update 方法如何更新弹珠和迷宫的常量缓冲区。迷宫的模型或世界矩阵始终保留着单位矩阵。除了主要的对角(它包含所有元素),单位矩阵是一个由 0 组成的方形矩阵。弹珠的模型矩阵基于它的位置矩阵与旋转矩阵的乘积。multranslation 函数在 BasicMath.h 中定义。


// Update the model matrices based on the simulation.
m_mazeConstantBufferData.model = identity();
m_marbleConstantBufferData.model = mul(
    translation(marblePosition.x, marblePosition.y, marblePosition.z),
    marbleRotationMatrix
    );

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


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

渲染场景

渲染一个场景时,通常包含以下步骤。

  1. 设置当前渲染目标的深度模具缓冲区。
  2. 清除渲染和模具视图。
  3. 为绘制准备顶点和像素着色器。
  4. 渲染场景中的 3D 对象。
  5. 渲染你希望出现在场景前面的任何 2D 对象。
  6. 向监视器呈现已渲染的图像。

MarbleMaze::Render 方法绑定渲染目标和深度模具视图,清除它们的视图,绘制场景,然后绘制覆盖图。

准备渲染目标

渲染场景之前,必须设置当前渲染目标的深度模具缓冲区。如果你的场景无法保证在屏幕上的所有像素上绘制,应清除渲染和模具视图。Marble Maze 清除每一帧上的渲染和模具视图,以确保没有来自前一帧的可视工件。

下面的示例展示了 MarbleMaze::Render 方法如何调用 ID3D11DeviceContext::OMSetRenderTargets 方法将渲染目标和深度模具缓冲区设置为当前缓冲区。m_renderTargetView 成员变量(一个 ID3D11RenderTargetView 对象)和 m_depthStencilView 成员变量(一个 ID3D11DepthStencilView 对象)由 DirectXBase 类定义和初始化。


// Bind the render targets.
m_d3dContext->OMSetRenderTargets(
    1,
    m_renderTargetView.GetAddressOf(),
    m_depthStencilView.Get()
    );

// Clear the render target and depth stencil to default values. 
const float clearColor[4] = { 0.0f, 0.0f, 0.0f, 1.0f };

m_d3dContext->ClearRenderTargetView(
    m_renderTargetView.Get(),
    clearColor
    );

m_d3dContext->ClearDepthStencilView(
    m_depthStencilView.Get(),
    D3D11_CLEAR_DEPTH,
    1.0f,
    0
    );


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

准备顶点和像素着色器

渲染场景对象之前,执行以下步骤来准备用于绘制的顶点和像素着色器:

  1. 将着色器输入布局设置为当前布局。
  2. 将顶点和像素着色器设置为当前着色器。
  3. 使用你需要传递到着色器的数据更新任何常量缓冲区。

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

本文档中的加载着色器一节介绍了在创建顶点着色器时如何创建输入布局。下面的示例展示了 MarbleMaze::Render 方法如何使用 ID3D11DeviceContext::IASetInputLayout 方法来将此布局设置为当前布局。


m_d3dContext->IASetInputLayout(m_inputLayout.Get());


下面的示例展示了 MarbleMaze::Render 方法如何使用 ID3D11DeviceContext::VSSetShaderID3D11DeviceContext::PSSetShader 方法,将顶点和像素着色器分别设置为当前着色器。


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

// Set the pixel shader stage state.
m_d3dContext->PSSetShader(
    m_pixelShader.Get(),    // use this pixel shader 
    nullptr,                // don't use shader linkage
    0                       // don't use shader linkage
    );

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


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


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

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

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


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

渲染迷宫和弹珠

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


m_mazeMesh.Render(m_d3dContext.Get(), 0, INVALID_SAMPLER_SLOT, INVALID_SAMPLER_SLOT);


MarbleMaze::Render 方法执行类似的步骤来渲染弹珠。

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

绘制用户界面和覆盖图

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

MarbleMaze::Render 方法最终绘制用户界面和覆盖图。


// Draw the user interface and the overlay.
UserInterface::GetInstance().Render();

m_sampleOverlay->Render();


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


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

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

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


SampleOverlay::Render 方法使用一种类似技术来绘制覆盖位图。

呈现场景

绘制所有 2D 和 3D 场景对象后,Marble Maze 向监视器呈现已渲染的图像。它将图形与垂直空白同步,确保游戏不会花时间绘制从不会在显示器上实际显示的帧。Marble Maze 在呈现场景时还会处理设备更改。

MarbleMaze::Render 方法返回后,游戏循环调用 MarbleMaze::Present 方法来将已渲染的图像发送到监视器或显示器。MarbleMaze 类不会覆盖 DirectXBase::Present 方法。 DirectXBase::Present 方法调用 IDXGISwapChain1::Present 来执行呈现操作,如下面的示例所示:


// The application may optionally specify "dirty" or "scroll" rects 
// to improve efficiency in certain scenarios. 
// In this sample, however, we do not utilize those features.
DXGI_PRESENT_PARAMETERS parameters = {0};
parameters.DirtyRectsCount = 0;
parameters.pDirtyRects = nullptr;
parameters.pScrollRect = nullptr;
parameters.pScrollOffset = nullptr;

// 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->Present1(1, 0, &parameters);


在此示例中,m_swapChain 是一个 IDXGISwapChain1 对象。此对象的初始化已在本文的初始化 Direct3D 和 Direct2D 一节中介绍。

IDXGISwapChain1::Present 的第一个参数 SyncInterval 指定在呈现帧之前等待的垂直空白数量。Marble Maze 指定 1,以便它等到下一个垂直空白。垂直空白是一帧完成向监视器的绘制后与下一帧开始时之间的时间。

IDXGISwapChain1::Present1 方法返回一个错误代码,表明设备已删除或失败。在此情况下,Marble Maze 重新初始化设备。


// Reinitialize the renderer if the device was disconnected  
// or the driver was upgraded. 
if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET)
{
    Initialize(m_window, m_dpi);
}
else
{
    DX::ThrowIfFailed(hr);
}


后续步骤

查阅向 Marble Maze 示例添加输入和交互性,了解在使用输入设备时要记住的一些重要实践。这篇文章讨论了 Marble Maze 如何支持触摸、加速计、Xbox 360 控制器和鼠标输入。

相关主题

向 Marble Maze 示例添加输入和交互性
Marble Maze 应用结构
开发 Marble Maze,一款使用 C++ 和 DirectX 的 Windows 应用商店游戏

 

 

显示:
© 2014 Microsoft