三维通用 Windows 平台 (UWP) 游戏通常使用多边形来表示游戏中的对象和图面。 构成这些多边形对象和图面结构的顶点列表称为网格。 在这里,我们将为立方体对象创建一个基本网格,并将其提供给着色器管道进行渲染。
重要 此处包含的示例代码使用在 DirectXMath.h 中声明的类型(如 DirectX::XMFLOAT3 和 DirectX::XMFLOAT4X4) 和内联方法。 如果要复制并粘贴这段代码,请在项目中 #include <DirectXMath.h>。
需要了解的内容
技术
先决条件
- 线性代数和三维坐标系的基础知识
- Visual Studio 2015 或更高版本 Direct3D 模板
说明书
这些步骤将向您演示如何创建一个基本网格立方体。
步骤 1:构造模型的网格
在大多数游戏中,游戏对象的网格是从包含特定顶点数据的文件中加载的。 这些顶点的排序依赖于应用程序,但它们通常排列为条带或风扇。 顶点数据可以来自任何软件源,也可以手动创建。 由你来解释数据的方式决定了顶点着色器能否有效地处理它。
在本示例中,我们使用一个立方体的简单网格。 在管道的这一阶段,像任何对象网格一样,立方体使用其自身的坐标系来表示。 顶点着色器采用其坐标,通过应用所提供的转换矩阵,返回同质坐标系中的最终二维视图投影。
定义一个立方体的网格。 (或者从文件中加载它。这取决于你!)
SimpleCubeVertex cubeVertices[] =
{
{ DirectX::XMFLOAT3(-0.5f, 0.5f, -0.5f), DirectX::XMFLOAT3(0.0f, 1.0f, 0.0f) }, // +Y (top face)
{ DirectX::XMFLOAT3( 0.5f, 0.5f, -0.5f), DirectX::XMFLOAT3(1.0f, 1.0f, 0.0f) },
{ DirectX::XMFLOAT3( 0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(1.0f, 1.0f, 1.0f) },
{ DirectX::XMFLOAT3(-0.5f, 0.5f, 0.5f), DirectX::XMFLOAT3(0.0f, 1.0f, 1.0f) },
{ DirectX::XMFLOAT3(-0.5f, -0.5f, 0.5f), DirectX::XMFLOAT3(0.0f, 0.0f, 1.0f) }, // -Y (bottom face)
{ DirectX::XMFLOAT3( 0.5f, -0.5f, 0.5f), DirectX::XMFLOAT3(1.0f, 0.0f, 1.0f) },
{ DirectX::XMFLOAT3( 0.5f, -0.5f, -0.5f), DirectX::XMFLOAT3(1.0f, 0.0f, 0.0f) },
{ DirectX::XMFLOAT3(-0.5f, -0.5f, -0.5f), DirectX::XMFLOAT3(0.0f, 0.0f, 0.0f) },
};
立方体的坐标系将立方体的中心置于原点,左手坐标系下,y 轴从上到下延伸。 坐标值表示为 32 位浮点值,介于 -1 和 1 之间。
在每个括号配对中,第二个 DirectX::XMFLOAT3 值组将与顶点关联的颜色指定为 RGB 值。 例如,第一个位于 (-0.5, 0.5, -0.5) 的顶点具有全绿色(G 值设置为 1.0,“R”和“B”值设置为 0)。
因此,有 8 个顶点,每个顶点都有特定颜色。 每个顶点/颜色配对是我们示例中顶点的完整数据。 指定顶点缓冲区时,必须牢记此特定布局。 我们将此输入布局提供给顶点着色器,以便它可以了解顶点数据。
步骤 2:设置输入布局
现在,顶点已经存储在内存中。 但是,你的图形设备有自己的内存,并且你使用 Direct3D 访问它。 要将顶点数据传输到图形设备进行处理,您需要做好准备:必须声明顶点数据的布局方式,以便图形设备在从游戏中获取数据时能够正确解释。 为此,您可以使用 ID3D11InputLayout。
声明并设置顶点缓冲区的输入布局。
const D3D11_INPUT_ELEMENT_DESC basicVertexLayoutDesc[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
ComPtr<ID3D11InputLayout> inputLayout;
m_d3dDevice->CreateInputLayout(
basicVertexLayoutDesc,
ARRAYSIZE(basicVertexLayoutDesc),
vertexShaderBytecode->Data,
vertexShaderBytecode->Length,
&inputLayout)
);
在此代码中,为顶点指定布局,具体来说,顶点列表中的每个元素包含的数据。 此处,在 basicVertexLayoutDesc中,定义两个数据组件:
POSITION:这是提供给着色器的位置数据的 HLSL 语义。 在此代码中,它是一个 DirectX::XMFLOAT3,或者更具体地说,它是一个结构,该结构具有 3 个 32 位浮点值,对应于 3D 坐标(x、y、z)。 如果提供同质的“w”坐标,则也可以使用 float4,在这种情况下,可以指定DXGI_FORMAT_R32G32B32A32_FLOAT。 无论是使用 DirectX::XMFLOAT3 还是 float4,都符合游戏的特定需求。 只需确保网格的顶点数据与所使用的格式正确对应!
每个坐标值在对象的坐标空间中以 -1 和 1 之间的浮点值表示。 顶点着色器完成后,转换的顶点位于同质(透视更正)视图投影空间中。
你聪明地指出:“但枚举值指示的是 RGB,而不是 XYZ!” 眼光不错! 在颜色数据和坐标数据的情况下,通常使用 3 或 4 个组件值,因此为什么不对两者使用相同的格式? HLSL 语义(而不是格式名称)指示着色器如何处理数据。
COLOR:这是颜色数据的 HLSL 语义。 与 POSITION 一样,它包含 3 个 32 位浮点值(DirectX::XMFLOAT3)。 每个值都包含一个颜色分量:红色(r)、蓝色(b)或绿色(g),表示为介于 0 和 1 之间的浮点数。
COLOR 值通常在着色器管道的末尾以 RGBA 的4分量值形式返回。 对于此示例,你将在所有像素的着色器管道中将“A”alpha 值设置为 1.0(最大不透明度)。
有关格式的完整列表,请参阅 DXGI_FORMAT。 有关 HLSL 语义的完整列表,请参阅 语义。
调用 ID3D11Device::CreateInputLayout 并在 Direct3D 设备上创建输入布局。 现在,需要创建一个可以实际保存数据的缓冲区!
步骤 3:填充顶点缓冲区
顶点缓冲区包含网格中每个三角形的顶点列表。 每个顶点必须在此列表中是唯一的。 在我们的示例中,立方体有 8 个顶点。 顶点着色器在图形设备上运行,并从顶点缓冲区读取数据,并根据上一步中指定的输入布局解释数据。
在下一个示例中,您将为缓冲区提供描述和子资源,这些内容告知 Direct3D 有关顶点数据的物理映射信息,以及如何在图形设备的内存中处理这些数据。 这之所以必要,是因为你使用了通用的 ID3D11Buffer,它可能含有任何东西! 提供了 D3D11_BUFFER_DESC 和 D3D11_SUBRESOURCE_DATA 结构,以确保 Direct3D 了解缓冲区的物理内存布局,包括缓冲区中每个顶点元素的大小以及顶点列表的最大大小。 还可以在此处控制对缓冲区内存的访问及其遍历方式,但这有点超出本教程的范围。
配置缓冲区后,调用 ID3D11Device::CreateBuffer 以实际创建缓冲区。 显然,如果你有多个对象,请为每个唯一模型创建缓冲区。
声明并创建顶点缓冲区。
D3D11_BUFFER_DESC vertexBufferDesc = {0};
vertexBufferDesc.ByteWidth = sizeof(SimpleCubeVertex) * ARRAYSIZE(cubeVertices);
vertexBufferDesc.Usage = D3D11_USAGE_DEFAULT;
vertexBufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
vertexBufferDesc.CPUAccessFlags = 0;
vertexBufferDesc.MiscFlags = 0;
vertexBufferDesc.StructureByteStride = 0;
D3D11_SUBRESOURCE_DATA vertexBufferData;
vertexBufferData.pSysMem = cubeVertices;
vertexBufferData.SysMemPitch = 0;
vertexBufferData.SysMemSlicePitch = 0;
ComPtr<ID3D11Buffer> vertexBuffer;
m_d3dDevice->CreateBuffer(
&vertexBufferDesc,
&vertexBufferData,
&vertexBuffer);
已加载顶点。 但是处理这些顶点的顺序是什么? 当你提供一个索引列表给顶点时,这些索引的顺序将决定顶点着色器处理它们的顺序。
步骤 4:填充索引缓冲区
请提供每个顶点的索引列表。 这些索引对应于顶点缓冲区中顶点的位置,从 0 开始。 为了帮助你可视化这一点,请考虑网格中的每个唯一顶点都有一个分配给它的唯一数字,例如 ID。 此 ID 是顶点缓冲区中顶点的整数位置。
在我们的示例多维数据集中,有 8 个顶点,用于为边创建 6 个象限。 将四边形拆分为三角形,总共有 12 个三角形,这些三角形使用我们的 8 个顶点。 在每个三角形的 3 个顶点处,索引缓冲区中有 36 个条目。 在我们的示例中,此索引模式称为三角形列表,并在设置基元拓扑时将其作为 D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST 指示给 Direct3D。
这可能是列出索引的最低效方法,因为当三角形共享点和两侧时有许多冗余。 例如,当三角形在隆隆形状中共享一侧时,将列出四个顶点的 6 个索引,如下所示:
在构造菱形 时,索引的顺序为
- 三角形 1: [0, 1, 2]
- 三角形 2: [0, 2, 3]
在条带或风扇拓扑中,你以在遍历期间消除许多冗余侧的方式对顶点进行排序(例如从索引 0 到图像中的索引 2 的一侧)。对于大型网格,这大大减少了顶点着色器的运行次数,并显著提高了性能。 但是,我们将保持简单化,并使用三角形列表。
将顶点缓冲区的索引声明为简单的三角形列表拓扑。
unsigned short cubeIndices[] =
{ 0, 1, 2,
0, 2, 3,
4, 5, 6,
4, 6, 7,
3, 2, 5,
3, 5, 4,
2, 1, 6,
2, 6, 5,
1, 7, 6,
1, 0, 7,
0, 3, 4,
0, 4, 7 };
只有 8 个顶点时,缓冲区中的 36 个索引元素非常冗余! 如果选择消除某些冗余并使用不同的顶点列表类型(如条带或风扇),则必须在向 ID3D11DeviceContext::IASetPrimitiveTopology 方法提供特定的D3D11_PRIMITIVE_TOPOLOGY值时指定该类型。
有关不同索引列表技术的详细信息,请参阅 基元拓扑。
步骤 5:为转换矩阵创建常量缓冲区
在开始处理顶点之前,需要提供在运行顶点时将应用到每个顶点的转换矩阵(乘数)。 对于大多数三维游戏,有三个:
- 从对象(模型)坐标系转换为整体世界坐标系的 4x4 矩阵。
- 从世界坐标系转换为相机(视图)坐标系的 4x4 矩阵。
- 从相机坐标系转换为二维视图投影坐标系的 4x4 矩阵。
这些矩阵将被传递给着色器,位于 常量缓冲区中。 常量缓冲区是一个内存区域,在着色器管道的下一次传递执行过程中保持不变,并且可以通过 HLSL 代码的着色器直接访问该区域。 您需要定义每个常量缓冲区两次:首先在您的游戏 C++ 代码中,然后至少在着色器代码类似 C 的 HLSL 语法中定义一次。 这两个声明必须直接对应于类型和数据对齐方式。 当着色器使用 HLSL 声明来解释 C++ 中声明的数据时,如果类型不匹配或数据对齐出错,就很容易引入难以发现的错误。
HLSL 不会更改常量缓冲区。 可以在游戏更新特定数据时更改它们。 通常,游戏开发人员会创建4类常量缓冲区:每帧更新一种类型,每个模型/对象更新一种类型,每次游戏状态刷新的更新一种类型,以及用于游戏生命周期中不变的数据的一种类型。
在此示例中,我们只有一个永远不会更改的数据:三个矩阵的 DirectX::XMFLOAT4X4 数据。
注意 此处提供的示例代码使用列主矩阵。 可以使用 HLSL 中的 row_major 关键字来改用行主矩阵,并确保源矩阵数据也是行主矩阵。 DirectXMath 使用行主矩阵,可以直接与 使用 row_major 关键字定义的 HLSL 矩阵一起使用。
声明并创建用于转换每个顶点的三个矩阵的常量缓冲区。
struct ConstantBuffer
{
DirectX::XMFLOAT4X4 model;
DirectX::XMFLOAT4X4 view;
DirectX::XMFLOAT4X4 projection;
};
ComPtr<ID3D11Buffer> m_constantBuffer;
ConstantBuffer m_constantBufferData;
// ...
// Create a constant buffer for passing model, view, and projection matrices
// to the vertex shader. This allows us to rotate the cube and apply
// a perspective projection to it.
D3D11_BUFFER_DESC constantBufferDesc = {0};
constantBufferDesc.ByteWidth = sizeof(m_constantBufferData);
constantBufferDesc.Usage = D3D11_USAGE_DEFAULT;
constantBufferDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
constantBufferDesc.CPUAccessFlags = 0;
constantBufferDesc.MiscFlags = 0;
constantBufferDesc.StructureByteStride = 0;
m_d3dDevice->CreateBuffer(
&constantBufferDesc,
nullptr,
&m_constantBuffer
);
m_constantBufferData.model = DirectX::XMFLOAT4X4( // Identity matrix, since you are not animating the object
1.0f, 0.0f, 0.0f, 0.0f,
0.0f, 1.0f, 0.0f, 0.0f,
0.0f, 0.0f, 1.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f);
);
// Specify the view (camera) transform corresponding to a camera position of
// X = 0, Y = 1, Z = 2.
m_constantBufferData.view = DirectX::XMFLOAT4X4(
-1.00000000f, 0.00000000f, 0.00000000f, 0.00000000f,
0.00000000f, 0.89442718f, 0.44721359f, 0.00000000f,
0.00000000f, 0.44721359f, -0.89442718f, -2.23606800f,
0.00000000f, 0.00000000f, 0.00000000f, 1.00000000f);
注意 在设置设备特定资源时,通常声明投影矩阵,因为与其相乘的结果必须与当前二维视区的大小参数相匹配(这些参数通常对应于显示器的像素高度和宽度)。 如果这些发生变化,您必须相应地调整 x 坐标值和 y 坐标值。
// Finally, update the constant buffer perspective projection parameters
// to account for the size of the application window. In this sample,
// the parameters are fixed to a 70-degree field of view, with a depth
// range of 0.01 to 100.
float xScale = 1.42814801f;
float yScale = 1.42814801f;
if (backBufferDesc.Width > backBufferDesc.Height)
{
xScale = yScale *
static_cast<float>(backBufferDesc.Height) /
static_cast<float>(backBufferDesc.Width);
}
else
{
yScale = xScale *
static_cast<float>(backBufferDesc.Width) /
static_cast<float>(backBufferDesc.Height);
}
m_constantBufferData.projection = DirectX::XMFLOAT4X4(
xScale, 0.0f, 0.0f, 0.0f,
0.0f, yScale, 0.0f, 0.0f,
0.0f, 0.0f, -1.0f, -0.01f,
0.0f, 0.0f, -1.0f, 0.0f
);
在此处时,在ID3D11DeviceContext上设置顶点和索引缓冲区,以及正在使用的拓扑。
// Set the vertex and index buffers, and specify the way they define geometry.
UINT stride = sizeof(SimpleCubeVertex);
UINT offset = 0;
m_d3dDeviceContext->IASetVertexBuffers(
0,
1,
vertexBuffer.GetAddressOf(),
&stride,
&offset);
m_d3dDeviceContext->IASetIndexBuffer(
indexBuffer.Get(),
DXGI_FORMAT_R16_UINT,
0);
m_d3dDeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
好吧! 输入装配已完成。 所有内容都已到位,可以呈现。 让我们启动顶点着色器。
步骤 6:使用顶点着色器处理网格
现在,你有了一个顶点缓冲区,其中包含定义网格的顶点,以及定义顶点的处理顺序的索引缓冲区,然后将它们发送到顶点着色器。 编译为高级着色器语言的顶点着色器代码会针对顶点缓冲区中的每个顶点运行一次,使你能够执行每个顶点的变换。 最终结果通常是二维投影。
(是否加载顶点着色器?如果没有,请查看 如何在 DirectX 游戏中加载资源。
在这里,你将创建顶点着色器...
// Set the vertex and pixel shader stage state.
m_d3dDeviceContext->VSSetShader(
vertexShader.Get(),
nullptr,
0);
...并设置常量缓冲区。
m_d3dDeviceContext->VSSetConstantBuffers(
0,
1,
m_constantBuffer.GetAddressOf());
下面是处理从对象坐标到世界坐标再到二维视图投影坐标系的转换的顶点着色器代码。 你还应用一些简单的每顶点照明,以增加美观。 这段代码应该放在你的顶点着色器的 HLSL 文件中(在这个例子中是 SimplerVertexShader.hlsl)。
cbuffer simpleConstantBuffer : register( b0 )
{
matrix model;
matrix view;
matrix projection;
};
struct VertexShaderInput
{
DirectX::XMFLOAT3 pos : POSITION;
DirectX::XMFLOAT3 color : COLOR;
};
struct PixelShaderInput
{
float4 pos : SV_POSITION;
float4 color : COLOR;
};
PixelShaderInput SimpleVertexShader(VertexShaderInput input)
{
PixelShaderInput vertexShaderOutput;
float4 pos = float4(input.pos, 1.0f);
// Transform the vertex position into projection space.
pos = mul(pos, model);
pos = mul(pos, view);
pos = mul(pos, projection);
vertexShaderOutput.pos = pos;
// Pass the vertex color through to the pixel shader.
vertexShaderOutput.color = float4(input.color, 1.0f);
return vertexShaderOutput;
}
你看到顶部的 cbuffer 吗? 这是在 HLSL 中与我们之前在 C++ 代码中声明的常量缓冲区相对应的部分。 VertexShaderInput 结构体? 哦,那看起来就像你的输入布局和顶点数据声明! 重要的是,C++代码中的常量缓冲区和顶点数据声明与 HLSL 代码中的声明匹配,包括符号、类型和数据对齐。
PixelShaderInput 指定顶点着色器主函数返回的数据的布局。 完成顶点处理后,你将返回二维投影空间中的顶点位置,以及用于每顶点照明的颜色。 图形卡使用着色器输出的数据来计算在管道下一阶段运行像素着色器时需要着色的“片段”(可能的像素)。
步骤 7:通过像素着色器传递网格
通常在图形管道的这一阶段,你会对对象可见的投影面执行逐像素操作。 人们喜欢纹理。然而,出于示例的目的,只需通过此阶段即可。
首先,创建像素着色器的实例。 像素着色器针对场景的二维投影中的每个像素运行,并将颜色分配给该像素。 在这种情况下,我们将直接传递顶点着色器返回的像素的颜色。
设置像素着色器。
m_d3dDeviceContext->PSSetShader( pixelShader.Get(), nullptr, 0 );
在 HLSL 中定义传递像素着色器。
struct PixelShaderInput
{
float4 pos : SV_POSITION;
};
float4 SimplePixelShader(PixelShaderInput input) : SV_TARGET
{
// Draw the entire triangle yellow.
return float4(1.0f, 1.0f, 0.0f, 1.0f);
}
将此代码放在与顶点着色器 HLSL(如 SimplePixelShader.hlsl)分开的 HLSL 文件中。 此代码针对视区中的每个可见像素运行一次(要绘制到的屏幕部分的内存中表示形式),在本例中,该代码映射到整个屏幕。 现在,您的图形管道已完全定义完成!
步骤 8:光栅化和显示网格
让我们运行管道。 这很简单:调用 ID3D11DeviceContext::DrawIndexed。
绘制立方体!
// Draw the cube.
m_d3dDeviceContext->DrawIndexed( ARRAYSIZE(cubeIndices), 0, 0 );
在显卡内部,每个顶点会按照索引缓冲区中指定的顺序被处理。 代码执行顶点着色器并定义二维片段后,将调用像素着色器并着色三角形。
现在,将立方体放在屏幕上。
向显示器显示该帧缓冲区。
// Present the rendered image to the window. Because the maximum frame latency is set to 1,
// the render loop is generally throttled to the screen refresh rate, typically around
// 60 Hz, by sleeping the app on Present until the screen is refreshed.
m_swapChain->Present(1, 0);
你完成了! 对于充满模型的场景,请使用多个顶点和索引缓冲区,甚至可能对不同的模型类型使用不同的着色器。 请记住,每个模型都有自己的坐标系,需要使用常量缓冲区中定义的矩阵将它们转换为共享世界坐标系。
注解
本主题介绍如何创建和显示自己创建的简单几何图形。 有关从文件加载更复杂的几何图形并将其转换为示例特定的顶点缓冲区对象(.vbo)格式的详细信息,请参阅 如何在 DirectX 游戏中加载资源。
相关主题