一、渲染优化的元素
和渲染优化相关的东西很多,大致可分为网格、着色器材质、光照和阴影。相关的优化技术有相机视椎体剔除、遮挡剔除、基于层的分类剔除与合并绘制调用。LOD降级分为着色器的LOD降级和LodGroup降级。在所有这些方面地形是比较特殊的,假设使用的是unity的地形,可以将这些与性能优化有关的元素整理成下图:
1.1 小型物体的优化
小型物体不能像一堵墙或桌子那样提供遮蔽,按照功能,小型物体可以分为非功能性的和功能性的。
对于以下非功能性的小型物体的解释:
1. 静态物体的图集。如果图集是静态的,处在同一个角落(屋子或营地)的小型物体可以合并贴图到一个图集中,并使用同一种材质。对于unity而言,如果你使用了零散贴图也没关系,可以设置打包的标签,让Sprite Packer帮你打包,这样得到的也是合并的图集。
一个角落有一个图集,这种图集应该是静态小物体贴图的合并。有些小型物体可能会出现在地图上各个角落,如果出现频率不是特别高,只在五六个地方反复出现,可以考虑地图上的冗余,即保持一个角落有一个图集。
如果这个小型物体的出现频率特别高,不合并到图集上,或者合并到一个单独的高频率物体的图集中也行,如子弹。
2. 动态物体的图集也应该专门放到动态物体准备的图集中,使用同一种材质来制作。
动态小型物体应该严格限制顶点数量,运行时的合批有一个的CPU计算、空间转换,而且unity合批的上限是900个与顶点相关的通道(目前)。
小型物体应该放置到一个单独的图层上(根据功能性也可以分别放到两个图层上)。小型物体没有遮蔽功能,而且是绘制调用数量的一个主要来源,应该对其使用基于层的、较小的剔除距离。
小型物体的遮挡剔除的优化重点,但也有问题,如烘焙精细度和精确度,对于中型物体,比如汽车,在视觉上和功能上都比较重要,可以考虑LOD Group这种不同细节的模型,保证视觉上的存在和远距离渲染的简化。
对于小型物体,不建议使用LOD Group。对于处于一个闭塞空间(如房子内使用的小物体),可以设定一个更小的剔除距离,比如设为房子大小的3倍距。相对于空旷的空间,从外面观察房间内一般都比较昏暗,因此看不清楚/无法发现房间内的小型物体是正常的,逻辑上讲得通。
如果闭塞空间内的物体可以被使用,比如被玩家带到了室外,那么就应该改变其在层上的剔除距离。闭塞空间内的物体本应该使用遮挡剔除来解决,但是使用遮挡剔除有代价和精度问题。如果可以使用遮挡剔除来解决,我们先在层的这个概念上做好优化。
1.2 中型物体的优化
对于建筑这种中型物体或者对于岩石这种网格细节密度比较大的物体,可以根据物体在屏幕上的大小使用LODGroup,动态切换/使用不同细节的网格,从而调节渲染性能的表现效果,这需要在项目规划阶段规划好时间。
但是LOD的这种功能是以冗余(内存为代价的),还会给美术添加额外的建模负担。不过对于建筑这种大方块物体,在较远的距离上,可以使用一个比立方体简单的表达方式。此外这种方式影响的只是“物体”的渲染,与物理碰撞没有关系,也就是说不会影响看不见的运算逻辑。
1.3 大型物体的优化
unity的遮挡剔除和相机的剔除功能是并行的,可以在基于View Frustum剔除的基础上进一步剔除那些不会出现在视野内的小物体。遮挡剔除主要靠烘焙静态物体之间的遮挡,将它们存储在一个树状结构中,然后在运行时查询,从而剔除那些被遮挡的、不会出现在视野内的物体。
但是遮挡剔除也有问题,首先遮挡剔除会线下烘焙、生成数据,这个线下处理中的时间不是问题,问题是它所产生的数据。根据场景的大小以及烘焙的细分粒度,会产生10KB到10MB的数据,如果遮挡剔除粒度太粗,可能会剔除失败,比如被墙挡住的方块并未被剔除掉。
如果简单地增加遮挡剔除的粒度,就会使遮挡剔除的数据膨胀,并且增加运行时CPU查询表结构的负担,因此这也是一个效能和开销之间平衡的问题。
另外从遮挡剔除的角度来说,物体被分割成小块,有利于增加遮挡剔除的效率。但把散乱、不同角度的小型物体连成一个大型物体,不利于遮挡剔除。
1.4 模型的优化
下面介绍建模的相关概念。
在建模软件(比如Max 和Maya)中几何体的同一位置的点就是一个点。但是在unity中,准确地说,在GPU上,点的意义对应于三角面和相邻的三角面。如果定义它们的顶点的法线不一致,就是不能共享的一个顶点。软边就是指相邻三角面共享同一个顶点,它的法线以及所有和顶点相关的性质(如切线、顶点色)和UV都共享。而硬边则意味着相邻的面具有不同的顶点,对同一个位置顶点的切割意味着更多的数据、内存,以及提交到GPU的数据。
角色建模大多涉及的是软边,像剑这种切割武器则涉及的是硬边。
当然不能为了更少的数据要求降低模型精度,这只是一个值得注意的东西,如果有些东西的软边可以表达,就不必使用硬边了,比如杯子,你可以说它是一个带棱的杯子,也可以说它是一个带弧的杯子(这种情况下就选择软边)。
模型的软边和硬边如图所示:
对于模型来说,为了计算光照,法线一般是必需有的,但是切线不是必需有的。同时具备切线和法线一般是为了计算切线空间内的数据,比如法线凹凸。对于渲染,这并不是一个必选项。而在unity中切线是一个Vector4,如果是一个由1024个顶点的模型,就可以省下16KB的内存。
对于一个模型来说,一般顶点位置、法线以及一套uv是必需有的,有些情况下会使用到顶点色来配合shader做表现。对于静态物体,如果使用unity的光照贴图,还需要一套uv,可以勾选fbx的Generate Lightmap UVs(需要烘焙的fbx都要勾这个):
这个uv用来在一个大的光照贴图中定位贴图,如果不勾然后烘焙,会出现uv乱序,它会默认指定一个没有拆分uv的图来烘焙,导致烘焙的不对。如果在建模软件中烘焙,这套uv不是必需有的,使用与diffuse贴图相同的uv就可以(如果diffuse贴图没有平铺现象)。unity勾选Generate Lightmap UVs生成的uv在烘焙的lightmap中会占用大面积的图块,而且分布是乱序的,我们选择一个prefab可以看到:
如果在建模软件烘焙,我们可以使uv顺序铺满一个图块,节省图块大小,例如:
另外如果在拆分uv时同一个位置的点具有不同的UV,也会导致产生新的顶点。
UV是一个2D向量,如果假设模型有1024个顶点,一套UV是8KB。
对于凸出的装饰物,应该避免对一个整面进行切割挤出,这样做不会减少必要的点,反而会增加面。装饰物应该尽量使用软边,烤箱是没办法使用硬边的。
删除或者合并在视觉上过小的面。
避免过大的面,插入必要的线进行分割,这个分割操作考虑光照和面剔除的因素。
如果使用Max,尽量使用厘米作为单位,这样在Max里100个单位是一米,如果使用默认的英制单位,在导入时的单位转换就不会这么直观了。
可以考虑为角色刷一套顶点色,用于控制/加权实时照明和环境光。
1.5 地形的优化
地形的数据和运行时细节管理都是通过Unity的Terrain Engine来完成的,unity提供了一套基于距离的内置LOD控制系统,可以提供各种细节调整。
地形的LOD控制有3个方面:
1. 地形自身的网格、网格细分密度;
2. 地形的渲染复杂度、阴影、材质和光照;
3. 地形上的修饰物(树木、草)。
对于这三个方面Unity均提供了相关优化或性能效果的平衡参数。
下面这些参数在运行时可修改。
地形网格的密度参数如下表所示:
上面这些参数的功能在表现效果上有重复,或者说内部实现途径不一样。
地形网格的渲染复杂度如下表:
地形上的修饰物(树木、花草)参数如下表:
这些参数有些是开关式的,如cast shadows、drawHeightmap以及 drawTreesAndFoliage,不过大多数参数是与距离相关的,这给了我们很多连续性调节性能和效果的空间,尤其是我们将地形分开的时候。
下图是距离和LOD的关系图:
根据当前角色的位置,找出当前那块地形,然后依次找出临近的地形块,以及远处地形块(当前地形可能多至4块,或恰好在4块地形的交界处)。
对地形分块,并且根据距离角色的位置分类之后,就可以对不同的地形块应用不同的LOD参数,在空间距离上调整细节。
地形上有很多细节,但是unity的地形为我们提供了现成的接口来做自适应的网格细分降级、细节的LOD降级、视距上的渲染剔除。这些和我们自己创建的物体不同(比如剑、枪和食物等),我们自己创建物体需要在设计时就考虑合并贴图到图集,放置到合适的图层,设置相机在这个图层的剔除距离等。
对于地形优化,可以考虑分块,这个可以用第三方插件,如T4M,以及地形的动态加载/卸载(当地图面积很大时)。如果地图面积不大,只有几个区块,并且不论什么情况下都会出现在视野内,就没有必要做动态加载/卸载这块,只需要考虑LOD调整就行了。
1.6 UI的优化
UI的优化关注的是图集的合理切分、Overdraw和网格的刷新,也就是动态元素和静态元素的分离。
1. 动态的UI元素
尤其是那些高频、几乎每帧都会更新Position、Rotation或者Scale的UI元素,应该单独放在一个画布下面,避免更新静态的UI元素,使Canvas.BuildBatch的CPU开销最小化。
如果动态UI元素无法避免,应该将其放置到RectTransform层级的最底层,否则会级联触发OnTransformChanged事件。
2. UI元素产生的绘制调用
每一个独立的画布和图集都会打断对UI元素的批处理,产生额外的绘制调用,在UI上使用自定义材质,也会打断对UI元素的批处理,增加绘制调用次数。
对于UI的绘制调用开销的关注应该是相对的、有条件的。比如,在绘制调用比较紧张的地方(游戏的战斗场景),应该注意UI的性能以及绘制调用。但是在一个独立的、只以UI元素为主的地方,比如游戏选择界面或加载时等地方,UI的性能和绘制调用就不会那么敏感,因为这个时候不会有挤占CPU或GPU时间的问题。
3. UI的图集划分
应该尽量确保一个情境下的UI元素使用一个图集,即使偶尔有几个精灵在多个UI情境下出现,如果这些精灵并不大,应该使用冗余的方式保证图集的独立,而不是企图在一个UI情境下使用多个图集,仅仅是为了复用几个小小的精灵,这不利于内存和资源的管理。
最后在创建UI元素的时候,会使用内置的一套默认图集,这个图集应该在所有UI元素上弃用。
4. 避免UI元素上的重绘
UI元素是按照透明物体的模式,从后往前渲染的,而不论具体UI元素是否透明,所以当UI面板展开,UI元素一层叠一层的时候,重绘在很多情况下是不可避免的,但对于静态的元素重绘则可以优化。
因为透明物体的正确渲染需要严格按照从后往前的顺序,所以在两个UI元素之间插入一个异类(使用了不同的图集、材质等方式)会打断UI元素的合批,产生额外的绘制调用。
5. UI上的文字
一个字母使用一个Quad渲染,所以界面上的文字要言简意赅。UGUI对字体很敏感,如果两个文字大小不同,或者样式不一致,字体文件中就会包含一个字母的多种形式。所以不要使用Test组件的BestFit特性,这会在字体文件中存放大小各异的字形。
Font文件可以根据需要分成两类,即静态字体文件和动态字体文件。第一类主要是有UI标题和Label等,而第二类主要是姓名输入、即时消息。第一类可以做到极大的优化,只包含那些已经使用的字符。而第二类对于unity来说则是一个动态维护字形(glyphs)的贴图。
6. 小心区块格式
这个格式会根据源精灵的大小产生网格,如果平铺精灵像素很小,平铺的区块会产生很多顶点和三角面。
7. UI和完美像素
在ScreenOverlay模式下会强制UI元素的边界和像素对齐,这会产生额外的CPU计算,应该慎用。
8. UI上的物理属性
对于物理属性,也需要避免遮盖。对于静态的UI元素,如果存在遮盖现象,应该只保留一个其中的一个Raycast Target属性。UI上的物理属性。
9. 其他
如果有大面积渐变色,首先,一般的压缩会破坏这种线性渐变色,其次会产生很大的贴图。对于这种需求,可以考虑继承UI中的Graphic类,通过自定义顶点色和GPU的线性插值产生渐变色,当然这种方式不太直观和方便。
1.7 物理引擎
实现物理碰撞时要做好层的划分,设置好FixedUpdate的频率、平衡物理引擎的性能。另外,如果游戏要进行多人联机,一定要注意物理引擎的使用。
考虑不同平台浮点运算单元的差异,如在计算浮点数时可能会产生极其微小的差异。如果使用状态同步产生的差异还可以接受(受制于需要同步的状态量,如果玩家需要控制同屏的许多个角色或对象,状态同步的数据量是不可接受的)。如果使用帧同步(受制于同步的指令量,同场的几百个玩家一起玩,则玩家的网格环境差异造成的体验也不太好),这种差异很可能被不断放大,造成执行效果在不同设备上完全不同。对于这种情况,需要禁止使用unity的物理引擎部分或禁用全部功能,而自己实现一套基于整数的物理引擎(部分数学计算通过查表和放大几百倍、几千倍实现),这样不同平台下的计算结果就不会存在差异,一般而言,整数计算速度也比浮点数高。
1.8 慎用后期效果
不推荐使用后期效果,这是一个逐像素的全屏操作。如果要,使用小一点的渲染纹理,且尽可能减少片元着色器的计算量。对于不同性能的目标平台,建议在系统设置当中暴露接口后缀使用一定的条件判断,允许玩家手动取消或者游戏自动取消某些后期效果。
如果可能,则把最终效果所需要的计算尽量集中到一个通道中完成。
1.9 慎用透明效果
不建议使用透明效果,如果可以,尽量多添加几个点来表达形状,透明意味着unity要排序,在GPU中逐像素地渲染,所以要尽量避免。
如果必须使用,一定要尽可能将多个透明图层进行合并,因为透明必将带来重绘问题,对于透明物体尽可能剔除正面/背面,减少渲染的像素数量。
尽可能减少对深度测试、剔除极不友好的操作,比如剪切、Alpha测试等操作,这些操作会直接使用Early-Z、隐藏面消除等技术作废。同时对现代的并行优化GPU而言(进行上一个问题的光栅化过程的同时还可以利用剩余的处理能力进行下一个物体的几何过程),这些操作也会造成这些优化失效。如果可能,使用混合来实现透明效果,但是即使使用混合,也要尽量避免透明物体的叠加,因为每一个透明物体的渲染都会迫使GPU对于每个像素执行一次片元着色器。
1.10 其他
粒子关注的重点则是重绘,美术人员对效果的追求是毫无节制的,但是对性能则是不敏感的,所以对美术人员提交的粒子特效需要重点关注。
关于阴影,阴影的caster一般是不输出颜色的,对于不同的材质的角色/物体,其产生阴影的材质是可以相同的。也就是说可以合批。
关于贴图,Unity为我们提供的压缩选项都是硬件压缩,也就是贴图资源,它们在内存中都处于压缩状态,但需要注意的是一旦平台不支持当前贴图的压缩格式,unity就会把贴图解压缩到内存中,通常使用unity默认的压缩格式解决大多数问题。
1.11 移动平台的特点
相对于PC来说,移动平台不论是内存还是运算速度,都相差了至少一个数量级,而且移动平台一般使用的是电池,因此其最大负荷是有限的,这就使我们不得不考虑如何最大化地优化着色器中的代码,使其提高运行效率。
1.11.1 一些指令的运算速度
首先对于现代的PC显卡,已经很智能了,能够识别出一个y*y*y*y为pow(y,4),并对应地做优化,比如优化为两个y*=y,y*=y,这样就可以把运算从4次减少为3次,但是移动平台目前还无法做到这一点,因此你可能不得不使用pow指令,或者在着色器的代码中手动进行数学运算上的优化。
其次,现在的PC显卡上,数学运算指令的非常快的,基本上不用考虑几条数学运算指令的代价,但是移动平台的情况不是这样。pow、sin、cos这些数学函数在移动平台上的运算速度很慢,因此要尽量避免使用它们,把这些计算移到vertex函数中,或者使用一张小的贴图,通过查表来代替一些复杂的数学运算。如果某些运算确定只需要执行一次,那么把他移动到CPU端计算并传给GPU,性能会得到提升。即使把这种计算交给顶点着色器而不是片元着色器,也比CPU计算一次节约计算力,除此之外还要注意,noise()这个在标准Cg函数库中的函数,在绝大多数主流平台上未实现,在大多数使用这个函数的情形下,使用噪声贴图替代是个更好的方法。
对于tex2D/texCUBE指令,在移动平台上,一个读取贴图的指令和一个普通的数学运算指令消耗大概相差至少一个数量级;在PC平台上,这个差别更大,大概在两个数量级以上,这是因为在PC平台上,数学运算指令比tex2D的速度快了很多。
1.11.2 几何复杂度
顶点数量可以成为一个影响渲染效率的重要阈值,在IOS上,一个参考值是每一帧渲染的物体,当前视口内的顶点总数不要超过10的6次方个,因为这是一个IOS底层驱动默认的顶点数据缓冲区的大小,超过这个数字,很可能导致驱动底层做一些开销巨大的分割操作。通常而言,将这个数值保持在10的四次方~2*10的四次方是比较理想的值,因为如果考虑做跨平台的游戏开发,这个数字对于绝大多数的Android 设备也能接受,实际生产中,建议采用LOD Group和遮挡剔除等技术来减少顶点数。
1.11.3 贴图的问题
对于贴图,其大小应该尽量是2的幂次方,因为所有的计算和存储最终都要以2的幂次方为单位。尽管平台可能支持非2次方幂的贴图,而且可能支持得很好,但是它存储和查找的效率总不会超过最接近的2的幂次方贴图。
在各种移动平台上,能支持的最大贴图是多少?实际开发中应该使用多大贴图?平台能支持的最大贴图和硬件密切相关,即使同为Android平台,因为硬件不同,所支持的最大贴图也是不一样的。PowerVR SGX540所支持的最大贴图为2048X2048,Tegra 2位2048X2048,Adreno 200为4096X4096,Mali 400MP为4096X4096。
那么GPU所支持的最大贴图是不是最佳的贴图尺寸?对于这个问题,为了应用在平台间移植的可能性,建议以1024为标杆,最大不要超过2048。在IOS游戏《无尽之剑》内,场景内的角色模型大概使用了3000个顶点,角色的贴图却高达2048个,但是对于移动应用来说,3000个顶点的模型已经是高模了,但是这个游戏里任何时刻最多只会出现2个角色,所以才会把角色顶点数设置的这么多。
对于贴图的mipmaps选项,如果游戏是3D,记得一定要打开mipmap选项,如果关闭了该选项,在iPad上只会导致大概3ms的性能损失,但是在三星的Tegra上可能是宕机。
当把所有的贴图导入unity中时,都会把它们处理为unity所支持的格式,这种处理包括分类、纹理压缩、mipmap的制作等。使得任何贴图文件的原始尺寸和类型与最终发布时的尺寸和类型完全无关,这意味着可以使用自己的格式和尺寸大小来保存贴图文件,而在发布应用时使用一个平台相关的大小和类型。
ETC、DXT和PVRTC都是硬件压缩,如果硬件不支持,而你又使用了,那么将在贴图加载时由CPU来解压缩。建议使用unity默认的压缩格式,它会自动针对平台进行适配并压缩纹理,可以在图像的质量、硬件处理效率和图像大小之间得到良好的平衡。
ETC是在Android平台上硬件普遍支持的一种压缩格式,推荐在Android上使用ETC的压缩格式,但是ETC不支持Alpha通道。但是ETC不支持Alpha通道。因此当贴图含有Alpha通道时,Unity默认在贴图大小、质量以及渲染速度之间的平衡格式是RGBA-16bit。不过对于硬件Tegra,最好使用DXT5的贴图压缩方式。
如果你的目标平台是IOS或者PowerVR,应选择PVRTC的贴图压缩格式。
1.11.4 数据类型的使用方式
fixed或者说lowp的精度是8位,在OpenGL ES2.0拥有的最小值域为[-2,2],适合作为颜色和其他单位化的方向矢量,half或者说mediump的精度为16位,比较适合表示三维空间的坐标和,用于进行数学运算的标量。尽管大多数现在PC的GPU里,fixed直接当做half处理,但移动GPU基于能耗 考虑仍然保留并处理fixed精度。所以一旦确定了特定变量的变化范围在某个精度允许的范围内,尽可能使用这种精度处理。
不论是PC还是移动的GPU,其双精度(double)的处理效率相比单精度直接降低了一个数量级,所以尽可能不要使用双精度来处理。
在移动平台上,关于这些数据类型的运算效率,可以这样考虑:half/mediump是float/highp的两倍,fixed/lowp又是half/mediump的两倍。除此之外尽量避免在这三个数据类型之间进行转换,也避免在fixed/lowp上做调配操作。这些操作都会引擎性能和精度的损失。最后,Adreno是个例外,Adreno的硬件都这类精度并不敏感。
1.11.5 变量的使用
大多数GPU上会尽量减少从vertex函数传递到fragment函数的参数数量,把变量包装起来,比如把两个fixed2打包到一个fixed4中,但是在PowerVR上,也就是IOS的GPU上不上这样,PowerVR对变量数量不敏感,其次如果变量是用来读取贴图的UV变量,尽量使用独立的UV变量,不要使用一个四元数来包装两个二元数。在进入fragment函数前,如果可能,确定uv,这样PowerVR就能提前读取贴图的值,从而避免在fragment函数中读取贴图。而在移动平台上,tex2D是一个消耗性能的操作。
版权声明:本文为CSDN博主「小橙子0」的原创文章,
遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cgy56191948/article/details/103403119