这一章将了解如何在DirectX 11利用硬件实例化技术高效地绘制重复的物体,以及使用视锥体裁剪技术提早将位于视锥体外的物体进行排除。html
在此以前须要额外了解的章节以下:git
章节回顾 |
---|
18 使用DirectXCollision库进行碰撞检测 |
19 模型加载:obj格式的读取及使用二进制文件提高读取效率 |
DirectX11 With Windows SDK完整目录github
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。app
硬件实例化指的是在场景中绘制同一个物体屡次,可是是以不一样的位置、旋转、缩放、材质以及纹理来绘制(好比一棵树可能会被屡次使用以构建出一片森林)。在之前,每次实例绘制(Draw方法)都会引起一次顶点缓冲区和索引缓冲区通过输入装配阶段传递进渲染管线中,大量重复的绘制则意味着屡次反复的输入装配操做,会引起十分庞大的性能开销。事实上在绘制一样物体的时候顶点缓冲区和索引缓冲区应当只须要传递一次,而后真正须要屡次传递的也应该是像世界矩阵、材质、纹理等这些可能会常常变化的数据。dom
要可以实现上面的这种操做,还须要图形库底层API自己可以支持按对象绘制。对于每一个对象,咱们必须设置它们各自的材质、世界矩阵等,而后才是调用绘制命令。尽管在Direct3D 10和后续的版本已经将本来Direct3D 9的一些API从新设计以尽量最小化性能上的开销,部分多余的开销仍然存在。所以,Direct3D提供了一种机制,不须要经过API上的额外性能开销来实现实例化,咱们称之为硬件实例化。ide
为何要担心API性能开销呢?Direct3D 9应用程序一般由于API致使在CPU上遇到瓶颈,而不是在GPU。之前关卡设计师喜欢使用单一材质和纹理来绘制许多对象,由于对于它们来讲须要常常去单独改变它的状态而且去调用绘制。场景将会被限制在几千次的调用绘制以维持实时渲染的速度,主要在于这里的每次API调用都会引发高级别的CPU性能开销。如今图形引擎可使用批处理技术以最小化绘制调用的次数。硬件实例化是API帮助执行批处理的一个方面。布局
以前咱们提到,在输入装配阶段中提供了16个输入槽,这意味着能够同时绑定16个顶点缓冲区做为输入。那这时候若是咱们使用多个顶点缓冲区做为输入会产生什么样的结果呢?性能
这里作一个铺垫,之前咱们在输入装配阶段只使用了1个输入槽。如今假定咱们有以下顶点缓冲区结构:设计
索引 | 顶点位置 | 顶点法向量 | 顶点颜色 |
---|---|---|---|
0 | P1 | N1 | 红 |
1 | P2 | N2 | 绿 |
2 | P3 | N3 | 蓝 |
这里咱们也可使用2个输入槽,第一个顶点缓冲区存放顶点位置,第二个顶点缓冲区存放顶点法向量和顶点颜色:
索引 | 顶点位置 |
---|---|
0 | P1 |
1 | P2 |
2 | P3 |
索引 | 顶点法向量 | 顶点颜色 |
---|---|---|
0 | N1 | 红 |
1 | N2 | 绿 |
2 | N3 | 蓝 |
先回顾一下顶点输入布局描述的结构:
typedef struct D3D11_INPUT_ELEMENT_DESC { LPCSTR SemanticName; // 语义名 UINT SemanticIndex; // 语义名对应的索引值 DXGI_FORMAT Format; // DXGI数据格式 UINT InputSlot; // 输入槽 UINT AlignedByteOffset; // 对齐的字节偏移量 D3D11_INPUT_CLASSIFICATION InputSlotClass; // 输入槽类别(此时为顶点) UINT InstanceDataStepRate; // 忽略(0) } D3D11_INPUT_ELEMENT_DESC;
而后咱们就能够在输入布局中这样指定(这里先简单说起一下):
语义 | 语义索引 | 数据格式 | 输入槽 | 该输入槽对应的字节偏移 |
---|---|---|---|---|
POSITION | 0 | R32G32B32_FLOAT | 0 | 0 |
NORMAL | 0 | R32G32B32_FLOAT | 1 | 0 |
COLOR | 0 | R32G32B32A32_FLOAT | 1 | 12 |
这样,下面在HLSL的结构体数据实际上来源于两个输入槽:
struct VertexPosNormalColor { float3 pos : POSITION; // 来自输入槽0 float3 normal : NORMAL; // 来自输入槽1 float4 color : COLOR; // 来自输入槽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.
在前面的例子,咱们知道一个结构体的数据能够来自多个输入槽,如今要使用硬件实例化,咱们须要使用至少两个输入槽(其中至少一个顶点缓冲区,至少一个实例缓冲区)
如今咱们须要使用的顶点与实例数据组合的结构体以下:
struct InstancePosNormalTex { float3 PosL : POSITION; // 来自输入槽0 float3 NormalL : NORMAL; // 来自输入槽0 float2 Tex : TEXCOORD; // 来自输入槽0 matrix World : World; // 来自输入槽1 matrix WorldInvTranspose : WorldInvTranspose; // 来自输入槽1 };
输出的结构体和之前同样:
struct VertexPosHWNormalTex { float4 PosH : SV_POSITION; float3 PosW : POSITION; // 在世界中的位置 float3 NormalW : NORMAL; // 法向量在世界中的方向 float2 Tex : TEXCOORD; };
如今顶点着色器代码变化以下:
VertexPosHWNormalTex VS(InstancePosNormalTex vIn) { VertexPosHWNormalTex vOut; matrix viewProj = mul(g_View, g_Proj); vector posW = mul(float4(vIn.PosL, 1.0f), vIn.World); vOut.PosW = posW.xyz; vOut.PosH = mul(posW, viewProj); vOut.NormalW = mul(vIn.NormalL, (float3x3) vIn.WorldInvTranspose); vOut.Tex = vIn.Tex; return vOut; }
至于像素着色器,和上一章为模型所使用的着色器的保持一致。
对于前面的结构体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, 0 }, { "World", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D11_INPUT_PER_INSTANCE_DATA, 1}, { "World", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 16, D3D11_INPUT_PER_INSTANCE_DATA, 1}, { "World", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 32, D3D11_INPUT_PER_INSTANCE_DATA, 1}, { "World", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 48, D3D11_INPUT_PER_INSTANCE_DATA, 1}, { "WorldInvTranspose", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 64, D3D11_INPUT_PER_INSTANCE_DATA, 1}, { "WorldInvTranspose", 1, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 80, D3D11_INPUT_PER_INSTANCE_DATA, 1}, { "WorldInvTranspose", 2, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 96, D3D11_INPUT_PER_INSTANCE_DATA, 1}, { "WorldInvTranspose", 3, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 112, D3D11_INPUT_PER_INSTANCE_DATA, 1} };
由于DXGI_FORMAT
一次最多仅可以表达128位(16字节)数据,在对应矩阵的语义时,须要重复描述4次,区别在于语义索引为0-3.
顶点的数据占用输入槽0,而实例数据占用的则是输入槽1。这样就须要咱们使用两个缓冲区以提供给输入装配阶段。其中第一个做为顶点缓冲区,而第二个做为实例缓冲区以存放有关实例的数据,绑定到输入装配阶段的方法以下:
struct VertexPosNormalTex { DirectX::XMFLOAT3 pos; DirectX::XMFLOAT3 normal; DirectX::XMFLOAT2 tex; static const D3D11_INPUT_ELEMENT_DESC inputLayout[3]; }; struct InstancedData { XMMATRIX world; XMMATRIX worldInvTranspose; }; // ... UINT strides[2] = { sizeof(VertexPosNormalTex), sizeof(InstancedData) }; UINT offsets[2] = { 0, 0 }; ID3D11Buffer * buffers[2] = { vertexBuffer.Get(), instancedBuffer.Get() }; // 设置顶点/索引缓冲区 deviceContext->IASetVertexBuffers(0, 2, buffers, strides, offsets); deviceContext->IASetInputLayout(instancePosNormalTexLayout.Get());
系统值SV_InstanceID
能够告诉咱们当前进行绘制的顶点来自哪一个实例。一般在绘制N个实例的状况下,第一个实例的索引值为0,一直到最后一个实例索引值为N - 1.它能够应用在须要个性化的地方,好比使用一个纹理数组,而后不一样的索引去映射到对应的纹理,以绘制出网格模型相同,但纹理不一致的物体。
一般咱们使用ID3D11DeviceContext::DrawIndexedInstanced
方法来绘制实例数据:
void ID3D11DeviceContext::DrawIndexedInstanced( UINT IndexCountPerInstance, // [In]每一个实例绘制要用到的索引数目 UINT InstanceCount, // [In]绘制的实例数目 UINT StartIndexLocation, // [In]起始索引偏移值 INT BaseVertexLocation, // [In]起始顶点偏移值 UINT StartInstanceLocation // [In]起始实例偏移值 );
下面是一个调用示例:
deviceContext->DrawIndexedInstanced(part.indexCount, numInsts, 0, 0, 0);
若没有索引数组,也能够用ID3D11DeviceContext::DrawInstanced
方法来进行绘制
void ID3D11DeviceContext::DrawInstanced( UINT VertexCountPerInstance, // [In]每一个实例绘制要用到的顶点数目 UINT InstanceCount, // [In]绘制的实例数目 UINT StartVertexLocation, // [In]起始顶点偏移值 UINT StartInstanceLocation // [In]起始实例偏移值 );
在调用实例化绘制后,输入装配器会根据全部顶点输入槽与实例输入槽进行笛卡尔积的排列组合,这里举个复杂的例子,有5个输入槽,其中顶点相关的输入槽含3个元素,实例相关的输入槽含2个元素:
输入槽索引 | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
按顶点/实例 | 顶点 | 顶点 | 顶点 | 实例 | 实例 |
数据类型 | 顶点位置 | 顶点法向量 | 纹理坐标 | 世界矩阵 | 世界矩阵逆转置 |
索引0 | P0 | N0 | T0 | W0 | WInvT0 |
索引1 | P1 | N1 | T1 | W1 | WInvT1 |
索引2 | P2 | N2 | T2 | ------ | ---------- |
最终产生的实例数据流以下表,含3x2=6组结构体数据:
实例ID | 顶点ID | 顶点位置 | 顶点法向量 | 纹理坐标 | 世界矩阵 | 世界矩阵逆转置 |
---|---|---|---|---|---|---|
0 | 0 | P0 | N0 | T0 | W0 | WInv0 |
0 | 1 | P1 | N1 | T1 | W0 | WInv0 |
0 | 2 | P2 | N2 | T2 | W0 | WInv0 |
1 | 0 | P0 | N0 | T0 | W1 | WInv1 |
1 | 1 | P1 | N1 | T1 | W1 | WInv1 |
1 | 2 | P2 | N2 | T2 | W1 | WInv1 |
和以前建立顶点/索引缓冲区的方式同样,咱们须要建立一个ID3D11Buffer
,只不过在缓冲区描述中,咱们须要将其指定为动态缓冲区(即D3D11_BIND_VERTEX_BUFFER
),而且要指定D3D11_CPU_ACCESS_WRITE
。
// 设置实例缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_DYNAMIC; vbd.ByteWidth = count * (UINT)sizeof(InstancedData); vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // 新建实例缓冲区 HR(device->CreateBuffer(&vbd, nullptr, m_pInstancedBuffer.ReleaseAndGetAddressOf()));
由于咱们不须要访问里面的数据,所以不用添加D3D11_CPU_ACCESS_READ
标记。
若须要修改实例缓冲区的内容,则须要使用ID3D11DeviceContext::Map
方法将其映射到CPU内存当中。对于使用了D3D11_USAGE_DYNAMIC
标签的动态缓冲区来讲,在更新的时候只能使用D3D11_MAP_WRITE_DISCARD
标签,而不能使用D3D11_MAP_WRITE
或者D3D11_MAP_READ_WRITE
标签。
将须要提交上去的实例数据存放到映射好的CPU内存区间后,使用ID3D11DeviceContext::Unmap
方法将实例数据更新到显存中以应用。
D3D11_MAPPED_SUBRESOURCE mappedData; HR(deviceContext->Map(m_pInstancedBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData)); InstancedData * iter = reinterpret_cast<InstancedData *>(mappedData.pData); // 省略写入细节... deviceContext->Unmap(m_pInstancedBuffer.Get(), 0);
在前面的全部章节中,顶点的抛弃一般发生在光栅化阶段。这意味着若是一份模型数据的全部顶点在通过矩阵变换后都不会落在屏幕区域内的话,这些顶点数据将会经历顶点着色阶段,可能会通过曲面细分阶段和几何着色阶段,而后在光栅化阶段的时候才抛弃。让这些不会被绘制的顶点还要走过这么漫长的阶段才被抛弃,能够说是一种很是低效的行为。
视锥体裁剪,就是在将这些模型的相关数据提交给渲染管线以前,生成一个包围盒,与摄像机观察空间的视锥体进行碰撞检测。若为相交或者包含,则说明该模型对象是可见的,须要被绘制出来,反之则应当拒绝对该对象的绘制调用,或者不传入该实例对象相关的数据。这样作能够节省GPU资源以免大量对不可见对象的绘制,对CPU的性能开销也不大。
能够说,若一个场景中的模型数目越多,或者视锥体的可视范围越小,那么视锥体裁剪的效益越大。
查看上图,能够知道的是物体A和D没有与视锥体发生碰撞,所以须要排除掉物体A的实例数据。而物体B和E与视锥体有相交,物体C则被视锥体所包含,这三个物体的实例数据都应当传递给实例缓冲区。
视锥体裁剪有三种等价的代码表现形式。须要已知当前物体的包围盒、世界变换矩阵、观察矩阵和投影矩阵。其中投影矩阵自己能够构造出视锥体包围盒。
下面有关视锥体裁剪的方法都放进了Collision.h
中。
如今已知物体的包围盒位于自身的局部坐标系,咱们可使用世界变换矩阵将其变换到世界空间中。一样,由投影矩阵构造出来的视锥体包围盒也位于自身局部坐标系中,而观察矩阵实质上是从世界矩阵变换到视锥体所处的局部坐标系中。所以,咱们可使用观察矩阵的逆矩阵,将视锥体包围盒也变换到世界空间中。这样就好似物体与视锥体都位于世界空间中,能够进行碰撞检测了:
std::vector<XMMATRIX> XM_CALLCONV Collision::FrustumCulling( const std::vector<XMMATRIX>& Matrices,const BoundingBox& localBox, FXMMATRIX View, CXMMATRIX Proj) { std::vector<DirectX::XMMATRIX> acceptedData; BoundingFrustum frustum; BoundingFrustum::CreateFromMatrix(frustum, Proj); XMMATRIX InvView = XMMatrixInverse(nullptr, View); // 将视锥体从局部坐标系变换到世界坐标系中 frustum.Transform(frustum, InvView); BoundingOrientedBox localOrientedBox, orientedBox; BoundingOrientedBox::CreateFromBoundingBox(localOrientedBox, localBox); for (auto& mat : Matrices) { // 将有向包围盒从局部坐标系变换到世界坐标系中 localOrientedBox.Transform(orientedBox, mat); // 相交检测 if (frustum.Intersects(orientedBox)) acceptedData.push_back(mat); } return acceptedData; }
该方法对应的正是龙书中所使用的裁剪方法,基本思路为:分别对观察矩阵和世界变换矩阵求逆,而后使用观察逆矩阵将视锥体从自身坐标系搬移到世界坐标系,再使用世界变换的逆矩阵将其从世界坐标系搬移到物体自身坐标系来与物体进行碰撞检测。改良龙书的碰撞检测代码以下:
std::vector<DirectX::XMMATRIX> XM_CALLCONV Collision::FrustumCulling2( const std::vector<DirectX::XMMATRIX>& Matrices,const DirectX::BoundingBox& localBox, DirectX::FXMMATRIX View, DirectX::CXMMATRIX Proj) { std::vector<DirectX::XMMATRIX> acceptedData; BoundingFrustum frustum, localFrustum; BoundingFrustum::CreateFromMatrix(frustum, Proj); XMMATRIX InvView = XMMatrixInverse(nullptr, View); for (auto& mat : Matrices) { XMMATRIX InvWorld = XMMatrixInverse(nullptr, mat); // 将视锥体从观察坐标系(或局部坐标系)变换到物体所在的局部坐标系中 frustum.Transform(localFrustum, InvView * InvWorld); // 相交检测 if (localFrustum.Intersects(localBox)) acceptedData.push_back(mat); } return acceptedData; }
这个方法理解起来也比较简单,直接将物体先用世界变换矩阵从物体自身坐标系搬移到世界坐标系,而后用观察矩阵将其搬移到视锥体自身的局部坐标系来与视锥体进行碰撞检测。代码以下:
std::vector<DirectX::XMMATRIX> XM_CALLCONV Collision::FrustumCulling3( const std::vector<DirectX::XMMATRIX>& Matrices,const DirectX::BoundingBox& localBox, DirectX::FXMMATRIX View, DirectX::CXMMATRIX Proj) { std::vector<DirectX::XMMATRIX> acceptedData; BoundingFrustum frustum; BoundingFrustum::CreateFromMatrix(frustum, Proj); BoundingOrientedBox localOrientedBox, orientedBox; BoundingOrientedBox::CreateFromBoundingBox(localOrientedBox, localBox); for (auto& mat : Matrices) { // 将有向包围盒从局部坐标系变换到视锥体所在的局部坐标系(观察坐标系)中 localOrientedBox.Transform(orientedBox, mat * View); // 相交检测 if (frustum.Intersects(orientedBox)) acceptedData.push_back(mat); } return acceptedData; }
这三种方法的裁剪表现效果是一致的。
该方法建立了树的模型,并以随机的方式在一个大范围的圆形区域中生成了225棵树,即225个实例的数据(世界矩阵)。其中该圆形区域被划分红16个扇形区域,每一个扇形划分红4个面,距离中心越远的扇面生成的树越多。
void GameApp::CreateRandomTrees() { srand((unsigned)time(nullptr)); // 初始化树 m_ObjReader.Read(L"Model\\tree.mbo", L"Model\\tree.obj"); m_Trees.SetModel(Model(m_pd3dDevice.Get(), m_ObjReader)); XMMATRIX S = XMMatrixScaling(0.015f, 0.015f, 0.015f); BoundingBox treeBox = m_Trees.GetLocalBoundingBox(); // 获取树包围盒顶点 m_TreeBoxData = Collision::CreateBoundingBox(treeBox, XMFLOAT4(1.0f, 1.0f, 1.0f, 1.0f)); // 让树木底部紧贴地面位于y = -2的平面 treeBox.Transform(treeBox, S); XMMATRIX T0 = XMMatrixTranslation(0.0f, -(treeBox.Center.y - treeBox.Extents.y + 2.0f), 0.0f); // 随机生成256颗随机朝向的树 float theta = 0.0f; for (int i = 0; i < 16; ++i) { // 取5-125的半径放置随机的树 for (int j = 0; j < 4; ++j) { // 距离越远,树木越多 for (int k = 0; k < 2 * j + 1; ++k) { float radius = (float)(rand() % 30 + 30 * j + 5); float randomRad = rand() % 256 / 256.0f * XM_2PI / 16; XMMATRIX T1 = XMMatrixTranslation(radius * cosf(theta + randomRad), 0.0f, radius * sinf(theta + randomRad)); XMMATRIX R = XMMatrixRotationY(rand() % 256 / 256.0f * XM_2PI); XMMATRIX World = S * R * T0 * T1; m_InstancedData.push_back(World); } } theta += XM_2PI / 16; } m_Trees.ResizeBuffer(m_pd3dDevice.Get(), 256); }
若实例缓冲区的大小容不下当前增加的实例数据,则须要销毁原来的实例缓冲区,并从新建立一个更大的,以确保恰好能容得下以前的大量实例数据。
void GameObject::ResizeBuffer(ComPtr<ID3D11Device> device, size_t count) { // 设置实例缓冲区描述 D3D11_BUFFER_DESC vbd; ZeroMemory(&vbd, sizeof(vbd)); vbd.Usage = D3D11_USAGE_DYNAMIC; vbd.ByteWidth = (UINT)count * sizeof(InstancedData); vbd.BindFlags = D3D11_BIND_VERTEX_BUFFER; vbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE; // 建立实例缓冲区 HR(device->CreateBuffer(&vbd, nullptr, m_pInstancedBuffer.ReleaseAndGetAddressOf())); // 从新调整m_Capacity m_Capacity = count; }
该方法接受一个装满世界矩阵的数组,把数据装填进实例缓冲区(若容量不够则从新扩容),而后交给设备上下文进行实例的绘制。
可是要注意须要将世界矩阵和其逆的转置矩阵都进行一次转置。
void GameObject::DrawInstanced(ID3D11DeviceContext * deviceContext, BasicEffect & effect, const std::vector<DirectX::XMMATRIX>& data) { D3D11_MAPPED_SUBRESOURCE mappedData; UINT numInsts = (UINT)data.size(); // 若传入的数据比实例缓冲区还大,须要从新分配 if (numInsts > m_Capacity) { ComPtr<ID3D11Device> device; deviceContext->GetDevice(device.GetAddressOf()); ResizeBuffer(device.Get(), numInsts); } HR(deviceContext->Map(m_pInstancedBuffer.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedData)); InstancedData * iter = reinterpret_cast<InstancedData *>(mappedData.pData); for (auto& mat : data) { iter->world = XMMatrixTranspose(mat); iter->worldInvTranspose = XMMatrixInverse(nullptr, mat); // 两次转置抵消 iter++; } deviceContext->Unmap(m_pInstancedBuffer.Get(), 0); UINT strides[2] = { m_Model.vertexStride, sizeof(InstancedData) }; UINT offsets[2] = { 0, 0 }; ID3D11Buffer * buffers[2] = { nullptr, m_pInstancedBuffer.Get() }; for (auto& part : m_Model.modelParts) { buffers[0] = part.vertexBuffer.Get(); // 设置顶点/索引缓冲区 deviceContext->IASetVertexBuffers(0, 2, buffers, strides, offsets); deviceContext->IASetIndexBuffer(part.indexBuffer.Get(), part.indexFormat, 0); // 更新数据并应用 effect.SetTextureDiffuse(part.texDiffuse.Get()); effect.SetMaterial(part.material); effect.Apply(deviceContext); deviceContext->DrawIndexedInstanced(part.indexCount, numInsts, 0, 0, 0); } }
剩余的代码均可以在GitHub项目中浏览。
该项目展现了一个同时存在225棵树的场景,用户能够自行设置开启/关闭视锥体裁剪或硬件实例化。若关闭硬件实例化,则是对每一个对象单独调用绘制命令。
DirectX11 With Windows SDK完整目录
欢迎加入QQ群: 727623616 能够一块儿探讨DX11,以及有什么问题也能够在这里汇报。