前言:
元宵节快到了,想要做一个上千盏孔明灯的场景,由于不懂建模,于是在Asset Story找了一套模型,结果找的灯笼模型精细度超标,当在游戏场景中实例化出四百个孔明灯时,帧率只有十帧左右
为了使得场景可以流畅的运行下去,需要通过性能优化来提升游戏的流畅度,要对游戏场景进行性能优化,首先需要了解是哪些原因消耗了过量的硬件资源,然后做出针对性的解决方法
分析游戏瓶颈问题:
1,通过Profiler分析游戏运行时硬件状态
首先通过Profiler
来分析场景运行时的性能分析数据,可以通过Window - Analysis - Profiler
来打开该窗口,也可以快捷键Ctrl + 7来打开,运行游戏场景,获取Profiler
显示的数据:
通过分析数据,发现在场景开始实例化出孔明灯时,GPU直接跑满,而CPU
在Rendering
方面也是处于高负荷状态。很明显时游戏渲染压力过大,其实可以直接想到,但是我们还是需要通过数据的分析来得出结论,保证严谨性
在游戏跑满时,同屏面数直接达到了194M,这是很大的一个量级,而突变点就是在于场景中添加400个孔明灯时:
2,分析游戏渲染资源消耗
由于场景是用的一套低模的资源包,应该不会出现问题,产生卡顿的原因,猜测是实例化出灯笼的模型出现了问题,首先通过Scene
窗口看了一下灯笼模型的Tris
(三角面)。确实,是一个高精度模型,于是猜测是不是由于场景中Tris
太多造成性能瓶颈的呢
为了验证这样的想法,通过脚本来获取该物体的三角面数,新建一个场景,并在场景中只保留灯笼这一个模型,编写一个脚本获取所有物体的三角面数,并显示在UI上:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class DisplayTris: MonoBehaviour
{private GameObject[] objects=null; //存储场景中物体的数组public Text gameText; //显示在UI上的文本private int numTris; //三角面个数private float timeF=0; // Update is called once per framevoid Update){ifTime.time>timeF){numTris = 0;//获取场景中所有物体objects = FindObjectsOfTypetypeofGameObject)) as GameObject[];Debug.Logobjects.Length);foreach GameObject ob in objects){if ob.GetComponent<MeshFilter>) != null){//最后获取的Length是所有三角面的三个顶点长度总和,所以要除以三numTris += ob.GetComponent<MeshFilter>).sharedMesh.triangles.Length/3;}}gameText.text = " 面数为:" + numTris.ToString);timeF = Time.time + 5f;}}
}
此时,如果直接运行场景,一般都会报错,因为我们使用sharedMesh.triangles
来读取模型的信息时,是没有权限的。为了可以访问模型的三角面信息,需要我们将模型的Read/Write Enable
勾选上:
运行场景进行测试,发现该模型足足有 八万多个面,很明显这样一个量级的模型不适合大量的存在于游戏场景中
同时也注意到相机中渲染的三角面到达了八十万个面,通过分析数据,要降低资源消耗,就需要在降低模型精度和更改相机渲染模式两方面入手
开始性能优化:
1,减少模型三角面
Unity作为实时渲染的游戏引擎,模型的面数量级在千就算是比较多的了,一般动态的物体的Tris要尽量的少于3000个。在本场景中的灯笼模型的三角面达到了八万个,而又要实例化出四百个同样的孔明灯,产生的数据量是巨大的,这个时候就需要适当的降低模型的三角面来提升游戏的性能
关于模型Tris的具体个数
- 大原则肯定是在保证显示效果的前提下尽量少,为了提供参考,给一些不太严谨的数据角色等关键模型移动端尽量保持在万面一下,其他模型量级尽量不过千
- 对于单个模型的面数不是关键,最终要保证同屏的面数在可控的范围内,这样才可以保证游戏有一个比较好的渲染性能
最简单的方式,就是交给美工来修改。不过没有美工而自己又不会建模也没有关系,我们可以借助Unity中的一些插件来自己降低模型的Tris
,不过这种处理方式往往是通过随机删面来实现减面的,可能会造成模型的瑕疵。还是推荐使用专门的建模工具来完成减面
通过模型减面后(孔明灯Tris保持在2700左右),重新运行游戏场景,可以看到帧数大幅度的提升,大概到了七八十帧左右,同屏面数也大概保持在八十万面,不过要是在移动端依旧还是一个比较高的数值,当然我们可以通过继续减面来提升游戏性能
本案例由于对于建模工具的使用能力有限,没法更进一步实现减面,有会建模的小伙伴可以尝试进一步缩减模型的三角面数量,这样可以进一步的提升性能
2,调整图形渲染路径
我们可以在相机的Camera
组件上看到这个Rendering Path
这个选项:
可以通过选择不同的Rendering Path
来确定对场景的渲染模式,我们知道画面中的物体是通过与光线的结合来显示出来的,对于光线的不同处理方式表现出来的画面也不同
渲染路径与游戏性能的关系
- 不同的渲染路径对于光线的处理不同,而关于其对于游戏性能的影响应该是比较好理解的,对于光线处理越复杂,则视图中的Tris越多,所以视图中的Tris并不是单纯的所有模型的Tris相加,而是模型与光线结合的结果
基于这样的原理,当想要表现出更多的光影效果时,需要消耗的性能会大幅度的增加,所以我们可以根据需求来选择合适的Randering Path
,关于几种Randering Path
适用如下:
Forward Rendering
:项目不使用大量的实时灯光,或者如果照明保真度对不重要,那么这种呈现路径是一个很好的选择Deferred Shading
:项目有大量的实时灯光,并且需要高水平的灯光保真度,而你的目标硬件支持延期着色,那么这种渲染路径可能是你的项目的一个很好的选择Legacy Deferred
:类似于Deferred Shading
,只不过它使用了不同的技术和不同的折衷方法。它不支持基于 Unity 5的物理标准着色器Legacy Vertex Lit
:遗留的顶点光照是渲染路径与最低的照明保真度和不支持实时阴影
通过对场景测试,很明显,Legacy Vertex Lit
虽然大幅度的提升了游戏帧数,但是基本损失了场景光照效果,这对于一个要刻意体现光影氛围的游戏是不可接受的,但是如果你的场景不那么看重光影,也可以通过这种方式选择该渲染路径来提升游戏流畅度,而为了保持显示效果,我莫得办法,只能选择最消耗资源的Deferred Shading
渲染路径
3,动态合批降Draw Call简称DC)
基本原理:
在开始通过动态合批来提升性能之前,需要先理解为什么可以通过这样的方式来提升性能
什么是
Draw call
:
Draw Call
就是CPU调用图形编程接口,比如DirectX
或OpenGL
,来命令GPU进行渲染的操作。简单来说,GPU要想工作,需要CPU线提供数据和命令,而把 每一个模型的数据以及处理数据方式的命令提交给GPU的处理方式就是Draw Call
由于场景模型数量巨大,Draw Call
通常是资源密集型的,图形 API 为每个绘制调用做大量的工作,在 CPU 方面造成性能开销。这主要是由绘制调用之间的状态更改例如切换到不同的材质)引起的,这将导致图形驱动程序中的资源密集型验证和转换步骤。
由于Draw Call
会消耗大量CPU性能,所以我们在使用Unity引擎时,需要尽可能的来降低其的数量,而主要的两种方式来解决:
- 动态合批:对于足够小的网格,这转换他们在 CPU 上的顶点,组合许多相似的顶点在一起,并绘制他们在一起
- 静态合批:结合多个静态不移动)游戏物体到一个大网格,一次性的渲染
由于这个场景使用了大量的移动游戏对象,在游戏运行时,需要大量的Draw Call
来绘制图形,而占用CPU资源。同时,这些模型又是完全相同的,所以我们可以通过第一种方式来降低Draw Call
,但是注意,前面提到这种方式的使用要保证是对于足够小的网格,所以其使用应遵循一定的使用原则
动态合批限制条件:
- 批处理动态游戏对象对每个顶点有一定的开销,所以批处理只适用于包含不超过900个顶点属性,不超过300个顶点的网格 , 如果你的
shader
是使用Vertex Position
、Normal
和single UV
,那么你可以批量多达300顶点,而如果你的shader
使用顶点位置,法线,UV0,UV1和切线,就只能处理在180顶点以下- 如果游戏对象在变换中包含镜像,那么它们就不会被批处理例如,游戏对象 aA的 正一缩放和游戏对象 b 的负一缩放就不能在一起批处理)
- 使用不同的材质会导致游戏对象不能一起批处理,即使它们本质上是相同的。
shadow caster rendering
是个例外- 游戏对象 有额外的渲染参数: 光照图索引和偏移/缩放到光照图。一般来说,动态灯光映射游戏对象应该指向完全相同的灯光映射位置进行批处理
- 几乎所有的联合着色器都支持几个光源 ,有效地为他们做额外的通行证,绘制要求“额外的每像素灯”没有批处理,遗产延迟光预通行) 有动态批处理禁用,因为它必须绘制游戏对象两次
操作流程:
首先可以通过Profiler
来查看游戏运行时的Draw Call
,打开Profiler点击Rendering
可以在下面的状态栏看到事实的Draw Call
的数量,如果其数值过高,可以通过静态或者动态合批的方式来降低其数值:
开启动态合批的方式很简单,首先需要在Project Setting
里面找到Player
选项中找到Other Setting
里面勾选上Dynamic Batching
:
而对于满足条件,可以动态合批的游戏对象,可以在其材质中勾选上Enable GPU Instancing
:
4,遮罩剔除
原理:
遮罩剔除非常适合Y轴元素复杂的场景,如果场景非常复杂,可以通过这种方式节省大量资源,其原理在于,Unity引擎会将被物体完全遮挡的物体不进行渲染:
首先相机对于场景中物体进行渲染与否,取决于其是存在于相机的视锥形内,如果存在,则对其进行渲染,即使被其他物体遮挡,不会在玩家的屏幕上显示出来,依旧会消耗资源去渲染,这样就会造成资源浪费
相机在Unity中的视野范围是从一个点扩散出的一个锥形,但是最顶点的三角被切掉了,类似一个梯形台子,称其为视锥:
而遮罩剔除就是为了避免这样的情况,即使物体在相机的视锥内,如果被设置为遮罩剔除的物体遮挡,即玩家无法直接看到,则不会对其进行渲染,这样就会减少资源的消耗,提升游戏性能
操作:
点击Window -> Rendering -> Occlusion Culling
可以打开遮罩剔除面板:
在进行遮罩剔除之前,我们可以看到,场景中看不到的物体依旧会进行渲染,我们在场景中添加一个Cube
,被房子完全遮挡起来,但是当我们的镜头对着房子时,Cube
依旧会被渲染出来:
首先遮罩剔除是对于静态物体来使用的,移动的物体无法进行遮罩剔除,我们将需要设置遮罩剔除的物体,勾选为静态,在上面的案例中,我们只需要将房子的Occluder Static
勾选,并点击Bake
,就可以将场景渲染,这样就可以实现遮罩剔除的效果:
完成上面的操作后,我i们运行场景,来测试显示结果,可以看到Cube被完全隐藏起来:
总结:
进行性能优化的方式有很多,可以根据实际的需要结合游戏具体情况来采用不同的方式进行对应的性能优化
最直接简单的就是降低画面表现来提升性能,这么做有很大的空间来提升性能,但是是有很大代价的
而关于动态合批降DC、遮罩剔除这种提升性能的方式条件苛刻,适用的范围很有限,在一些特殊的情况下来使用才有比较好的效果