在之前的教程中,我们建立了一个最小的Direct3D 11的应用程序,它用来在窗口上输出一个单一颜色。在本次教程中,我们将扩展这个应用程序,在屏幕上渲染出一个单一颜色的三角形。我们将通过设置数据机构的过程关联到三角形。
这个教程的输出结果是在窗口中央渲染出一个三角形。
(SDK root)\Samples\C++\Direct3D11\Tutorials\Tutorial02
Github-LearnDirectX-DX3D11 tutorial02 (源码已上传至Github)
三角形由其三个点定义,也称为顶点。 具有唯一位置的一组三个顶点定义了唯一的三角形。 为了让GPU渲染三角形,我们必须告诉它三角形的三个顶点的位置。举一个2D的例子,假设我们希望渲染一个三角形,例如图1中的三角形。我们将三个顶点与位置(0,0)(0,1)和(1,0)一起传递给GPU,然后 GPU有足够的信息来渲染我们想要的三角形。
图1.由三个顶点定义的2D三角形
所以现在我们知道我们必须将三个位置传递给GPU才能渲染三角形。 我们如何将这些信息传递给GPU? 在Direct3D 11中,诸如位置的顶点信息存储在缓冲区资源中。 用于存储顶点信息的缓冲区被称为顶点缓冲区,这并不奇怪。 我们必须为三个顶点创建一个足够大的顶点缓冲区,并用顶点位置填充它。 在Direct3D 11中,应用程序必须在创建缓冲区资源时指定缓冲区大小(以字节为单位)。 我们知道缓冲区必须足够大才能容纳三个顶点,但每个顶点需要多少字节? 要回答这个问题,需要了解顶点布局。
顶点有一个位置。 通常,它还具有其他属性,例如法线,一种或多种颜色,纹理坐标(用于纹理映射)等。 顶点布局定义了这些属性在内存中的位置:每个属性使用的数据类型,每个属性的大小以及内存中属性的顺序。 因为属性通常具有不同的类型,类似于C结构中的字段,所以顶点通常由结构表示。 顶点的大小可以方便地从结构的大小中获得。
在本教程中,我们只处理顶点的位置。 因此,我们使用XMFLOAT3类型的单个字段定义顶点结构。 此类型是三个浮点组件的向量,通常是用于3D位置的数据类型。
struct SimpleVertex
{
XMFLOAT3 Pos; // Position
};
我们现在有一个表示我们的顶点的结构。 它负责在我们的应用程序中将顶点信息存储在系统内存中。 然而,当我们向GPU提供包含顶点的顶点缓冲区时,我们只是给它一块内存。 GPU还必须知道顶点布局,以便从缓冲区中提取正确的属性。 要实现此目的,需要使用输入布局。
在Direct3D 11中,输入布局是Direct3D对象,它以GPU可以理解的方式描述顶点的结构。 可以使用D3D11_INPUT_ELEMENT_DESC结构描述每个顶点属性。 应用程序定义一个或多个D3D11_INPUT_ELEMENT_DESC的数组,然后使用该数组创建输入布局对象,该对象将顶点描述为一个整体。 现在我们将详细介绍D3D11_INPUT_ELEMENT_DESC的字段。
SemanticName | SemanticName是一个字符串,其中包含描述此元素的性质或目的(或语义)的单词。 这个词可以是C标识符可以的任何形式,也可以是我们选择的任何形式。 例如,顶点位置的良好语义名称是POSITION。 语义名称不区分大小写。 |
---|---|
SemanticIndex | SemanticIndex补充了语义名称。 顶点可以具有相同性质的多个属性。 例如,它可以具有2组纹理坐标或2组颜色。 不是使用附加了数字的语义名称,例如“COLOR0”和“COLOR1”,这两个元素可以共享单个语义名称“COLOR”,具有不同的语义索引0和1。 |
Format | 格式定义要用于此元素的数据类型。 例如,DXGI_FORMAT_R32G32B32_FLOAT的格式有三个32位浮点数,使元素长12个字节。 DXGI_FORMAT_R16G16B16A16_UINT的格式有四个16位无符号整数,使元素长8个字节。 |
InputSlot | 如前所述,Direct3D 11应用程序通过使用顶点缓冲区将顶点数据传递给GPU。 在Direct3D 11中,可以同时向GPU提供多个顶点缓冲区,准确地说是16。 每个顶点缓冲区都绑定到0到15之间的输入槽号.InputSlot字段告诉GPU它应该为该元素获取哪个顶点缓冲区。 |
AlignedByteOffset | 顶点存储在顶点缓冲区中,顶层缓冲区只是一块内存。 AlignedByteOffset字段告诉GPU开始获取此元素数据的内存位置。 |
InputSlotClass | 该字段的值通常为D3D11_INPUT_PER_VERTEX_DATA。 当应用程序使用实例化时,它可以将输入布局的InputSlotClass设置为D3D11_INPUT_PER_INSTANCE_DATA以使用包含实例数据的顶点缓冲区。 Instancing是一个高级的Direct3D主题,这里不再讨论。 对于我们的教程,我们将专门使用D3D11_INPUT_PER_VERTEX_DATA。 |
InstanceDataStepRate | 该字段用于实例化。 由于我们不使用实例化,因此不使用此字段,必须将其设置为0。 |
现在我们可以定义D3D11_INPUT_ELEMENT_DESC数组并创建输入布局:
// Define the input layout
D3D11_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
};
UINT numElements = ARRAYSIZE(layout);
在下一个教程中,我们将解释技术对象和关联的着色器。 目前,我们将专注于为该技术创建Direct3D 11顶点布局对象。 但是,我们将了解顶点着色器与此顶点布局紧密耦合。 原因是创建顶点布局对象需要顶点着色器的输入签名。 我们使用从D3DX11CompileFromFile返回的ID3DBlob对象来检索表示顶点着色器的输入签名的二进制数据。 获得此数据后,我们可以调用ID3D11Device :: CreateInputLayout()来创建顶点布局对象,并使用ID3D11DeviceContext :: IASetInputLayout()将其设置为活动顶点布局。 完成所有这些操作的代码如下所示:
// Create the input layout
if( FAILED( g_pd3dDevice->CreateInputLayout( layout, numElements, pVSBlob->GetBufferPointer(),
pVSBlob->GetBufferSize(), &g_pVertexLayout ) ) )
return FALSE;
// Set the input layout
g_pImmediateContext->IASetInputLayout( g_pVertexLayout );
在初始化期间我们还需要做的一件事是创建保存顶点数据的顶点缓冲区。 要在Direct3D 11中创建顶点缓冲区,我们填写两个结构D3D11_BUFFER_DESC和D3D11_SUBRESOURCE_DATA,然后调用ID3D11Device :: CreateBuffer()。
D3D11_BUFFER_DESC描述了要创建的顶点缓冲区对象,D3D11_SUBRESOURCE_DATA描述了在创建过程中将复制到顶点缓冲区的实际数据。 顶点缓冲区的创建和初始化是一次完成的,因此我们以后不需要初始化缓冲区。 将复制到顶点缓冲区的数据是顶点,即三个简单结构的数组。 选择顶点数组中的坐标,以便在使用着色器渲染时在应用程序窗口的中间看到一个三角形。 创建顶点缓冲区后,我们可以调用ID3D11DeviceContext :: IASetVertexBuffers()将其绑定到设备。 完整的代码如下所示:
// Create vertex buffer
SimpleVertex vertices[] =
{
XMFLOAT3( 0.0f, 0.5f, 0.5f ),
XMFLOAT3( 0.5f, -0.5f, 0.5f ),
XMFLOAT3( -0.5f, -0.5f, 0.5f ),
};
D3D11_BUFFER_DESC bd;
ZeroMemory( &bd, sizeof(bd) );
bd.Usage = D3D11_USAGE_DEFAULT;
bd.ByteWidth = sizeof( SimpleVertex ) * 3;
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags = 0;
bd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA InitData;
ZeroMemory( &InitData, sizeof(InitData) );
InitData.pSysMem = vertices;
if( FAILED( g_pd3dDevice->CreateBuffer( &bd, &InitData, &g_pVertexBuffer ) ) )
return FALSE;
// Set vertex buffer
UINT stride = sizeof( SimpleVertex );
UINT offset = 0;
g_pImmediateContext->IASetVertexBuffers( 0, 1, &g_pVertexBuffer, &stride, &offset );
原始拓扑是指GPU如何获得渲染三角形所需的三个顶点。 我们在上面讨论过,为了渲染单个三角形,应用程序需要向GPU发送三个顶点。 因此,顶点缓冲区中有三个顶点。 如果我们想渲染两个三角形怎么办? 一种方法是将6个顶点发送到GPU。 前三个顶点定义第一个三角形,后三个顶点定义第二个三角形。 此拓扑称为三角形列表。 三角形列表具有易于理解的优点,但在某些情况下它们效率非常低。 当连续渲染的三角形共享顶点时会出现这种情况。 例如,图3a左侧显示了由两个三角形组成的正方形:ABC和CB D.(按照惯例,三角形通常通过按顺时针顺序列出它们的顶点来定义。)如果我们使用三角形列表将这两个三角形发送到GPU ,我们的顶点缓冲区会这样:
A B C C B D
请注意,B和C在顶点缓冲区中出现两次,因为它们由两个三角形共享。
图3a包含一个由两个三角形组成的正方形; 图3b包含由三个三角形组成的五边形。
如果我们可以告诉GPU在渲染第二个三角形时,我们可以使顶点缓冲区更小,而不是从顶点缓冲区获取所有三个顶点,使用前一个三角形中的2个顶点,并从顶点缓冲区中仅获取1个顶点。 事实证明,这是由Direct3D支持的,拓扑结构称为三角形条带。 渲染三角形条带时,第一个三角形由顶点缓冲区中的前三个顶点定义。 下一个三角形由前一个三角形的最后两个顶点加上顶点缓冲区中的下一个顶点定义。 以图3a中的方块为例,使用三角形条带,顶点缓冲区看起来像:
A B C D
前三个顶点A B C定义第一个三角形。 第二个三角形由B和C定义,即第一个三角形的最后两个顶点加上D.因此,通过使用三角形条带拓扑,顶点缓冲区大小从6个顶点变为4个顶点。 类似地,对于三个三角形,例如图3b中的三角形,使用三角形列表将需要顶点缓冲区,例如:
A B C C B D C D E
使用三角形条带,顶点缓冲区的大小显着减少:
A B C D E
你可能已经注意到,在三角形条带示例中,第二个三角形定义为B C D.这三个顶点不形成顺时针顺序。 这是使用三角形条带的自然现象。 为了克服这个问题,GPU会自动交换来自前一个三角形的两个顶点的顺序。 它只对第二个三角形,第四个三角形,第六个三角形,第八个三角形等执行此操作。 这确保每个三角形由顶点以正确的缠绕顺序(在这种情况下为顺时针方向)定义。 除了三角形列表和三角形条带外,Direct3D 11还支持许多其他类型的原始拓扑。 我们不会在本教程中讨论它们。
在我们的代码中,我们有一个三角形,所以我们指定的并不重要。 但是,我们必须指定一些内容,因此我们选择了三角形列表。
// Set primitive topology
g_pImmediateContext->IASetPrimitiveTopology( D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST );
缺少的最后一项是执行三角形实际渲染的代码。我们创建了两个用于渲染的着色器,顶点着色器和像素着色器。顶点着色器负责将三角形的各个顶点转换为正确的位置。像素着色器负责计算三角形的每个像素的最终输出颜色。这将在下一个教程中详细介绍。要使用这些着色器,我们必须分别调用ID3D11DeviceContext :: VSSetShader()和ID3D11DeviceContext :: PSSetShader()。我们做的最后一件事是调用ID3D11DeviceContext :: Draw(),它命令GPU使用当前顶点缓冲区,顶点布局和原始拓扑进行渲染。 Draw()的第一个参数是要发送到GPU的顶点数,第二个参数是要开始发送的第一个顶点的索引。因为我们渲染一个三角形并且我们从顶点缓冲区的开头渲染,所以我们分别使用3和0作为两个参数。整个三角形渲染代码如下所示:
// Render a triangle
g_pImmediateContext->VSSetShader( g_pVertexShader, NULL, 0 );
g_pImmediateContext->PSSetShader( g_pPixelShader, NULL, 0 );
g_pImmediateContext->Draw( 3, 0 );