DrawCall合并

Date: 2018-3-11 03:14
Categories: 游戏引擎

Geometry Instancing

关于Geometry Instancing主要的资料来自GPU Gems的一篇文章[1], MSDN[2]和DX SDK[5]。

相关细节不再赘述,本篇文章对这些资料中提到的各种Instancing方式做个总结。

  1. 静态批量(Static Batching)[1]

把所有需要批量绘制的几何对象先转换到世界空间(也就是应用每个对象的实例数据),然后拷贝顶点数据和索引数据到一个vertex buffer和index buffer,最后用一个draw call把所有几何对象绘制出来。注意这两个buffer的内容一旦创建就不再更新,所以可以设置为default模式,一次性放置到显存中。

这种方法的特点是消耗显存较多,不支持LOD,不支持蒙皮动画,几何对象如果有位置变换也不好处理。

对于无位置变化也无顶点运动的大量静态Mesh,比较适合这种方式。比如室内场景中的墙和家具等。

  1. 动态批量(Dynamic Batching)[1]

与静态批量不同,动态批量会每帧更新vertex buffer和index buffer的内容。其流程还是类似静态批量,只是因为绘制对象的实例数据(位置、动画等信息)可能有变化,所以需要每帧都应用实例属性数据。然后才把结果数据拷贝到vertex buffer和index buffer.

动态批量效率比较低,也会消耗大量显存。虽然draw call减少了,但是需要消耗CPU时间做拷贝。

  1. Shader Instancing[5]

Shader Instancing和上述Static/Dynamic Batching一样,也需要一个大的vertex buffer和一个大的index buffer以容纳所有几何实例的顶点信息。不过这些顶点信息是没有应用过实例属性的原始顶点信息。Shader Instancing还在vertex buffer里面为每个实例存了一个instance index(DX SDK中是存在TEXCOORD1)。instance index的作用是在shader中应用实例属性时做实例属性的索引。

绘制对象的实例属性数组是通过顶点常量传递到shader中的。由于顶点常量的个数有限,所以一批几何体可能需要渲染几次才能全部完成。如下是绘制调用的主体逻辑。

//所有绘制对象的顶点数据和索引数据,每个绘制对象都要存
pd3dDevice->SetStreamSource(0, g_pVBBox, 0, sizeof(BOX_VERTEX_INSTANCE));
pd3dDevice->SetIndicies(g_pIBBox);

while (nRemainingBoxes > 0)
{
int nRenderBoxes = min(nReaminingBoxes, g_nNumBatchInstance); //分批
g_pEffect->SetVectorArray( //传递实例属性数组到shader
g_HandleBoxInstanceArray, g_InstanceDataOffset, nRenderBoxes);
g_pEffect->CommitChanges();

pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,
    0, 0, nRenderBoxes * 4 * 6, 0, nRenderBoxes * 6 * 2);
nRemainingBoxes -= nRenderBoxes;

}
这个方法和gpu gems2中的顶点常量实例化的方法[1]很相似,不过由于gpu gems2中的这个方法将所有的顶点常量拿来做instancing运算,无法进行骨骼运算。为了解决这个问题dx sdk中的sample中并没有使用全部的顶点常量[3]。

即使每帧更新实例数据,由于vertex constant很快,速度不受影响。

  1. Stream Instancing[5]

与上述Instancing方法不同,Stream Instancing使用一个vertex buffer和一个index buffer存储一份原始顶点数据和索引数据。不需要每个绘制对象存一份,所以占用内存很少。

在DX SDK的sample中,所有实例数据是统一存在另一个vertex buffer中的。但是绘制的时候是逐个传递实例数据,每个绘制对象需要一次draw call。实例数据是放在stream1中的。如下是主体绘制逻辑。

//所有绘制对象的顶点数据和索引数据,只需要存一份
pd3dDevice->SetStreamSource(0, g_pVBBox, 0, sizeof(BOX_VERTEX));
pd3dDevice->SetIndicies(g_pIBBox);

for (int nRemainingBoxes = 0; nRemainingBoxes < g_numBoxes; ++nRemainingBoxes)
{
//逐个传递实例数据,一次绘制一个
pd3dDevice->SetStreamSource(1, g_pVBInstanceData,
nRemaningBoxes * sizeof(BOX_INSTANCEDATA_POS) 0);
pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,
0, 0, 4 * 6, 0, 6 * 2);
}
Stream Instancing并没有减少draw call数量。

  1. Constants Instancing[5]

与Stream Instancing类似,Constants Instancing不需要每个绘制对象存一份顶点数据和索引数据,绘制的时候也是一次draw call绘制一个对象。不同点在于,实例数据是通过一个shader变量传到shader的。

//所有绘制对象的顶点数据和索引数据,只需要存一份
pd3dDevice->SetStreamSource(0, g_pVBBox, 0, sizeof(BOX_VERTEX));
pd3dDevice->SetIndicies(g_pIBBox);

for (int nRemainingBoxes = 0; nRemainingBoxes < g_numBoxes; ++nRemainingBoxes)
{
//逐个传递实例数据,一次绘制一个
pd3dDevice->SetVector(
g_HandleBoxInstance, &g_vBoxInstance[nRemainingBoxes]);
pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST,
0, 0, 4 * 6, 0, 6 * 2);
}
Constants Instancing和Stream Instancing本质上是一样的。

  1. Hardware Instancing[1][5]

Hardware Instancing既不需要每个绘制对象存一份vertex和index数据(Indexed Geometry Instancing),而且一次draw call可以把所有对象都绘制出来,所以是共用几何数据情况下批量绘制的最佳方法。不过Hardware Instancing需要vs_3_0支持。

Hardware Instancing使用两个stream,其中stream 0存储静态几何数据,stream 1存储动态实例数据。

Hardware Instancing分为indexed和non-indexed两种方式。Indexed geometry instancing需要设置index buffer和两个vertex buffer(一个用于vertex数据,一个用于instance数据)。Non-Indexed geometry instancing只需要两个vertex buffer(一个用于vertex数据,一个用于instance数据)。Non-Indexed geometry instancing需要每个绘制对象存一份vertex数据,这就是为了绘制无index几何数据的代价。各buffer layout如下图所示[2]

Indexed, Non-Index情况下设置stream的方法如下[2]:

// Set up the geometry data stream
pd3dDevice->SetStreamSourceFreq(0,
(D3DSTREAMSOURCE_INDEXEDDATA | g_numInstancesToDraw));
pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

// Set up the instance data stream
pd3dDevice->SetStreamSourceFreq(1,
(D3DSTREAMSOURCE_INSTANCEDATA | 1));
pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0,
D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));


// Set the divider
pd3dDevice->SetStreamSourceFreq(0, 1);
// Bind the stream to the vertex buffer
pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

// Set up the instance data stream
pd3dDevice->SetStreamSourceFreq(1, verticesPerInstance);
pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0,
D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));
至于non-indexed为啥需要每个绘制对象都存一份顶点数据,我理解是API设计的权衡。现在这种方式可以支持每个绘制对象有自己的顶点连接方式。除了没有Index数据等少数情况无法使用Indexed模式外,使用Index Geometry Instancing不管是内存使用还是效率都是最好的。

[1] GPU Gems - Chapter 3. Inside Geometry Instancing
[2] Efficiently Drawing Multiple Instances of Geometry (Direct3D 9) (Windows)
[3] 批次渲染 - 程序园
[4] 文明5渲染分析:树林 - 燃野的文章 - 知乎专栏
[5] DX SDK

================

有人提了减少statechange的方法,我来谈谈draw call合并。
虽然D3D9的时代好多人在嘶吼batch的意义,但是实际上现在PC的硬件对于batch是越来越不敏感了。当
然越早的显卡越垃圾,垃圾显卡对啥都敏感。另外drawcall的合并对移动平台意义挺大的。
是否需要对batch进行优化需要首先profile。其实,能合并drawcall的情况是非常少的。即使能合并,同屏
400个batch与380个batch的帧率区别也可能微乎其微。你要考虑你同屏N多个材质,材质参数,材质贴图
完全一样的概率,否则合了也看不出来。
如果你确定要做这方面优化,请往下看。
以下谈谈我在batch合并方面的经验,以DirectX9描述。
其实就dx9来说,真正能合并drawcall的场合是非常少的,而且这些场合有一个共同点:动态vertex buffer
Or index buffer的使用。我们知道动态创建和填充Vertex buffer/Index buffer的开销是非常大的,所以合并
drawcall只适用于动态创建填充buffer的开销可以接受的情况,而这里的优化策略需要解决的重点问题就是
vertex/index buffer频繁lock填充的开销和删除/重建的开销。
1.Unity3D对于场景中纯静态物体的batch合并: dynamic Index buffer
适用场合:场景中纯静态的物体
纯静态的场景物体为了合并drawcall预先将场景里材质相同的物体的vertex buffer/index buffer全部合并掉没
什么意义,你看上去是静态的,但是哪个mesh什么情况下显示不显示是由view frustum动态决定的,即使
你预先暴力合并了这些mesh的顶点,材质和所有贴图,这可能也会因为物体包围盒过大导致渲染了过多冗
余网格而拖慢渲染效率。
但是动态地合并同样材质的物体Unity有做:对于场景中勾选了static的物体,Unity会预先将它们的mesh数
据合并为一个大的vertex buffer。对view frustum里所有可见且shader相同,材质所有参数包括贴图也相同
的两个static物体,Unity会lock一个index buffer,然后将两个网格的indices排在一起,通过一个
drawindexedprimitive画出来。
这种做法在算法上并无高明之处,就是比较一下shader和材质参数是否相等而已。但是动态合并index
buffer会带来其他问题:动态Vertex buffer的创建开销,顶点数据拷贝的开销。
从vertex buffer和index buffer的管理策略上来说,Unity有自己的想法。我测试过在非常小的场景中,这个
大的vertex buffer的大小是25440个float,而index buffer(动态每帧都lock的)的大小是16384个元素。
但是有个问题,不同view frustum下,渲染用的vertex buffer并不是同一个buffer。这其中,我不知道unity
这是为了降低lock开销用了一种swap方法呢,还是说其实它的vertex buffer是由某种程度的逻辑控制的纯动
态创建的?这个我没仔细看。
动态的index buffer比起动态的vertex buffer有一个好:省得填充更多的数据。这是非常有意义的。因为PC
上如果有几万到十几万个顶点的数据需要拷贝的话,memcpy很可能会成为某种程度上的性能瓶颈。当
然,你未必用memcpy填充数据就是了。
(没看过unity代码,用pix偷窥过Unity的渲染流程而已。部分内容脑补错了别打我
2.Particle System的合并
适用场合:Sprite particle, ribbon, beam
单拎出来说Particle是因为……很多粒子系统的粒子绘制时是每帧填充粒子的Vertex Buffer的。不过另外一
些粒子系统用Instance绘制,那个稍后再说。
在每帧顶点数据都被lock以更新渲染的情况下,合并batch可谓顺理成章。同一个粒子发射器发射的渲染选
项和参数完全相同的粒子用一个batch画出是常识。另外,World space中,所有贴图材质相同,粒子渲染
参数也相同的条件下,如果材质的类型是alpha add(我指那种越叠越亮的不用排序的Alpha混合模式),
两个粒子发射器的Vertex Buffer可以暴力地合二为一。这种情况,暴力地拿粒子渲染参数(billboard
matrix, world matrix, etc.)进行比较,排个序就是了。
频繁lock可能会带来性能问题,我的解决方案是使用一个或多个大的vertex buffer,一次lock,统一填充。
这里有另一个问题需要特殊说明。早期有资料说,对频繁lock的buffer,请创建时使用
D3DUSAGE_DYNAMIC这个flag。我亲自评测过,如果你buffer管理的好,一帧只有个位数的vertex buffer
被lock填充的话,还是不用D3DUSAGE_DYNAMIC这个flag要快很多。尽信文档则不如无文档,其实还是
自己profiling的结果最可信。

3.Instancing
适用场合:Mesh particle,Vegetation
这个就是众所周知的Instancing了。另外,听说D3D9的instancing效率也不怎么高,一个两个三个batch你
就不用合了。。。
instancing也需要动态管理vertex buffer,只不过由于功能限制,它的buffer重建的情况可能比上述Particle
System多得多。因为DrawIndexedPrimitive函数中,你可以设置从第几个顶点开始画。而instancing,你没
法指定第一个instance data的偏移量。所以每一次drawcall都要使用一个单独的vertex buffer,这可能不是
个位数的vertex buffer能搞定的。

====

这些的确算是比较常见又泛的方法,instance的合并,至于先渲染近的物体可能归入culling的范围更合适,但是具体到材质合并减少state change方面有更具体的做法吗?我的理解里,如果预先通过profile工具测定了各个状态切换函数的开销,在架构引擎时再封装一层state object,再提供比较state object的方法diff,每次只更新diff的部分,那就可以以渲染对象为节点移动代价为带权边构造无环图就可以把材质排序转换为最小代价遍历图的问题,只是个构想,不知道在更商用的引擎里这部分是怎么做的呢?有没有什么比较高大上的做法。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值