1. 顶点与输入布局
在 DirectX12 3D 中,顶点是构建几何体的基本单元,它不仅包含了空间位置信息,还可以存储颜色、法线、纹理坐标等其他属性数据。通过定义不同的顶点结构体,可以创建出满足各种需求的顶点格式。例如,以下结构体定义了一个包含位置和颜色信息的顶点:
struct Vertex
{
XMFLOAT3 Pos;
XMFLOAT4 Color;
};
在这个结构体中,XMFLOAT3类型的Pos成员表示顶点的三维位置,XMFLOAT4类型的Color成员表示顶点的颜色,其中包含了红、绿、蓝和透明度四个分量。
输入布局描述则用于让 Direct3D 了解如何处理所定义顶点结构体的成员。它由D3D12_INPUT_LAYOUT_DESC结构体表示,其中包含一个D3D12_INPUT_ELEMENT_DESC元素构成的数组以及该数组的元素数量。D3D12_INPUT_ELEMENT_DESC结构体的每个元素依次描述了顶点结构体中对应的成员,其定义如下:
typedef struct D3D12_INPUT_ELEMENT_DESC
{
LPCSTR SemanticName;
UINT SemanticIndex;
DXGI_FORMAT Format;
UINT InputSlot;
UINT AlignedByteOffset;
D3D12_INPUT_CLASSIFICATION InputSlotClass;
UINT InstanceDataStepRate;
} D3D12_INPUT_ELEMENT_DESC;
SemanticName:一个与元素相关联的特定字符串,称为语义,它传达了元素的预期用途。例如,“POSITION” 表示该元素是顶点的位置信息,“COLOR” 表示颜色信息。通过语义,可以将顶点结构体中的元素与顶点着色器输入签名中的元素一一映射起来。
SemanticIndex:附加到语义上的索引。当存在多个相同语义名时,索引可以用于区分,例如 “TEXCOORD0” 和 “TEXCOORD1”,其中无索引时默认为 0。
Format:通过枚举类型DXGI_FORMAT中的成员来指定顶点元素的格式,即数据类型。常用的格式有DXGI_FORMAT_R32_FLOAT(1D 32 位浮点标量)、DXGI_FORMAT_R32G32_FLOAT(2D 32 位浮点向量)、DXGI_FORMAT_R32G32B32_FLOAT(3D 32 位浮点向量)等。
InputSlot:指定传递元素所用的输入槽索引。Direct3D 共支持 16 个输入槽(索引值为 0 - 15),可以通过它们向输入装配阶段传递顶点数据。通常情况下,简单的几何体绘制只会用到输入槽 0。
AlignedByteOffset:在特定输入槽中,从 C++ 顶点结构体的首地址到其中某元素起始地址的偏移量(用字节表示)。例如,在上述Vertex结构体中,Pos成员的偏移量为 0 字节,Color成员的偏移量为 12 字节(因为XMFLOAT3占用 12 字节)。
InputSlotClass:指定输入元素的分类,通常使用D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA,表示每个顶点都有独立的数据。另一个选项D3D12_INPUT_CLASSIFICATION_PER_INSTANCE_DATA用于实现实例化这种高级技术。
InstanceDataStepRate:目前通常将此值指定为 0。若要采用实例化技术,则将此参数设为 1。
对于上述定义的Vertex结构体,相应的输入布局描述可以如下设置:
std::vector<D3D12_INPUT_ELEMENT_DESC> mInputLayout =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 },
{ "COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12, D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0 }
};
输入布局与顶点着色器密切相关。顶点着色器的输入签名必须与输入布局描述相匹配,这样才能正确地接收和处理顶点数据。顶点着色器通过语义名将顶点结构体中的元素与自身签名中的元素进行映射,从而对顶点数据进行各种变换和计算。例如,顶点着色器可以根据顶点的位置信息进行坐标变换,将其从局部空间转换到世界空间、观察空间和裁剪空间等;根据颜色信息进行光照计算,确定顶点的最终颜色。
2. 顶点缓冲区与索引缓冲区
顶点缓冲区是用于存储顶点数据的缓冲区对象,它是 Direct3D 中实现高效渲染的关键组件之一。在 Direct3D 中,顶点缓冲区以ID3D12Buffer接口的形式存在。要创建一个顶点缓冲区对象,首先需要定义一个D3D12_BUFFER_DESC结构体,并设置好相应的参数,如缓冲区的大小、使用方法、绑定标志等。然后使用ID3D12Device接口的CreateBuffer方法来创建顶点缓冲区对象。以下是创建顶点缓冲区的示例代码:
// 假设已经定义了顶点结构体Vertex和顶点数量vertexCount
UINT vertexBufferSize = vertexCount * sizeof(Vertex);
// 创建缓冲区描述结构体
D3D12_BUFFER_DESC bufferDesc = {};
bufferDesc.ByteWidth = vertexBufferSize;
bufferDesc.Usage = D3D12_USAGE_DEFAULT;
bufferDesc.BindFlags = D3D12_BIND_VERTEX_BUFFER;
bufferDesc.CPUAccessFlags = 0;
bufferDesc.MiscFlags = 0;
bufferDesc.StructureByteStride = 0;
// 创建顶点缓冲区对象
ComPtr<ID3D12Buffer> vertexBuffer;
device->CreateBuffer(&bufferDesc, nullptr, vertexBuffer.GetAddressOf());
在上述代码中,ByteWidth指定了缓冲区的大小,即所有顶点数据的总字节数;Usage设置为D3D12_USAGE_DEFAULT,表示默认的使用方式,适用于大多数静态几何体的渲染;BindFlags设置为D3D12_BIND_VERTEX_BUFFER,表明该缓冲区将用于绑定顶点数据;CPUAccessFlags设置为 0,表示 CPU 不直接访问该缓冲区,这样可以提高 GPU 的访问效率;MiscFlags设置为 0,表示没有其他特殊标志;StructureByteStride设置为 0,表示结构体的步长由系统自动计算。
顶点缓冲区的作用是将顶点数据存储在显存中,以便 GPU 能够快速访问和处理。在渲染过程中,顶点缓冲区中的顶点数据会被传递到渲染流水线的输入装配阶段,用于构建几何图元(如三角形、线段等)。通过将顶点数据存储在缓冲区中,可以减少对内存的频繁访问,提高渲染效率。
索引缓冲区则用于存储顶点的索引数据,它与顶点缓冲区密切配合,用于确定三角形、线段或点的连接方式,从而构建更复杂的图形形状。索引缓冲区同样以ID3D12Buffer接口的形式存在,创建索引缓冲区的过程与顶点缓冲区类似,需要定义D3D12_BUFFER_DESC结构体并设置相应参数,然后使用CreateBuffer方法创建。以下是创建索引缓冲区的示例代码:
// 假设已经定义了索引数组indices和索引数量indexCount
UINT indexBufferSize = indexCount * sizeof(UINT);
// 创建缓冲区描述结构体
D3D12_BUFFER_DESC indexBufferDesc = {};
indexBufferDesc.ByteWidth = indexBufferSize;
indexBufferDesc.Usage = D3D12_USAGE_DEFAULT;
indexBufferDesc.BindFlags = D3D12_BIND_INDEX_BUFFER;
indexBufferDesc.CPUAccessFlags = 0;
indexBufferDesc.MiscFlags = 0;
indexBufferDesc.StructureByteStride = 0;
// 创建索引缓冲区对象
ComPtr<ID3D12Buffer> indexBuffer;
device->CreateBuffer(&indexBufferDesc, nullptr, indexBuffer.GetAddressOf());
// 将索引数据复制到索引缓冲区
UINT8* indexDataBegin;
CD3DX12_RANGE readRange(0, 0);
indexBuffer->Map(0, &readRange, reinterpret_cast<void**>(&indexDataBegin));
memcpy(indexDataBegin, indices, sizeof(indices[0]) * indexCount);
indexBuffer->Unmap(0, nullptr);
在上述代码中,BindFlags设置为D3D12_BIND_INDEX_BUFFER,表明该缓冲区将用于绑定索引数据。创建索引缓冲区后,需要将索引数据复制到缓冲区中。通过Map方法获取缓冲区的映射指针,使用memcpy函数将索引数据复制到该指针指向的内存区域,最后使用Unmap方法取消映射。
索引缓冲区的主要作用是避免重复存储和传输相同的顶点数据,提高内存利用率和渲染性能。在渲染时,GPU 可以根据索引缓冲区中的索引值,快速从顶点缓冲区中获取相应的顶点数据,从而构建出复杂的几何图形。例如,对于一个由多个三角形组成的网格模型,如果每个三角形都单独存储其三个顶点的位置信息,会导致大量的重复数据。而使用索引缓冲区,只需存储一次所有顶点的数据,然后通过索引来指定每个三角形的顶点连接方式,大大减少了数据量和传输带宽。
在将顶点缓冲区和索引缓冲区绑定到渲染流水线时,需要使用ID3D12GraphicsCommandList接口的相关方法。对于顶点缓冲区,使用IASetVertexBuffers方法,示例代码如下:
UINT stride = sizeof(Vertex);
UINT offset = 0;
commandList->IASetVertexBuffers(0, 1, &vertexBufferView, stride, offset);
其中,0表示起始输入槽,1表示要绑定的顶点缓冲区数量,&vertexBufferView是指向顶点缓冲区视图的指针,stride表示每个顶点的字节大小,offset表示偏移量。
对于索引缓冲区,使用IASetIndexBuffer方法,示例代码如下:
D3D12_INDEX_BUFFER_VIEW indexBufferView;
indexBufferView.BufferLocation = indexBuffer->GetGPUVirtualAddress();
indexBufferView.Format = DXGI_FORMAT_R32_UINT;
indexBufferView.SizeInBytes = indexBufferSize;
commandList->IASetIndexBuffer(&indexBufferView);
其中,BufferLocation指定索引缓冲区的 GPU 虚拟地址,Format指定索引数据的格式,SizeInBytes指定索引缓冲区的大小。通过这些方法,将顶点缓冲区和索引缓冲区成功绑定到渲染流水线的输入装配阶段,为后续的几何图形绘制做好准备。
3. 顶点着色器与像素着色器
顶点着色器是一段在 GPU 上运行的小程序,其主要功能是对顶点数据进行处理和变换。它接收来自顶点缓冲区的顶点数据,根据预设的算法对顶点的位置、颜色、法线等属性进行计算和转换,然后将处理后的顶点数据输出到渲染流水线的下一阶段。顶点着色器的一个重要任务是将顶点从局部空间转换到世界空间、观察空间和裁剪空间,以便后续的渲染操作能够正确进行。例如,通过矩阵变换,可以将顶点的局部坐标转换为世界坐标,再结合摄像机的位置和方向,将世界坐标转换为观察坐标,最后通过投影矩阵将观察坐标转换为裁剪坐标。在这个过程中,顶点着色器还可以对顶点的颜色进行光照计算,考虑光源的位置、强度和方向等因素,计算出每个顶点在当前光照条件下的最终颜色。
以下是一个使用高级着色器语言(HLSL)编写的简单顶点着色器示例:
struct VS_INPUT
{
float3 PosL : POSITION;
float4 Color : COLOR;
};
struct VS_OUTPUT
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
matrix WorldViewProj;
VS_OUTPUT main(VS_INPUT input)
{
VS_OUTPUT output;
output.PosH = mul(float4(input.PosL, 1.0f), WorldViewProj);
output.Color = input.Color;
return output;
}
在这个顶点着色器中,VS_INPUT结构体定义了输入数据,包括顶点的位置PosL和颜色Color,其中POSITION和COLOR是语义,用于标识数据的含义。VS_OUTPUT结构体定义了输出数据,包括裁剪空间中的位置PosH和颜色Color,SV_POSITION是一个特殊的语义,表示输出的是裁剪空间中的位置,这是 GPU 进行后续渲染操作所必需的。WorldViewProj是一个全局矩阵变量,用于存储世界、观察和投影矩阵的组合,通过mul函数将顶点位置与该矩阵相乘,实现顶点坐标的变换。
像素着色器同样是在 GPU 上运行的小程序,它的主要功能是对每个像素进行着色处理,计算出最终显示在屏幕上的像素颜色。像素着色器接收来自顶点着色器输出的经过光栅化后的片段数据,这些片段数据包含了像素的位置、颜色、纹理坐标等信息。像素着色器根据这些信息,结合纹理、光照、材质等因素,对每个像素进行详细的颜色计算。例如,它可以从纹理中采样获取纹理颜色,根据光照模型计算光照效果,再结合材质属性,最终确定每个像素的颜色。
以下是一个简单的像素着色器 HLSL 代码示例:
struct PS_INPUT
{
float4 PosH : SV_POSITION;
float4 Color : COLOR;
};
float4 main(PS_INPUT input) : SV_TARGET
{
return input.Color;
}
在这个像素着色器中,PS_INPUT结构体定义了输入数据,包含像素在裁剪空间中的位置PosH和颜色Color。main函数是像素着色器的入口点,它接收输入数据,并将颜色直接返回作为输出,这里简单地将顶点着色器传递过来的颜色作为像素的最终颜色。在实际应用中,像素着色器会进行更复杂的计算,如纹理采样、光照计算等。
顶点着色器和像素着色器在渲染流水线中起着关键作用。顶点着色器负责对顶点数据进行初步处理和变换,为后续的渲染操作奠定基础;像素着色器则专注于对每个像素进行精细的颜色计算,最终决定了屏幕上每个像素的显示效果。它们相互协作,共同完成了从 3D 模型到 2D 图像的转换过程,是实现高质量 3D 图形渲染的核心组件。
4. 常量缓冲区与根签名
常量缓冲区是一种特殊的缓冲区,用于存储着色器程序所需要的常量数据,如矩阵变换信息、光照参数、材质属性等。这些常量数据通常由 CPU 在每一帧更新,并传递给 GPU 供着色器使用。与顶点缓冲区和索引缓冲区不同,常量缓冲区的数据更新频率相对较低,一般每帧更新一次。常量缓冲区在 DirectX12 中通过ID3D12Resource接口来表示,创建常量缓冲区时,通常将其创建在上传堆中,因为 CPU 需要频繁地更新其数据。例如:
// 定义常量缓冲区结构体
struct ObjectConstants
{
DirectX::XMFLOAT4X4 WorldViewProj;
};
// 计算常量缓冲区的大小,必须为硬件最小分配空间(256B)的整数倍
UINT mElementByteSize = d3dUtil::CalcConstantBufferByteSize(sizeof(ObjectConstants));
// 创建常量缓冲区资源
ComPtr<ID3D12Resource> mUploadCBuffer;
device->CreateCommittedResource(
&CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_UPLOAD),
D3D12_HEAP_FLAG_NONE,
&CD3DX12_RESOURCE_DESC::Buffer(mElementByteSize),
D3D12_RESOURCE_STATE_GENERIC_READ,
nullptr,
IID_PPV_ARGS(mUploadCBuffer.GetAddressOf()));
在上述代码中,首先定义了一个包含世界视图投影矩阵的常量缓冲区结构体ObjectConstants。然后通过d3dUtil::CalcConstantBufferByteSize函数计算出常量缓冲区的大小,确保其为 256 字节的整数倍。接着使用CreateCommittedResource方法创建常量缓冲区资源,将其创建在上传堆中,并设置为通用可读状态。
当需要更新常量缓冲区的数据时,首先需要映射缓冲区,获取指向其内存的指针,然后将新的数据复制到该内存区域,最后取消映射。例如:
// 映射常量缓冲区
BYTE* mMappedData = nullptr;
mUploadCBuffer->Map(0, nullptr, reinterpret_cast<void**>(&mMappedData));
// 复制数据到常量缓冲区
ObjectConstants data;
// 假设已经计算好了新的世界视图投影矩阵
data.WorldViewProj = CalculateWorldViewProjMatrix();
memcpy(mMappedData, &data, sizeof(data));
// 取消映射常量缓冲区
mUploadCBuffer->Unmap(0, nullptr);
根签名是 DirectX12 中一个重要的概念,它定义了着色器程序期望的资源输入方式和布局。可以将根签名理解为 GPU 渲染管线的 “参数声明”,它描述了常量、常量缓冲区、资源(如纹理)、无序访问缓冲、采样器等资源在 GPU 寄存器中的存储规划。根签名的作用是告诉 GPU 渲染管线需要哪些输入数据,以及这些数据在内存和显存中的布局方式,从而使 GPU 能够正确地访问和使用这些数据。根签名由ID3D12RootSignature接口表示,创建根签名时,需要定义一系列的根参数,这些根参数描述了不同类型的资源。例如,以下代码展示了如何创建一个包含常量缓冲区的根签名:
// 创建常量缓冲区视图描述
D3D12_CONSTANT_BUFFER_VIEW_DESC cbvDesc;
cbvDesc.BufferLocation = mUploadCBuffer->GetGPUVirtualAddress();
cbv
//在DirectX12 3D中,绘制几何体主要通过`ID3D12GraphicsCommandList`接口提供的方法来实现。对于非索引描述的几何体,即直接以顶点数据来绘制的几何体,常用`DrawInstanced`方法。该方法的定义如下:
void DrawInstanced(
UINT VertexCountPerInstance,
UINT InstanceCount,
UINT StartVertexLocation,
UINT StartInstanceLocation
);
VertexCountPerInstance:指定每个实例要绘制的顶点数量。例如,若要绘制一个由三角形组成的平面,每个三角形需要 3 个顶点,那么对于一个平面实例,此值通常为 3。
InstanceCount:用于实现实例化技术,指定要绘制的实例数量。如果只需绘制一个几何体实例,此值设为 1;若要绘制多个相同的几何体实例,如一片树林中的众多树木,可将此值设置为树木的数量。
StartVertexLocation:指定顶点缓冲区内第一个被绘制顶点的索引。当顶点缓冲区中存储了多个几何体的顶点数据时,通过此参数可以确定从哪个顶点开始绘制当前几何体。
StartInstanceLocation:同样用于实例化,一般情况下,若不涉及复杂的实例化操作,可将其设置为 0。
以下是使用DrawInstanced方法绘制一个简单三角形的示例代码:
// 假设已经创建好顶点缓冲区和相关资源
// 设置顶点缓冲区视图
UINT stride = sizeof(Vertex);
UINT offset = 0;
commandList->IASetVertexBuffers(0, 1, &vertexBufferView, stride, offset);
// 设置图元拓扑为三角形列表
commandList->IASetPrimitiveTopology(D3D12_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// 调用DrawInstanced方法绘制三角形
commandList->DrawInstanced(3, 1, 0, 0);
在上述代码中,首先设置了顶点缓冲区视图,将顶点缓冲区绑定到渲染流水线的输入装配阶段。然后设置图元拓扑为三角形列表,表示将以三角形的方式来绘制顶点数据。最后调用DrawInstanced方法,指定每个实例绘制 3 个顶点(即一个三角形),绘制 1 个实例,从顶点缓冲区的第 0 个顶点开始绘制。
对于以索引描述的几何体,即通过索引来确定顶点连接方式的几何体,常用DrawIndexedInstanced方法。该方法的定义如下:
void DrawIndexedInstanced(
UINT IndexCountPerInstance,
UINT InstanceCount,
UINT StartIndexLocation,
INT BaseVertexLocation,
UINT StartInstanceLocation
);
IndexCountPerInstance:指定每个实例要绘制的索引数量。由于每个三角形需要 3 个索引来确定其三个顶点的连接方式,所以对于一个三角形实例,此值通常为 3。
InstanceCount:与DrawInstanced方法中的含义相同,指定要绘制的实例数量。
StartIndexLocation:指定索引缓冲区内第一个被绘制索引的位置。当索引缓冲区中存储了多个几何体的索引数据时,通过此参数可以确定从哪个索引开始绘制当前几何体。
BaseVertexLocation:指定在顶点缓冲区中,相对于起始顶点的偏移量。这在处理多个几何体共享顶点缓冲区时非常有用,通过此偏移量可以正确地从顶点缓冲区中获取对应的顶点数据。
StartInstanceLocation:同样用于实例化,一般情况下,若不涉及复杂的实例化操作,可将其设置为 0。
以下是使用DrawIndexedInstanced方法绘制一个由多个三角形组成的立方体的示例代码:
// 假设已经创建好顶点缓冲区、索引缓冲区和相关资源
// 设置顶点缓冲区视图
UINT stride = sizeof(Vertex);
UINT offset = 0;
commandList->IASetVertexBuffers(0, 1, &vertexBufferView, stride, offset);
// 设置索引缓冲区视图
D3D12_INDEX_BUFFER_VIEW indexBufferView;
indexBufferView.BufferLocation = indexBuffer->GetGPUVirtualAddress();
indexBufferView.Format = DXGI_FORMAT_R32_UINT;
indexBufferView.SizeInBytes = indexBufferSize;
commandList->IASetIndexBuffer(&indexBufferView);
// 设置图元拓扑为三角形列表
commandList->IASetPrimitiveTopology(D3D12_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
// 调用DrawIndexedInstanced方法绘制立方体
commandList->DrawIndexedInstanced(cubeIndexCount, 1, 0, 0, 0);
在上述代码中,首先设置了顶点缓冲区视图和索引缓冲区视图,将顶点缓冲区和索引缓冲区分别绑定到渲染流水线的输入装配阶段。然后设置图元拓扑为三角形列表。最后调用DrawIndexedInstanced方法,指定每个实例绘制cubeIndexCount个索引(即组成立方体所需的索引数量),绘制 1 个实例,从索引缓冲区的第 0 个索引开始绘制,顶点缓冲区的偏移量为 0。