原文Optimizing graphics rendering in Unity games。这里对这篇文章的粗略翻译,当作自己的笔记。
介绍
在这篇文章中我们将会学习Unity在渲染一帧的时候其内部的发生的事情,在渲染的时候会发生什么类型的性能问题同时我们怎样去处理这些性能问题。
在阅读这篇文章之前,必须明白没有一个同样的尺度来提高渲染的性能问题。在我们游戏中影响性能问题的原因有很多同时非常依赖游戏运行的硬件和操作系统。通过调查,实验和细心的分析实验结果来解决性能问题是我们最重要的方式。
这篇文章包含的信息主要是大部分常见的性能问题的修改建议和一些更深层次的阅读链接。我们的游戏中可能也存在这其中的一个问题或者一些问题或者都没有包含。但是这篇文章仍然将帮助我们理解我们自己的问题同时给予我们去搜索我们问题的知识和线索。
渲染的简介
在我们开始介绍之前,让我们快速简单看下Unity渲染一帧时其内部发生了什么。理解事件流和正确的术语将会帮助我们 理解,调查和解决我们的性能问题。
注:在这篇文章中,我们将会使用”对象(object)”术语来表示一个我们游戏的渲染的对象。任何渲染组件的GameObject将会被统称为对象。
在最基本的层面上,渲染可以描述为一下几步:
- CPU计算出必须渲染的对象和决定怎么去绘制。
- CPU发送指令到GPU。
- GPU根据CPU的指令来确定绘制的内容。
现在让我们来更近一步看看究竟发生了什么。这篇文章后面将会详细的介绍上面的每一个步骤。但是现在让我们先来熟悉下文中使用的词语和理解CPU和GPU在渲染中扮演的不同角色。
描述渲染的词语通常用渲染管线(rendering pipeline),记住这是一个非常有用的情景。高效的渲染都是关于保持更高信息的传送能力。
对于每一个渲染的帧,CPU会做如下的工作:
- CPU会检查场景中的每一个对象来决定这个对象是否需要被渲染。只有当这个对象满足特定的条件的时候才会被渲染;例如,它的包围盒的一些部分在相机视锥体内。一个对象如果将不会被渲染我们称之为这个对象被裁剪了(culled)。关于相机的视锥体和视锥体裁剪的更多信息可以看着这篇文章。
- CPU搜集每一个将会被渲染的对象的信息然后将这些数据分类放入叫做draw calls命令中。一个draw call包含了一个mesh和这个mesh怎样被渲染的数据;例如,这个mesh用哪一张贴图。基于这些情况,共享设置的图像会合并到一些相同的draw call中。合并不同对象的数据到同一个draw call中我们称之为batching。
- CPU为每次draw call创建一个叫batch的数据包。batchs有可能包含了draw calls之外的数据,但是这种情况不像常见的一个性能问题,因此我们不会在这篇文章里面来讨论。
对于每个draw call包含的batchs,CPU现在必须要入如下的工作:
- CPU可以向GPU发送一个命令来改变一些被统称为渲染状态的变量。这个命令叫做SetPass call。SetPass call告诉GPU哪个设置被用来渲染下一个mesh。只有当上一个mesh渲染完成下一个mesh准备渲染的时候需要改变渲染状态的时候才需要发送SetPass call命令。
- CPU发送draw call命令到GPU。draw call会指示GPU使用上一次设置好的SetPass call去渲染制定的mesh。
- 基于这些情况,batchs的数据被渲染可能需要不止一个pass。一个pass是一段着色器的代码,使用一个新的pass需要改变渲染状态。对于一个batchs中的每个pass,CPU必须发送一个新的SetPass call命令同时必须再次发送draw call命令。这个pass才会生效。
同时,GPU会做如下的工作:
- GPU按照CPU发送的顺序来处理CPU发送的任务。
- 如果当前的任务是SetPass call,GPU就更新渲染状态。
- 如果当前的任务是draw call, GPU就渲染这个draw call的mesh。这个是分阶段进行的,每个阶段单独定义在着色器代码中。这部分渲染比较复杂我们不会详细的讨论,但是这部分对于我们去理解顶点着色器(vertex shader)这部分代码和片段着色器(fragment shader)这部分代码非常有用。顶点着色器是用来告诉GPU怎么去处理mesh的顶点数据的代码,片段着色器使用来怎样去处理每个单独像素代码。
- GPU重复处理从CPU发送过来的任务,直到所有的任务都处理完成。
现在我们理解了当Unity渲染一帧时其内部发生了什么,接下来让我们思考下当渲染发生的时候会随之发生什么问题。
渲染问题的类型
关于渲染需要理解的最终的一点:为了渲染一帧,CPU和GPU必须完成各自所有的任务。如果任何一个任务花费了太长的时间才完成,这个任务将会导致帧的渲染延迟。
渲染问题有两个类型的根本的原因。第一种类型是管线无效率导致的。渲染管线中一个或者多个花费太长的时间才完成,中断了数据的平滑传送,这会导致管线无效率。管线内的低效率被称为瓶颈。第二种类型是简单的尝试将太多的数据压入到管线中。即使再有效率的管线处理一帧时也会有数据量的限制。
当CPU花费太长的时间去执行它的渲染任务导致我们的游戏花费很长的时间去渲染一帧,我们的游戏就称为CPU性能受限(译:性能瓶颈在CPU这边)。当GPU花费太长的时间去执行它的渲染任务导致我们的游戏花费很长的时间去渲染一帧,我们的游戏称之为GPU性能受限(译:性能瓶颈在GPU这边) 。
理解渲染的问题
在我们做出任何改变之前使用性能分析工具去理解性能问题的原因非常重要。不同的问题需要不同的解决方案。同样重要的是衡量我们每次的改动所带来的影响;修复性能问题是一个在各方面问题中取平衡的行为,改善这方面的性能问题会降低另一方面的性能。
我们将使用两个工具来帮助我们理解和修复我们的渲染性能问题:Profiler window 和 Frame Debugger 。这两个工具都是Unity内置的。
Profiler window
Profiler window允许我们查看关于我们游戏执行情况的实时数据。我们可以使用Profiler window工具查看很多方面的数据,包括内存使用,渲染管线和用户脚本的性能。
如果你对Profiler window还不熟悉,这个Unity手册的页面是一个非常好的介绍,这个教程详细展示了怎样使用这个工具。
Frame Debugger
Frame Debugger可以让我们查看一帧是怎样一步一步渲染出来的。使用Frame Debugger,我们可以看到很多详细的信息,例如每一个draw call中渲染的内容,没一个draw call的着色器的属性和发送到GPU的时间顺序。这些信息帮助我们去理解我们的游戏怎样被渲染的同时知道我们需要提高性能的地方。
如果你对Frame Debugger还不熟悉,这个Unity手册页面是一个非常有用的介绍它是怎样工作的,这个视频教程展示了它怎样使用。
确定导致性能问题的原因
在我们尝试改进我们游戏的渲染性能之前,我们必须先确定我们的游戏运行慢是由于渲染问题导致的。如果我们的运行慢的原因是由于用户的脚本过于复杂导致的那么尝试着优化渲染性能是没有意义的!如果你还不确定你的性能问题是否和渲染有关,你应该先看看这个教程。
一旦我们确定了我们的问题是和渲染相关的,我们必须先确定我们的游戏是CPU性能受限的还是GPU性能受限的。不同的问题需要不同的解决方案,所以在解决问题之前首先得理解问题产生的原因。如果我们还不确定我们的游戏是CPU性能受限的还是GPU性能受限的,你应该先看看这个教程。
如果我们确定我们的问题是和渲染有关同时我们知道了我们的游戏是CPU性能受限还是GPU性能受限的,我们就已经准备好了。
CPU性能受限
一般来讲,为了渲染一帧CPU必须执行的工作可分为三个类别:
- 决定一定会绘制的内容
- 准备发送到GPU的命令
- 发送命令发GPU
这些广义的类别包含了很多单独的任务并且这些任务可能在多线程中执行。线程允许单独的任务同时运行;然而一个线程执行一个任务,另外一个线程可以完全独立的执行任务。这意味着任务的处理可以更加的快速。当渲染的任务分开在不同的线程上执行,这就是所谓的多线程渲染;
在Unity的渲染过程中有三种类型的线程: 主线程,渲染线程和辅助线程(worker threads)。主线程是我们游戏中CPU处理任务的主要的地方,包括一些渲染的任务。渲染线程是比较特殊的线程,它的工作是发送渲染命令到GPU。辅助线程处理每个单独的任务,例如裁剪或者mesh的蒙皮。哪个任务由哪个线程来处理这取决于我们的游戏设置和我们游戏运行的硬件。例如,我们的目标硬件有更多的CPU核心,就有越多的辅助线程被分配。基于这个原因,在目标硬件上来剖析我们的游戏是非常重要的;我们的游戏可能在不同的平台上表现的非常不同。
由于多线程渲染比较复杂并且依赖于不同的硬件,在我们改进性能之前我们必须明白哪个线程是造成我们游戏称为CPU性能受限的原因。如果我们的游戏运行慢是由于裁剪操作在一个线程上花费了太多的时间,那么它就不能帮助我们减少在不同线程上发送命令到GPU所花费的时间(译:因为裁剪操作花费的时间可能远超过辅助线程所节省的时间)。
注:并不是所有的平台都支持多线程渲染;在写这篇文章的时候,WebGL还没有支持这个特性。在不支持多线程渲染的平台上,所有的CPU任务都在同一个线程里面处理。如果我们是在这种平台上CPU性能受限,优化所有的CPU任务将会改进CPU的性能。如果这个情况发生在我们的游戏中,我们应该阅读接下来的所有部分同时思考哪些优化可能最适合我们的游戏。
图形工作
在Player Settings里面的图形工作的设置项决定了Unity是否使用辅助线程来处理原本需要在主线程上处理的渲染任务,如果不选择则这些工作在主线程里面处理。在这个特性可用的平台上,它可以带来相当大的性能的提升。我们应该分析我们游戏在图形选项开启和不开启的情况,观察它给我们游戏性能带来的影响。
找出导致性能问题的任务
我们可以使用Profiler window来确定导致我们游戏CPU性能受限的任务。这个教程展示了怎样去确定问题的位置。
现在我们了解了哪些任务导致了我们游戏的CPU性能受限,让我们来看看一些常见问题和这些问题的解决技术。
发送命令到GPU
发送命令到GPU花费的时间是我们游戏中一个最普遍的导致CPU性能受限的原因。在大多数平台上这个任务是在渲染线程执行的,尽管在一些平台(例如,PS4)可能使用辅助线程执行。
发送渲染命令到GPU时最消耗性能的操作是SetPass call。如果你的游戏CPU性能受限是由于向GPU发送命令,减少SetPass calls的数量可能是最好的改进性能的途径。
我们可以在Unity的Profiler window工具中看到有多少SetPass calls和batches被发送到渲染分析器里面。在游戏性能受到影响之前可以发送多少数量的SetPass calls命名这取决于目标硬件;高端的电脑相比于手机能够发送更多的SetPass calls。
SetPass calls的数量和它关联的batches数量取决于几个因素,我们会在接下来的内容详细的讨论这几个主题。然而,通常情况是这样的:
- 减少batches的数量同时最好使更多的对象共享同一个渲染的状态。在大多数情况下可以减少SetPass calls的数量。
- 减少SetPass calls的数量,在大多数情况下可以改进CPU的性能。
如果减少了batches的数量并没有降低SetPass calls的数量,这样任然可能带来性能上的改进。这是因为CPU能够更有效率的处理的batch,即使batches包含相同数量的mesh数据。
一般来说有三个技术减少batches和SetPass calls的数量。我们将更加深入的研究下面的每个技术:
- 减少渲染对象的数量将很有可能减少batches和SetPass calls的数量。
- 减少每个最终会渲染的对象的渲染时间通常会减少SestPass calls。
- 合并需要渲染的对象的数据到更少的batches将会减少batches的数量。
不同的技术适用于不同的游戏,所以我们应该考虑这些所有的选项来决定哪个技术在我们游戏和实验中有效。
减少渲染对象的数量
减少最终会渲染的对象的数量是减少batches和SetPass calls数量的最简单的途径。这里有几个我们可以同来减少渲染对象的技术。
- 简单的减少在我们场景中的可见对象的数量是一个有效的解决技术。例如,假设我们在人群中渲染了大量的不同的角色,我们尝试着逐渐减少场景中的角色。如果场景看起来逐渐更好同时性能逐渐提高,那么这个技术将比更加复杂在技术更加快速的解决我们的问题。
- 我们可以使用摄像机的远裁剪面来降低我们的摄像机的绘制距离。如果对象的距离超出了摄像机设置的这个属性的距离,那么这个对象就不会被渲染。如果我们想伪装这些因为超出了摄像机的远裁剪面而不可见的对象,我们可以尝试用雾来遮住这些对象。
- 对于基于距离来隐藏对象更细致的做法,我们可以使用我们摄像机的层裁剪距离属性为不同层的对象提供自定义的裁剪距离。这个技术对于我们有很多装饰细节的前景是个非常有用优化技术; 我们可以把这些细节在比大型的地形的特征点更短距离的时候隐藏。
- 我们可以使用一个叫做封闭裁剪的技术去隐藏那些被其他对象遮盖住的对象。例如,如果我们场景上有一个很大的建筑物我们可以使用封闭裁剪来关闭(disable)在这个建筑物后面的渲染对象。Unity的封闭裁剪并不适合所有的场景,这个技术会增加额外的CPU消耗和更加复杂的设置,但是它确实能给一些场景带来性能上的改善。这篇Unity发布的封闭裁剪的最佳实践是一篇很好的关于这个主题的指导。此外使用Unity的封闭裁剪我们也可以以自己的方式去手动停用(deactivating)一些我们知道用户已经不可见的对象。例如,如果我们场景中包含了用来过场动画用的对象,但是这些对像在过场动画展现之前和之后都不可见,我们应该停用(deactivate)它们。通常结合我们自己的游戏内容知识比要求Unity来解决我们的问题更加有效率。
减少每个最终会渲染的对象的渲染次数。
实时光照、阴影和反射为游戏增加了很好的现实效果,但是这些效果非常的消耗性能。使用这些特性会带来很多对象被渲染多次,这些相当影响性能。
这些特性的性能影响取决于我们游戏选择的rendering path。Rendering path 是指绘制场景的时候对象执行计算的顺序,rendering path之间的主要区别是它们是怎样处理实时光照、阴影和反射的。一般而言,Deferred Rendering 可能是我们游戏运行在高端硬件上使用了大量的实时光照、阴影和反射的更好的选择。Forward Rendering可能更适合用在我们游戏运行在低端硬件上同事没有使用这些特性的情况。然而,如果我们希望使用实时光照、阴影和折射这些特性这是个非常复杂的问题,最好是先去调查这个主题然后尝试做些尝试。这个Unity手册的页面提供了更多的关于不同Unity的rendering paths信息,这是个不错的起点。这个教程包含了Unity的光照的内容。
不管选择哪个rendering path,使用了实时光照、阴影和反射都会影响游戏的性能,重要的是我们要知道怎样去优化它们。
- Unity的动态光照是一个非常复杂的主题,深入的讨论这个主题已经超出了这篇文章的范围了,但是这个教程非常好的介绍了这个主题。这个Unity的手册页面非常详细介绍了场景的光照优化。
- Unity的动态光照是非常消耗性能的。当我们场景中包含了一些不会移动的对象,比如 背景,我们可以使用一个叫做烘焙(baking)的技术来提前计算好场景光照这样游戏运行起来时光照不再需要计算了。这个教程介绍了这个技术,同时Unity手册的这个部分内容详细介绍了关照的烘焙。
- 如果我们希望在游戏中使用实时的阴影,这可能有非常多的优化的地方。Unity手册的这个页面是个非常好的对阴影属性的指南。阴影可以在Quality Settings中调整同事这些调整影响着阴影的显示效果和性能。例如,我们可以使用Shadow Distance属性来去保证只有近距离的对象才会投射阴影。
- 反射探测创建了实时反射但是在批次(batches)的时候会非常消耗性能。在性能消耗比较集中的时候最好保持反射在我们游戏中最低限度的使用,同时尽可能的在使用反射的地方优化它们。Unity手册的这个页面是一个非常好的优化反射探测的指南。
将渲染对象合并到更少的批次(batches)里
当某些条件满足时,一个批次能够包含多个对象的数据。对象可以批处理的必须具备一下条件:
- 共享同一个材质的同一个实例
- 设置了同一个material(例如, 贴图,着色器和着色器的参数)
符合批处理条件的对象能够提高性能。尽管如此,我们任然需要和所有优化技术一样非常细心的分析批处理,确保批处理的消耗不会超过性能优化所得。
这里有几种不同的技术对符合批处理条件的对象进行批处理:
- 静态合批是一个允许在附近的符合合批的对象并且不会移动的进行合批的技术。静态合批非常有用的一个例子就是对于一堆相似的对象的处理。例如卵石。这个Unity手册的页面包含了在我们游戏中设置静态合批的说明。静态合批会导致更高的内存占用,所以我们应该在分析我们的游戏的时候记住这点。
- 动态合批是另外一种允许允许Unity批处理符合合批的对象,不管这些对象是否会移动。这里有一些对能够使用批处理的对象的限制。这个Unity手册的页面列出了这些限制。动态合批对CPU使用会有影响,它会引起比动态合批节省的部分更多的CPU时间消耗。当我们尝试这个技术的时候我们应该记住这个消耗同时非常谨慎的使用这个技术。
- Unity的UI元素合批会更复杂一点,这会影响我们UI的布局。这个来自Unite曼谷2015的视频对这个主题很好的全面的介绍,Unity UI优化的指南提供了怎样确保UI合批工作如我们希望的那样处理的更深入的信息。
- GPU instancing是允许大量相同对象变得非常有效的合批的一个技术。这个技术的使用有一些限制并且不是所有的硬件都支持,但是如果我们游戏里一次有很多相同的对象显示在屏幕上我们可能会从这个技术里面得到收益。这个Unity手册的页面包含了Unity 中GPU instancing的一个指南,这个指南介绍了这个技术怎样使用,哪些平台支持这个技术,在什么样的环境下这个技术会有利于我们的有利。
- 图集是一个在多张贴图合并成一张大的贴图的地方使用的技术。它常被用在2D游戏和UI系统中,同样也可以用在3D游戏中。当为我们的游戏创作一个作品的时候我们使用这个技术,我们能够确保对象共享这张贴图,因此这些对象也是满足合批的条件的。Unity内置了一个针对2D游戏中使用图集的工具,它叫做Sprite Packer。
- 无论是在编辑器中或者通过运行时的代码,我们都可以手动合并共享同一个材质和贴图的网格(meshs)。当使用合并网格这个方法时,我们必须注意到每个层级的对象任然会执行阴影,光照和裁剪这些操作;这意味着合并网格所带来的性能提升会被不再能够裁剪那些不被渲染的对象所抵消。如果我们希望研究这个方法,我们应该测试Mesh.CombineMeshes函数。Unity的标准资源包里面的CombineChildren就是使用这个技术的例子。
- 当我们在脚本里访问Renderer.materialAPI时我们必须非常小心。这个API会复制一份这个材质然后返回复制的这份材质的引用。如果这个渲染器(renderer)是合批的一部分的话,那么这个操作会中断批处理操作。因为渲染器不再有相同的材质的相同实例引用。如果我们希望在脚本里访问合批的对象的材质,我们应该使用Renderer.shareMaterial这个API。
裁剪,排序和合批
裁剪,收集将要渲染的对象的数据,将这些数据分类到不同类型的批次中以及生成GPU命令这些都有可能导致CPU性能受限。这些任务不管是在主线程执行还是在独立的辅助(worker)线程上执行,这取决于你的游戏的设置和目标硬件。
- 裁剪不太会是一个非常耗时的操作,但是减少不必要的裁剪可以有助于性能的提高。所有活动场景中的对象对每个相机都有一份开销。即使这些对象所在的层没有被渲染。为了减少这些开销,我们应该关闭(disable)相机和没有用的渲染器,以及可以停用(deactivate)没有用的渲染器。
- 合批能够很大程度上提高发送到GPU命令的速度,但是有的时候合批会在其他地方增加不必要的消耗。如果合批操作导致了我们游戏的CPU性能收到限制,我们可能希望限制在我们游戏中的手动或者自动合批操作。
蒙皮网格
当我们使用一个叫做骨骼动画的技术使一个网格进行变形时会用到SkinnedMeshRenderers。它常用于角色动画上。与渲染相关的渲染蒙皮网格任务通常在主线程或者独立的辅助线程上执行,具体取决于你的游戏设置和目标硬件。
渲染蒙皮网格是一个耗时的操作。如果我们通过Profiler window看到我们游戏由于渲染蒙皮网格导致CPU性能受限,这里有几个方法可以尝试提高我们的游戏性能:
- 我们应该考虑是否需要为我们当前使用的每个对象都用到SkinnedMeshRenderer组件。有可能我们导入了一个模型挂载了SkinnedMeshRenderer组件但是我们实际上没有用到它的动画。例如,在这种情况中用MeshRenderer替换SkinnedMeshRenderer组件可以缓解性能压力。当导入一个模型到Unity中,如果我们没有在模型Import Settings中设置动画,这个模型将会用一个MeshRenderer替换掉SkinnedMeshRenderer。
- 如果我们的动画只是在某些段时间播放(例如,只有当启动的时候或者是只有在距离摄像机一定距离的时候),我们可以在动画不播放的时候把网格切换到一个少量细节的版本或者是用MeshRenderer组件来代替SkinnedMeshRenderer组件。SkinnedMeshRenderer组件有一个BakeMesh 函数,这个函数能够创建一个SkinnedMeshRenderer动画状态的mesh(译:相当于是SkinnedMeshRendere的某一个动作的一帧画面),这对不同的网格或者渲染器之间进行交换而不改变对象的可见状态非常有用(译:如果把Mesh看作一个图片,这里的交换相当于播放帧动画。帧动画不需要额外计算,只需要一张图片接着一张图片的播放。类似的这里是切换不同的Mesh看起来像是有动作一样,省去了蒙皮的计算消耗)。
- Unity手册的这个页面包含了对于使用了蒙皮网格(skinned meshs)的动画角色的优化建议,SkinnedMeshRenderer组件在Unity手册中的这页包含一些能够提高性能的调整。除了这些网页的介绍,值得我们记住的是蒙皮网格会增加每个顶点的消耗;因此在我们的模型中使用更少的顶点会降少大量的模型渲染的工作量。
- 在一些平台上,蒙皮可以由GPU上处理而不是CPU上处理。如果我们的GPU的显存够高还是值得我们去尝试下。我们能够在当前平台的Player Settings中的quality target设置GPU skinning选项是否开启。
主线程的操作与渲染无关
理解很多在主线程上的CPU的任务其实和渲染这块没有关系是很重要的。这意味如果我们是主线程导致CPU性能问题,我们可以通过减少与渲染不相关的任务所消耗的CPU时间来提高性能。
来看一个例子,在我们游戏的某一些地方主线程上处理非常耗时的渲染操作和非常耗时的用户脚本,导致了我们游戏CPU性能首先。如果我们已经在不损失画面效果的前提下尽可能的优化了渲染操作,那么我们可以减少我们自己脚本的CPU消耗来提高性能。
GPU性能受限
如果我们游戏是GPU性能受限,我们首先要做的是找出导致GPU性能瓶颈的原因。GPU性能通常是由于填充率的限制。尤其在移动设备上,但是内存带宽和定点处理也是值得关注的点。让我们来对这些问题每个都测下,同时研究下问题的原因,怎样去查证和修复这些问题。
填充率
填充率是指GPU每秒可以在屏幕上渲染的像素数量。如果我们游戏因为填充率收到性能的限制,这意味我们的游戏尝试每帧去画比GPU能够处理的量更多的像素。
如果我们游戏中由于填充率导致GPU性能问题,我们可以简单的测试出来:
- 分析我们游戏记录GPU时间
- 在Player Settings设置降低显示分辨率。
- 再次分析游戏,如果游戏性能提高了,这就很有可能是填充率的问题。
如果是填充率引起的我们的问题,这里有几个方法帮助我们解决这些问题。
- 片段着色器这部分代码的功能是告诉GPU怎样去绘出单个像素。每个最终会绘制的像素都会经过在GPU中执行的这部分代码的处理。所以如果这部分的代码效率低的话,那么绘制的像素一多很容易堆积成性能问题。复杂的片段着色器经常造成填充率问题。
- 如果我们的游戏使用内置着色器。我们应该使用最简单的尽可能的优化了的着色器来达到我们期望的效果。举个列子,Unity提供的针对移动设备的着色器是经过深度优化过的。我们应该尝试着使用它们看看这些着色器是否在没有损失我们游戏的表现效果的前提下提高了性能。这些着色器是针对移动设备上使用而设计的,但是适用于任何项目。如果使用在非移动设备上增加了性能并且表现的效果符合项目的要求自然是非常好的。
- 如果我们游戏中的对象使用了Unity的Standard Shader,要记住Unity是基于当前的材质的设置来编译这些shader的,只会编译当前使用的功能。这意味Unity编译时会移除例如地图细节这种功能,这样会大大的减少片段着色器的代码复杂度从而有利于提升性能。重申下,如果我们项目中出现了填充率问题,我们应该尝试这种设置看看我们游戏是否在没有损失效果的情况下提高了性能。
- 如果我们项目使用了定制的着色器,我们应该尽可能的优化它们。优化着色器是一个复杂的主题,但是Unity手册的这个页面和Unity手册的这个页面的着色器优化部分包含了优化我们着色器代码的有用的切入点。
- 过度绘制(Overdraw)是指当同一个像素被绘制多次。这发生在对象绘制在其他对象上层的时候,这很容易造成填充率的问题。为了理解过度绘制,我们必须知道Unity绘制场景中的对象的顺序。对象的着色器决定了绘制顺序,通常指明对象在哪个渲染队列中。Unity利用这个信息按照严格的顺序绘制对象,更详细的介绍在Unity手册的这页。此外,在绘制之前在不同渲染队列中的对象会进行排序。例如,集合队列的项Unity会按照从前往后的顺序排序使得最小化过度绘制的情况。但是透明队列的对象为了达到要求的效果按照从后往前排序。透明队列上的对象从后往前排序实际上把过度绘制的影响最大化了。过度绘制是一个比较复杂的主题,没有一个统一的方法能够解决所有的过度绘制的问题,关键是减少重叠的对象的数量使得Unity不需要自动对这些对象排序。最好的切入点是在Unity场景视图中测试这个问题;我们场景中的绘制模式允许我们查看过度绘制的情况,在这个模式下,确定可以减少过度绘制的地方。最普遍的导致过度绘制的是透明度材质,没有优化的粒子特效和重叠的UI元素,所以我们应该尝试优化或者减少它们。Unity学习网站的这篇文章主要介绍了Unity UI,但是也包含了非常好的过度绘制的指南。
- 使用图片后期效果(image effects)会极大的造成填充率问题,如果我们游戏使用了图片后期效果并且我们的游戏在填充率问题上挣扎着,我们可以尝试不同的设置或者使用更加优化的图片后期效果的版本(例如针对于Bloom的优化版Bloom(优化版))。如果我们的游戏在同一个相机中使用了超过一个图片后期效果,这会导致这个效果需要多个着色器的SetPass calls来渲染。这种情况,我们最好是把我们图片后期效果的着色器代码合并到一个pass中,例如在Unity的后期处理栈。如果我们已经已经优化了我们的图片后期效果但是任然有填充率的问题,我们可能需要考虑关闭图片后期效果,尤其是在低端机器。
内存带宽
内存宽带是指GPU可以读写专用的内存的速率。如果我们游戏内存宽带受限,通常表示我们使用了大多的纹理,GPU已经无法快速的处理了。
检查内存内存宽带是否有问题,我们可以这样做:
- 分析我们的游戏,记下GPU的时间。
- 在Quality Settings里面降低当前平台贴图质量(Texture Quality)和运行时画质
- 再次分析游戏和记录下GPU的时间。如果性能得到提高,这似乎内存带宽是有问题的。
如果我们游戏中存在内存带宽问题,我们需要减少我们游戏中贴图的内存使用量。重申一遍,减少贴图内存使用量对每个游戏的优化的最好的效果是不同的,但是这里有几个方法可以优化我们的贴图。
- 贴图压缩是个能够很好的减少贴图的运行内存和物理内存的大小的技术。如果内存带宽在我们游戏中是一个显著的问题,使用贴图压缩能够来减少内存中纹理的大小有助于提高性能。Unity中有很多不同的贴图研所格式和可用设置。每个贴图都有独立的设置。一般而言,无论在什么时候尽可能的使用压缩格式。然而,为找到适合我们游戏的最好的贴图设置的最佳方式是去尝试和试错那些设置。Unity手册的这页包含了关于不同格式和设置的非常有用的信息。
- Mipmaps是Unity能够在对象在处于远处时使用的低分辨率版本。如果我们场景中包含了远离摄像机的对象,我们可以使用mipmaps来缓解内存带宽问题。在场景视图中的Mipmaps绘制模式允许我们看我们场景中的那些对象使用mipmaps是有利的,Unity手册的这一页包含了为贴图是否设置mipmaps的更多信息。
顶点处理
顶点处理是指GPU一定会渲染的mesh中的每个顶点的工作。影响顶点处理的消耗有两个方面:最终会渲染的顶点数量,需要处理的每个顶点的数量。如果我们游戏是GPU性能受限同时我们确定了不是填充率和内存带宽受限导致的,那么久很有可能是顶点处理引起的问题。如果是这个原因导致的,尝试减少GPU必须进行的顶点处理的量可能会提高提高性能。
这里有几个方法我们可以考虑帮助我们在处理每个顶点时减少顶点数量或者顶点处理的数量。
- 首先,我们应该期望减少任何不必要的复杂的网格。如果我们使用的网格有一些在我们游戏中看不到的细节,或者是由于创建时的错误导致太多顶点,这些会浪费很多GPU处理时间。最简单的减少顶点处理的消耗的方法是在我们3D美术软件(译:如,maya)里使用最少量的顶点数据创建网格。
- 我们可以尝试一个叫做法线贴图(normal mapping)的技术,这是使用纹理创建更复杂的几何网格的错误看法的地方。尽管这个技术会有一些GPU开销,但是它在很多情况下会带来性能上的提升。Unity手册的这页有实用的法线贴图指南,可以使用法线贴图模拟我们网格中复杂的几何图形。
- 如果我们游戏中的mesh没有使用法线贴图,我们通常可以在网格的导入设置中关闭使用顶点切线。这回减少大量的发送到GPU的顶点数据。
- 细节层次(Level of detail),也叫做LOD,是一对网格远离相机时减少复杂度的优化技术。这会减少GPU渲染的顶点的数量,同时不影响游戏的显示效果。Unity手册关于LOD的页面包含了更多怎样设置我们游戏的LOD的信息。
- 顶点着色器是一块用来告诉GPU怎样去绘制每个顶点的着色器代码。如果我们游戏由于顶点处理受到性能限制,减少我们游戏顶点着色器的复杂度可能有帮助。
- 如果我们游戏使用的是Unity内置着色器,我们应该期望使用最简单的尽可能最优化的着色器来达到我们期望的显示效果。举个例子,Unity发布的移动设备的着色器是高度优化了的;我们应该尝试使用这些材质,看看是否能提高性能但不影响我们游戏的显示效果 。
- 如果我们项目使用了定制的着色器,我们应该尽可能的优化它们。优化着色器是一个复杂的主题,但是Unity手册的这个页面和Unity手册的这个页面的着色器优化部分包含了优化我们着色器代码的有用的切入点。
总结
我们学习了Unity渲染是怎样工作的,当渲染的时候会出现什么类型的问题,怎样提高我们游戏的性能。使用这部分知识并且使用性能分析工具,我们可以修复渲染相关的性能问题,构建我们的游戏使得游戏的渲染管线更显平滑和高率。