向 Marble Maze 示例添加可视内容

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

在开发大理石迷宫游戏的视觉方面时,我们遵循了以下基本步骤:

  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 硬件上运行。 大理石迷宫游戏支持功能级别 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 支持在图形运行时之间共享视频内存图面。 大理石迷宫游戏使用 Direct3D 设备中的基础 DXGI 设备根据 Direct2D 工厂创建 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 运行时中,视图是指应用的用户界面设置集合,包括显示区域和输入行为以及用于处理的线程。 可以在创建视图时指定所需的配置和设置。 大理石迷宫游戏应用程序结构中介绍了设置应用视图的过程。 在 Direct3D 中,“视图”整个术语具有多个含义。 资源视图定义一种资源可访问的子资源。 例如,当某个纹理对象与某个着色器资源视图相关联时,该着色器稍后可访问该纹理。 资源视图的一个优点是,可以在渲染管道中的不同阶段以不同方式解释数据。 有关资源视图的详细信息,请参阅资源视图。 在视图变换或视图变换矩阵的上下文中使用时,视图是指相机的位置和方向。 视图变换在相机位置和方向周围的世界中重新定位对象。 有关视图变换的详细信息,请参阅视图变换 (Direct3D 9)。 本主题更详细地介绍了大理石迷宫游戏如何使用资源和矩阵视图。

 

加载场景资源

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

为了确保响应式应用,大理石迷宫游戏以异步方式或在后台加载场景资源。 资源加载到后台后,游戏即可响应窗口事件。 若要更详细地了解此过程,请查看本指南中的在后台加载游戏资产

加载 2D 覆盖和用户界面

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

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

用户界面 (UI) 由 2D 组件组成,例如菜单和抬头显示器 (HUD),它们显示在场景前面。 大理石迷宫游戏定义了以下 UI 元素:

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

Marble Maze 在 UserInterface.h 中定义特定于游戏的 UI 元素。 大理石迷宫游戏将 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 管理并提供可重用的用户界面管理器。 大理石迷宫游戏定义派生自 ElementBase 的类型,这些类型实现游戏特定行为。 例如,HighScoreTable 定义高分表的行为。 有关这些类型的详细信息,请参阅源代码。

注意

因为 XAML 支持更轻松地创建复杂的用户界面(例如模拟和战略游戏中的用户界面),所以应考虑是否使用 XAML 来定义 UI。 有关如何使用 XAML 在 DirectX UWP 游戏中开发用户界面的信息,请参阅扩展游戏示例,它指的是 DirectX 3D 射击游戏示例。

 

加载着色器

大理石迷宫游戏使用 BasicLoader::LoadShader 方法从文件加载着色器。

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

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

MarbleMazeMain::LoadDeferredResources 方法在加载覆盖后,加载一个顶点着色器和一个像素着色器。 这些着色器的设计时版本分别在 BasicVertexShader.hlsl 和 BasicPixelShader.hlsl 中定义。 大理石迷宫游戏在渲染阶段将这些着色器应用于球和迷宫。

大理石迷宫游戏项目包括着色器文件的 .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;
}

像素着色器接收顶点着色器的输出作为输入。 此着色器执行照明计算来模拟将鼠标悬停在迷宫上的软边聚光灯,并与大理石的位置对齐。 对于直接指向光的图面,照明最强。 当表面法线垂直于光时,漫射分量逐渐减少到零,而环境项随着法线点远离光而减少。 更靠近大理石的点(因此更靠近聚光灯的中心)照明更强。 不过,针对大理石下面的点调整了照片来模拟柔和阴影。 在真实环境中,像白色大理石这样的对象会将聚光灯漫射到场景中的其他对象上。 对于在大理石明亮那半面视野内的图面,这是近似值。 额外的照明因素在与大理石相对的角度和距离中。 生成的像素颜色是采样纹理与照明计算结果的组合。

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.cso。 m_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 对象。 input-layout 对象封装了输入汇编程序 (IA) 阶段的输入状态。 IA 阶段的一项工作是使用系统生成的值(也称为语义)来提高着色器的效率,以便仅处理尚未处理的基元或顶点。

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

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

将输入布局与顶点着色器定义的 sVSInput 结构进行比较,如以下示例所示。 sVSInput 结构定义 POSITION、NORMAL 和 TEXCOORD0 元素。 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 缓冲区对数据集合进行分组。 常量缓冲区是一种可用于将数据传递给着色器的缓冲区。 大理石迷宫游戏使用常量缓冲区来保存模型视图(也称为世界视图),以及活动场景对象的投影矩阵。

下面的示例展示了 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 对象绑定到常量缓冲区。 下面的示例展示了 ConstantBuffer 结构,它位于 MarbleMazeMain.h 中。

// 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 中的缓冲区简介。 有关寄存器关键字的详细信息,请参阅寄存器

加载网格

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

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

大理石迷宫游戏使用 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
        )
    );

加载碰撞数据

虽然本部分没有着重介绍大理石迷宫游戏如何实现大理石和迷宫之间的物理模拟,但请注意,加载网格时会读取物理系统的网格几何图形。

// 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);

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

更新游戏状态

大理石迷宫游戏通过在渲染场景对象之前先更新所有对象,将游戏逻辑与渲染逻辑分开。

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 方法基于状态机的当前状态更新游戏(GameState 存储在 m_gameState 中)。 游戏处于活动状态时 (GameState::InGameActive),Marble Maze 更新相机以跟踪弹珠、更新常量缓冲区的视图矩阵部分,并更新力学模拟。

下面的示例展示了 MarbleMazeMain::Update 方法如何更新相机的位置。 Marble Maze 使用 m_resetCamera 变量表明必须重置相机,才能将其放在弹珠正上方。 当游戏开始或大理石穿过迷宫时,相机将重置。 当主菜单或高分显示屏幕处于活动状态时,相机将设置在一个固定位置。 否则,大理石迷宫游戏使用 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 之外,单位矩阵是一个由 0 组成的方块矩阵。 大理石的模型矩阵基于其位置矩阵乘以其旋转矩阵。

// 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 方法绑定呈现目标和深度模具视图、清除这些视图、绘制场景,然后绘制覆盖。

准备渲染目标

在渲染场景之前,必须设置当前渲染目标深度-模具缓冲区。 如果无法保证场景在屏幕上的每个像素上绘制,还要清除渲染和模具视图。 大理石迷宫游戏会清除每一帧上的渲染和模具视图,确保没有来自上一帧中的可见工件。

下面的示例展示了 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 管道的输出合并阶段。 有关输出合并阶段的详细信息,请参阅输出合并阶段

准备顶点着色器和像素着色器

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

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

重要

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

 

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

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

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

// 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 内存。 回想一下,ConstantBuffer 结构的模型和视图组件在 MarbleMazeMain::Update 方法中更新。 然后,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 向监视器呈现已渲染的图像。 它将绘图同步到垂直空白,确保不会花费时间来绘制实际上永远不会在显示器上显示的帧。 大理石迷宫游戏还会在呈现场景时处理设备变化。

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_swapChain 是一个 IDXGISwapChain1 对象。 有关此对象的初始化,请参阅本文档的初始化 Direct3D 和 Direct2D 部分。

IDXGISwapChain::Present 的第一个参数 SyncInterval 指定在呈现帧之前等待的垂直空白数量。 大理石迷宫游戏指定 1,因此它会等待,直到下一个垂直空白。

IDXGISwapChain::Present 方法返回一个错误代码,表明设备已删除或失败。 在这种情况下,大理石迷宫游戏会重新初始化设备。

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

后续步骤

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