上一篇博客已经讲述了关于性能瓶颈的所有信息,现在就可以根据这些信息进行针对性的性能修复了,下面将介绍一系列技术来提升CPU受限和GPU受限程序的渲染管线渲染性能问题;
第一个技巧就是通过GPUSkinning来降低CPU或者GPU前端的负载,Skinning是基于动画骨骼的房前位置变化网格顶点的过程,在CPU上工作的系统会转化对象的骨骼,用于确定其当前的姿势,但是动画过程中的下一个重要步骤是围绕这些骨骼包裹的网格顶点,将网格放在最终的姿势中,为此需要迭代每个顶点,并对连接到这些顶点的骨骼做加权平均;
该顶点处理任务可以在CPU上执行,也可以在GPU前端执行,具体便是取决于是否启用了GPUSkinning
在Edit->project settings ->player settings ->other settings ->GPU skinning下切换,启用这个功能,会将skining活动推送到GPU中,但是CPU仍然推送数据到GPU中,并且在命令缓冲区中为任务生成指令,所以,启用这个命令,并不会完全消除CPU的负载,禁用这个选线可以使CPU在传输网格数据之前解析网格的姿态,并且简单的要求GPU按照原样绘制,从而减轻GPU的负担;显然,当场景中有很多动画网格,哪这个功能就很有用,可以将工作推给空闲的设备上;
这是一个GPU前端的技巧,之前说了一些网格优化的技术,这有助于减少网格顶点属性,正常来讲网格常常包含了大量不必要的UV和法线矢量数据,因此应该仔细检查网格是否包含这种多余的数据信息,还应该让unity优化结构,这样可以在前端内读取顶点数据时最大限度的减少丢失缓存的情况;
实际上,我们的目标只是降低实际的顶点数量,可以从三种方面入手;
1:让美术团队手动调整,生成多边形数更少的网格,或者使用网格抽取工具来简化网格;
2:简单的从场景中移除网格;
3:实现网格的自动剔除特性,后面会说;
通过几何着色器进行曲面细分,可以真正的让图形效果在使用最常见效果的游戏中脱颖而出,但是,它也极大的增加了前端处理的工作量;
除了改进曲面细分算法或者减轻其他前端任务的负载,来使曲面细分任务有更多的空闲空间外,并没有其他简单的技巧可以改进曲面细分,不管什么方式,如果前端遇到瓶颈,确在使用曲面细分技术,那么可以仔细的检查一下曲面细分是否消耗了大量的资源;
GPU实例化利用对象都具有相同渲染状态的特点,快速渲染统一网格的多个副本,因此只需要最少的DrawCall,这其实和批处理一样,只不过这不是自动处理的过程,实际上,可以将动态批处理看成一种简单的GPU实例化,因为真正的GPU实例化可以节省更多的资源,并且支持通过参数调整实现更多的定制化;
徐安重Enable GPU Instanceing复选框,可以在材质级别上应用GPU实例化,修改着色器代码,就可以引入变化,这样就可以为不同的实例提供不同的旋转、比例颜色等特性,这对于渲染森林、演视区域场景很有用,在这种场景中,可以渲染成百上千个有细微差异的网格副本;
这个系统比动态批处理更加统用,因为可以更多的控制对象的批处理过程,当然,如果以低效的方式进行批处理的操作,出错的机会更多,需要谨慎;
LOD(Level of Detail)是一个广义的术语,指的是根据对象与相机的距离或对象在相机视图所占空间,动态的替换对象,由于远距离很难分辨出低质量和高质量对象之间的差异,一般不会采用高质量的方式渲染对象,因此可以用尽可能简化的版本动态替换远距离对象,LOD最常见的实现是基于网格的LOD,当相机越来越远,网格会采用细节更少的版本替代;
网上有很多LOD的使用教程,这里不再赘述;
需要知道的是,基于网格的LOD还会消耗磁盘占用空间、RAM和CPU;替代网格需要捆绑在一起加载到RAM中,并且LODGrounp组件必须要定期测试相机是否移动到新的位置,以修改LOD级别,但是管线渲染的优点相当显著,因为动态渲染简单的网格,减少了需要传递的顶点数量,并且潜在的减少了渲染对象时所需要的DrawCall数量、填充率和内存带宽;
另外,并不是所有场景都适合使用这个功能,使用LOD特性会牺牲很多,过度的使用这个特性会增加应用程序中其他部分的负担,因此,当游戏中拥有广阔视野和大量摄像机运动的场景,可能会尽早的考虑实现这种技术,因为增加的距离和大量可见物体可能会极大的增加顶点数,相反如果总是在室内的场景,或者相机俯视视角的尝尽,使用他没有什么好处;需要我们自己判断;
window->rendering->Occlusion Culling开启
减少填充率消耗和过度绘制的最佳方法之一就是使用unity的遮罩剔除系统,该系统的原理就是将世界分割成一系列的小单元,并且在场景中运行一个虚拟摄像机,根据对象的大小和位置,记录那些单元对其他单元是不可见的
剔除前
剔除后
上面两图展示了遮罩剔除是如何有效的从外部角度减少场景中渲染对象数的;
由于使用这个功能,必须要开启static,因此无法应用于动态物体;
启用遮挡剔除功能将消耗额外的磁盘空间、RAM和CPU时间,需要额外的磁盘空间来存储遮挡数据,需要额外的RAM’来保存数据结构,需要CPU处理资源来确定每一帧的那些对象需要被遮挡,遮挡剔除数据必须要正确配置,以创建场景中适当大小的单元,单元越小,生成数据结构所需要的时间就越长,但是如果为场景进行了正确的配置,遮挡剔除可以剔除不可见的对象,减少过度绘制和DrawCall数,来减少填充率;
注意:即使对象被遮挡剔除,也必须计算其阴影,所以不会节省这些任务的DrawCall数和填充率
粒子系统适用于大量不同的视觉效果,通常生成的粒子越多,效果看起来越好看,但是由于粒子系统会使用着色器,因此,他们可以触碰到渲染管线的所有部分,实际上,他们为前端生成了多个顶点(每个粒子都是一个四元体)并且可以使用多个纹理,这些纹理也会消耗后端的填充率和带宽;
降低粒子系统密度和复杂度非常简单,使用更少的系统,生成更少的粒子,使用更少的特殊效果;图集也是另一种降低粒子系统性能成本的常用技术,然而还有一个重要的性能因素-----粒子系统的自动剔除过程;
粒子剔除系统
这个文章的思想就是根据不同的设置,所有粒子都将会变成可预测或者不可预测的,当粒子系统是可预测且对主视图是不可见的时候,可以自动删除整个粒子系统,以提升性能,一旦可预测的粒子系统重新出现在视野中,untiy就能够精确的计算出粒子系统在那个时刻的样子,就好像它一直在生成看不见的粒子一样,
ParticleSystem 组件中的很多方法都是递归调用的,这些方法的调用需要遍历粒子系统的每个子节点,并调用子节点的GetComponent的方法获取组件信息,如果组件存在,则调用组件中对应的方法,以此类推,对该子粒子系统的父节点、子节点重复此操作,这对粒子系统的深层次结构来说是一个很大的问题,例如以下的几个粒子系统API会收到递归调用的影响:Start、stop、pause、clear、simulate、isalive ,我们正常使用粒子系统的时候,不可能做到不用这些方法,但是,这些方法有一个默认为true的withChildren参数,给这个参数传递false就可以禁用递归行为和子节点的调用,因此此方法调用只会影响指定的粒子系统,从而降低调用的成本开销;
画布主要任务就是管理在层次窗口中绘制UI元素的网格,并且发出渲染这些元素所需要的DrawCall,另一个重要作用就是将网格合并进行批处理(材质相同),以降低DrawCall,然而当画布或者其自动向发生改变后,(画布污染),当这种现象发生之后,需要为画布上的所有UI对象重新生成网格,才可以发出DrawCall;这重新生成网格的过程不是一个简单的任务,因此会损耗Unity的性能;
因此只要发现UI的改变大破之CPu的使用率大幅上升,就需要对这方面进行优化;
一个很好的方案就是使用更多的画布,千万不要使用单一的画布去绘制所有的UI元素,这意味着需要检查UI中的任何元素在任何时候发生的改变,随着越来越多的元素填充到这个单个画布上,性能就会变得越来越糟糕,但是如果每个画布都是独立的,不需要和UI中的其他画布进行交互,那么就可以将工作负载分离,简化整个画布所需要的任务;
在这种情况下,即使单个元素仍然发生变化,响应的时候需要重新生成的其他元素也更少,从而降低了性能成本,但是这种方法的缺点就是,不同画布上的元素不会批量组合到一起,因此,如果有可能的化,应该尽量将具有相同材质的相似元素组合到同一个画布上;
为了便于组织,也可以将画布作为另一个画布的子节点,并应用相同的规则,这样一个画布的元素发生改变,另一个画布不会收到影响
在生成画布的时候,可以采用元素更新的时间来给元素分组,如静态、偶尔、连续,这样可以最大程度的减少重新生成元素期间浪费的工作量;
UI元素具有RayCastTarget选项,允许通过单机、触摸、和其他用户行为进行交互,当以上任意行为发生的时候,GraphicsRayCaster组件将执行像素到边界框检查,以确定与之交互的是那个元素,这是一个简单的for循环,对非交互元素不执行检查,这样就减少了迭代的元素数量,提高了性能;
造成画布污染可能会因为某些元素重新生成的工作,其中最明显的就是启用和禁用这些元素,因此如果想要禁用UI的一部分,只需要禁用其子节点的画布组件,这样就可以避免布局系统的这种昂贵的生成调用,为此,可以将画布组件的enable属性设置为false,这种方法的缺点就是,如果任何子对象具有Update、Fixed Update、LateUpdate或者Coroutine方法,需要手动去禁用他们,否则这些方法会继续执行;禁用画布只会停止UI的渲染和交互,各种更新会正常执行;
color属性中的alpha值为0的UI元素仍然会发出DrawCall,因此如果想要隐藏,应该使用IsActive元素;
ScrollRect是一种UI元素,用来滚动其他UI元素,但是这种元素的性能会随着大小改变而变得非常差,下面几种方法可以对进行改善;
只要把其他UI元素的Depth值设置为低于ScrollRect元素,就可以实现滚动式UI特性,但是这种方式并不是很好,因为ScrollRect中的元素不会被剔除,当ScrollRect移动的时候,需要每一帧重新生每个对象,如果元素没有被剔除,就应该使用RectMask2D组件来剪裁和剔除不可见的子对象,
Unity 3D - Mask和RectMask2D区别
PixelPerfect是画布组件上的一个设置,它强制其他子UIUI元素与屏幕上的像素对齐,这通常是美术上的一个要求,因为UI元素比禁用它的时候显示的更清晰,但是对于动画和快速移动的物体,由于涉及到运动,因此这个属性没什么用,禁用ScrollRect元素的PixelPerfect属性是一种节省大量成本的好方法,但是由于pixelPerfect设置会影响整个画布,因此画布下的子对象启用pixelperfect
即使移动速度是每一帧只移动像素的很小一部分,画布也需要重新生成整个ScrollRect元素,因此可以使用ScrollRect.velocity和ScrollRect.stopMoveMovement()方法检测帧移动的速度低于某个阙值,就可以手动的冻结它的运动,这样有助于大大降低重新生成的频率;
后续会补充
这种方法简单直接,在之前的纹理文件优化有说过
unity性能优化之纹理文件的优化
在之前的纹理文件优化有说过
unity性能优化之纹理文件的优化
如果内存带宽存在问题,就需要减少正在进行的纹理采样量,这里并没有什么特别的技巧科研,因为带宽只和吞吐量有关,而我们考虑的置表是降低索要推送的数据量;
减少纹理容量的一种方法是直接降低纹理分辨率,从而降低纹理质量,但是这显然不理想,所以另一种方法是采用不同的材质和着色器,在不同的网格上重复使用纹理,比如适当变暗的砖纹理可能看起来像墙一样,但是这需要不同的渲染状态,虽然这种方法不能减少DrawCall,但是可以减少内存带宽的消耗;
另外就是,使用图集,避免GPU在同一帧中反复拉去不同的纹理文件;
与纹理相关的最后一个考虑因素是可用的VRAM数量,大多数纹理的传输都是在初始化阶段从CPU传输到GPU的,但是也可能发生在当亲视图的第一次需要某个不存在的纹理的时候,这个过程是异步的,并使用一个空白纹理,知道完整的纹理准备好渲染位置,因此应该避免在运行时频繁的引入新的纹理;
在异步加载纹理过程中使用空白的纹理可能会影响游戏质量,因此,我们可以创建一个使用纹理的隐藏Gameobject,将其放弃场景的某个地方,一旦玩家到达某个位置,就触发这个纹理的加载,还可以通过脚本代码更改材质的texture属性,GetComponent.material.texture = texturetopreload
之前就已经说过,实时阴影很容易成为DrawCall大量增加的和填充率高消耗的因素之一,因此应该花时间调整以下这些设置,直到可以获得所需性能和图形质量;
在Editor->projectsettings->quality->shadows 下有一些重要的阴影设置,对于shadows而言,soft Shadow 代价最大,Hard Shadows代价最小,NoShadows不产生代价,而Resolution、shadow projectiopn、shadow distance、和shadow cascades 也是影响阴影性能的重要设置;
Shadow Distance(阴影距离)是运行时阴影渲染的全局乘数。在离相机很远的地方去渲染阴影几乎没有什么意义,所以这个设置时针对游戏以及在游戏期间希望看到的阴影量进行配置;
较高的Shadowwe(阴影分辨率)和ShadowCascades(阴影级联)值将增加内存带宽和填充率的消耗,这两种设置都有助于抑制阴影渲染中生成的伪影效果,代价就是纹理的尺寸会大得多;
遮罩剔除时基于层的,可以用于限制受给定灯光都西昂影响的物体,而这个属性是降低照明开销的有效办法;
与运行时生成光照和阴影相比,在场景中烘焙光照和阴影对处理器的计算强度要低很多,其缺点时增加了应用程序的磁盘占用、内存消耗、内存带宽;
个人学习记录,借鉴unity游戏优化