在Unity3D中,常用的减少Draw call的优化技术就是批处理技术。批处理的原理是减少每一帧需要的Draw call数目。为了把一个对象渲染到屏幕上,CPU需要检查哪些光源影响了该物体,绑定shader并设置它的参数,再把渲染命令发送给GPU。当场景中包含了大量的对象时,这些操作就会非常耗时。例如,如果我们需要渲染一千个三角形,把它们按一千个单独的网格进行渲染所花费的时间要远远大于渲染一个包含一千个三角形的网格。在这两种情况下,GPU的性能消耗其实并没有多大的区别,但CPU的draw call数目就会成为性能瓶颈。因此,批处理的思想很简单,就是在每次调用draw call时尽可能多地处理多个物体。
使用同一材质的物体可以进行批处理,因为对于使用同一材质的物体,它们之间的不同仅仅在于顶点数据的差别。我们可以把这些顶点数据合并在一起,再一起发送给GPU,这样就可以完成一次批处理。
在Unity中支持两种类型的批处理,一种是动态批处理,另一种是静态批处理。对于动态批处理来说,优点是一切处理Unity自动完成的,不需要我们做任何操作,而且物体可以是移动的,缺点是限制有很多,可能一不小心就会破坏了这种机制,导致Unity无法动态批处理一些使用了相同材质的物体。而对于静态批处理来说,它的优点是自由度很高,限制很少;但缺点是可能会占用更多的内存,而经过静态批处理后的所有物体都不可以再移动了(即便在脚本中尝试改变物体的位置也是无效的)。
Unity3D中的动态批处理技术
动态批处理的原理是,每一帧把可以进行批处理的模型网格进行合并,再把合并后的模型数据传递给GPU,然后使用同一个材质对其渲染。除了实现方便,动态批处理的另一个好处就是,经过批处理的物体仍然可以移动,这是由于在处理每帧时Unity都会重新合并一次网格。
虽然Unity的动态批处理不需要我们进行任何额外工作,但只有满足条件的模型和材质才可以被动态批处理。(需要注意的是,随着Unity版本的变化,这些条件也有一些改变)这些条件限制是:
1.能够进行动态批处理的网格的顶点属性规模要小于900.例如,如果shader中需要使用顶点位置,法线和纹理坐标这三个顶点属性,那么要想让模型能够被动态批处理,它的顶点数目不能超过300.需要注意的是,这个数字在未来有可能会发生变化,因此不要依赖这个数据。
2.一般来说,所有对象都需要使用同一缩放尺度(可以是(1,1,1),(1,2,3),(1.5,1.4,1.3)等,但必须都一样)。一个例外情况是,如果所有的物体都是用了不同的非统一缩放,那么他们也是可以被动态批处理的。但在Unity5中,这种对模型缩放的限制已经不存在了。
3.对于使用光照贴图纹理的物体需要小心处理。这些物体需要额外的渲染参数,例如,在光照贴图纹理上的索引和偏移量以及缩放信息等因此,为了让这些物体可以被动态批处理,我们需要保证它们指向光照贴图纹理中的同一个位置。
4.有多个Pass通道的shader会中断批处理。在前向渲染中,我们有时需要使用额外的Pass来为模型添加更多的光照效果,但这样一来模型就不会被动态批处理了。
Unity3D中的静态批处理技术
静态批处理的实现原理是,只在运行的开始阶段,把需要进行静态批处理的模型合并到一个新的网格结构中,这意味着这些模型不可以在运行时刻被移动。但由于它只需要进行一次合并操作,因此比动态批处理更加高效。但静态批处理的缺点是需要占用更多的内存来存储合并后的几何结构。这时因为,如果在静态批处理前一些物体共享了相同的网格,那么在内存中每一个物体都会对应一个该网格的复制品,即一个网格会变成多个网格再发送给GPU。如果这类使用相同网格的对象很多,那么这就会成为一种性能瓶颈了。例如,如果在一个使用了1000个相同树模型的森林中使用静态批处理,那么就会多使用1000倍的内存,这会造成严重的内存影响。这时的解决方法就是要么忍受这种牺牲内存换取性能的方法,要么不要使用静态批处理,而使用动态批处理技术(但要小心控制模型的顶点属性数目),或者自己编写批处理方法。
在Unity中使用静态批处理的方法是,在场景中选中需要静态批处理的物体,在其Inspector面板的右上角勾选上Batching static静态属性。在内部实现上,Unity首先把这些静态物体变换到世界空间下,然后为它们构建一个更大的顶点和索引缓存,对于使用同一材质的物体。Unity只需要调用一个drawcall就可以绘制全部物体。而对于使用不同材质的物体,静态批处理同样可以提升渲染性能。尽管这些物体仍然需要调用多个draw call,但静态批处理可以减少这些draw call之间的状态切换,而这些切换往往是费时的操作。
我们可以在Unity的分析器中观察到应用静态批处理前后VBO total(Vertex Buffer Object,顶点缓存对象)的变化。在一些物体共享了相同的网格的情况下,我们可以看到这些物体在使用了静态批处理技术后,VBO total的数目变大了,这正是因为静态批处理会占用更多内存的缘故。正如上面所讲,静态批处理需要占用更多的内存来存储合并后的几何结构,如果一些物体共享了相同的网格,那么在内存中每个物体都会对应一个该网格的复制品。
如果场景中包含了除了平行光以外的其他光源,并且在Shader中定义了额外的Pass来处理它们,这些额外的Pass部分是不会被批处理的,但是处理平行光的Base Pass部分仍然会被静态批处理。
Unity3D中使用共享材质
无论是动态批处理还是静态批处理,都要求模型之间需要共享同一个材质。但不同的模型之间总会需要有不同的渲染属性,例如,使用不同的纹理,颜色等。这时我们需要一些策略来尽可能的合并材质。
如果两个材质之间只有使用的纹理不同,我们可以把这些纹理合并到一张更大的纹理中,这张更大纹理被称为是一张图集(atlas)。一旦使用了同一纹理,我们就可以使用同一材质,再使用不同的采样坐标对纹理采样即可。
但有时除了纹理不同外,不同的物体在材质上还有一些微小的参数变化,例如,颜色不同,某些浮点属性不同。但是,不管是动态批处理还是静态批处理,它们的前提都是使用同一个材质。是同一个,而不是使用了同一shader的材质,也就是说它们指向的材质必须是同一个实体。这意味着,只要我们调整了参数,就会影响到所欲使用这个材质的对象。那么想要微小的调整怎么办呢?一种常用的方法就是使用网格的顶点数据来存储这些参数(最常见的就是顶点颜色数据)。
经过批处理后的物体会被处理成更大的VBO发送给GPU,VBO中的数据可以作为输入传递给顶点着色器,因此,我们可以巧妙地对VBO中的数据进行控制,从而达到不同效果的目的。一个例子就是,森林场景中的所有树使用了同一材质,我们希望它们可以通过批处理来减少draw call,但不同树的颜色可能不同。这时,我们可以利用网格顶点的颜色数据来调整。
需要注意的是,如果我们需要在脚本中访问共享材质,应该使用Renderer.sharedMaterial来保证修改的是和其他物体共享的材质,但这意味着修改会应用到所有使用该材质的物体上。另一个类似的API是Renderer.material,如果使用Renderer.material来修改材质,Unity会创建一个该材质的复制品,从而破坏批处理在该物体上的应用。
关于在Unity3D中使用批处理的注意事项:
1.尽可能的使用静态批处理,但要时刻小心对内存的消耗,并且记住经过静态批处理的物体不可以再被移动。
2.如果无法进行静态批处理,而要使用动态批处理的话,那么尽可能减少物体的数目并且让这些物体包含少量的顶点属性和顶点数目。
3.对于游戏中的小道具,例如可以捡拾的金币等,可以使用动态批处理。
4.对于动画的这类物体,我们无法全部使用静态批处理,但其中如果有不动的部分,可以把这部分标识成“Static”。
5.由于批处理需要把模型变换到世界空间下再合并它们,因此,如果shader中存在一些基于模型空间下坐标的运算,那么往往会得到错误的结果。一个解决方法是,在shader中使用DisableBatching标签强制使该shader的材质不会被批处理。
6.使用半透明材质的物体通常需要使用严格的从后往前的绘制顺序来保证透明混合的正确性。这意味着,当绘制顺序无法满足时,批处理无法在这些物体上被成功地应用。