前言
这一章将了解如何在DirectX 11利用硬件实例化技术高效地绘制重复的物体,以及使用视锥体裁剪技术提前将位于视锥体外的物体进行排除。
在此之前需要额外了解的章节如下:
章节回顾 |
---|
18 使用DirectXCollision库进行碰撞检测 |
19 模型加载:obj格式的读取及使用二进制文件提升读取效率 |
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 可以一起探讨DX11,以及有什么问题也可以在这里汇报。
硬件实例化(Hardware Instancing)
硬件实例化指的是在场景中绘制同一个物体多次,但是是以不同的位置、旋转、缩放、材质以及纹理来绘制(比如一棵树可能会被多次使用以构建出一片森林)。在以前,每次实例绘制(Draw方法)都会引发一次顶点缓冲区和索引缓冲区经过输入装配阶段传递进渲染管线中,大量重复的绘制则意味着多次反复的输入装配操作,会引发十分庞大的性能开销。事实上在绘制同样物体的时候顶点缓冲区和索引缓冲区应当只需要传递一次,然后真正需要多次传递的也应该是像世界矩阵、材质、纹理等这些可能会经常变化的数据。
要能够实现上面的这种操作,还需要图形库底层API本身能够支持按对象绘制。对于每个对象,我们必须设置它们各自的材质、世界矩阵等,然后才是调用绘制命令。尽管在Direct3D 10和后续的版本已经将原本Direct3D 9的一些API重新设计以尽可能最小化性能上的开销,部分多余的开销仍然存在。因此,Direct3D提供了一种机制,不需要通过API上的额外性能开销来实现实例化,我们称之为硬件实例化。
为什么要担忧API性能开销呢?Direct3D 9应用程序通常因为API导致在CPU上遇到瓶颈,而不是在GPU。以前关卡设计师喜欢使用单一材质和纹理来绘制许多对象,因为对于它们来说需要经常去单独改变它的状态并且去调用绘制。场景将会被限制在几千次的调用绘制以维持实时渲染的速度,主要在于这里的每次API调用都会引起高级别的CPU性能开销。现在图形引擎可以使用批处理技术以最小化绘制调用的次数。硬件实例化是API帮助执行批处理的一个方面。
顶点着色器
硬件实例化需要在输入装配阶段额外提供以二进制数据流表示的实例数据才能工作,而不仅仅是提供顶点/索引数据。然后我们将通过调用对应的Draw命令来告诉硬件需要绘制这个网格模型多少次,即绘制多少个这样的实例。对应顶点着色器来说,可以同时接受来自顶点信息和实例信息的数据作为输入:
struct InstancePosNormalTex
{
float3 PosL : POSITION;
float3 NormalL : NORMAL;
float2 Tex : TEXCOORD;
matrix World : World;
matrix WorldInvTranspose : WorldInvTranspose;
};
其中前面三项数据来自顶点,后面两项数据则是来自一个实例,因为对于一个实例来说,在绘制的时候它的世界矩阵是不会发生变化的。
输出的结构体和以前一样:
struct VertexPosHWNormalTex
{
float4 PosH : SV_POSITION;
float3 PosW : POSITION; // 在世界中的位置
float3 NormalW : NORMAL; // 法向量在世界中的方向
float2 Tex : TEXCOORD;
};
顶点着色器代码变化如下:
VertexPosHWNormalTex VS(InstancePosNormalTex pIn)
{
VertexPosHWNormalTex pOut;
matrix viewProj = mul(gView, gProj);
vector posW = mul(float4(pIn.PosL, 1.0f), pIn.World);
pOut.PosW = posW.xyz;
pOut.PosH = mul(posW, viewProj);
pOut.NormalW = mul(pIn.NormalL, (float3x3) pIn.WorldInvTranspose);
pOut.Tex = pIn.Tex;
return pOut;
}
至于像素着色器,和上一章为模型所使用的着色器的保持一致。
实例ID
系统值SV_InstanceID
可以告诉我们当前进行绘制的顶点来自哪个实例。通常在绘制N个实例的情况下,第一个实例的索引值为0,一直到最后一个实例索引值为N - 1.它可以应用在需要个性化的地方,比如使用一个纹理数组,然后不同的索引去映射到对应的纹理,以绘制出网格模型相同,但纹理不一致的物体。
流式实例化数据
和之前顶点着色器的做法一样,我们需要使用D3D11_INPUT_ELEMENT_DESC
来描述实例的字节流对应的元素信息:
typedef struct D3D11_INPUT_ELEMENT_DESC
{
LPCSTR SemanticName; // 语义名
UINT SemanticIndex; // 语义名对应的索引值
DXGI_FORMAT Format; // DXGI数据格式
UINT InputSlot; // 输入槽
UINT AlignedByteOffset; // 对齐的字节偏移量
D3D11_INPUT_CLASSIFICATION InputSlotClass; // 输入槽类别(顶点/实例)
UINT InstanceDataStepRate; // 实例数据步进值
} D3D11_INPUT_ELEMENT_DESC;
最后两个成员与实例所有联系:
1.InputSlotClass
:指定输入的元素是作为顶点元素还是实例元素。枚举值含义如下:
枚举值 | 含义 |
---|---|
D3D11_INPUT_PER_VERTEX_DATA | 作为顶点元素 |
D3D11_INPUT_PER_INSTANCE_DATA | 作为实例元素 |
2.InstanceDataStepRate
:指定每份实例数据绘制出多少个实例。例如,假如你想绘制6个实例,但提供了只够绘制3个实例的数据,1份实例数据绘制出1种颜色,分别为红、绿、蓝。那么我们可以设置该成员的值为2,使得前两个实例绘制成红色,中间两个实例绘制成绿色,后两个实例绘制成蓝色。通常在绘制实例的时候我们会将该成员的值设为1,保证1份数据绘制出1个实例。对于顶点成员来说,设置该成员的值为0.
对于前面的结构体InstancePosNormalTex
,与之对应的输入成员描述数组如下:
D3D11_INPUT_ELEMENT_DESC basicInstLayout[] = {
{ "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,