CPU bound:CPU性能边界,是指CPU计算时一直处于占用率很高的情况。
GPU bound:GPU性能边界,同样的是指GPU计算时一直处于占用率很高的情况。
简介
在这篇文章中,我们将学习Unity渲染一帧时在幕后会发生什么,渲染时会出现什么样的性能问题,以及如何修复与渲染相关的性能问题。
在阅读本文之前,有一点需要记住的是,即没有适合所有情况的方法可以提高渲染性能。渲染性能受到游戏中许多因素的影响,同时也高度依赖于游戏所运行的硬件和操作系统。最重要的是要记住,我们通过研究、实验和严格分析实验结果来解决性能问题。
这篇文章包含了关于渲染的最常见的性能问题的信息,以及如何修复这些问题的建议和进一步的延伸阅读链接。我们的游戏可能会有一个或多个问题这里并没有提到。然而,这篇文章仍然会帮助我们理解我们的问题,并给我们知识和词汇来有效地寻找解决方案。
渲染的简要介绍
在我们开始之前,让我们来看看Unity渲染一帧时会发生什么。理解事件流和正确的术语将帮助我们理解、研究和解决性能问题。
注:在本文中,我们将使用术语“object”来表示游戏中可能渲染的对象。任何带有Renderer组件的GameObject都会被称为object。
在最基本的层面上,渲染可以描述如下:
- 中央处理器,即CPU,计算出必须绘制什么以及如何绘制。
- CPU向图形处理单元(即GPU)发送指令。
- GPU根据CPU的指令绘制物体。
现在让我们仔细看看发生了什么。在本文后面的部分中,我们将更详细地介绍这些步骤,但是现在让我们先熟悉所使用的单词,并理解CPU和GPU在渲染过程中扮演的不同角色。
描述渲染常用的短语是渲染管线,这是一个需要牢记的过程;高效的渲染就是保持数据高效地流动。
对于渲染的每一帧,CPU都做了以下工作:
- CPU检查场景中的每个对象,已确定是否应该渲染它。对象只有在满足某些条件时才被渲染;例如,它的包围盒的某些部分必须位于摄像机的视锥体内。将不渲染的对象称为剔除对象。有关视锥体和视锥体剔除的更多信息,请参见此页。
- CPU收集关于将要渲染的每个对象的信息,并将这些数据排序为称为draw call的命令。一个draw call包含关于单个网格的数据以及如何渲染该网格;例如,应该使用哪些纹理。在某些情况下,共享设置的对象可以组合到同一个draw call中。将不同object的数据组合到同一个draw call中称为批处理。
- CPU为每个draw call创建一个称为批处理的数据包。批处理有时可能包含draw call调用之外的数据,但是这些情况不太可能导致常见的性能问题,因此我们在本文中不考虑这些情况。
对于每个包含一个draw call的批处理,CPU现在必须执行以下操作:
- CPU可以向GPU发送一个命令来更改一些变量,这些变量统称为渲染状态。这个命令称为SetPass call。SetPass call告诉GPU使用哪些设置渲染下一个网格。只有当渲染的下一个网格需要从前一个网格更改渲染状态时,才会发送SetPass call。
- CPU将draw call发送到GPU。draw call指示GPU使用最近SetPass调用中定义的设置来渲染指定的网格。
- 在某些情况下,批处理可能需要不只一个pass。pass是着色器代码的一部分,新的pass需要对渲染状态进行更改。对于批处理中的每一个pass,CPU必须发送一个新的SetPass call,然后必须再次发送draw call。
同时,GPU还做了以下工作:
- GPU按照从CPU发送的顺序处理任务。
- 如果当前任务是SetPass call,GPU将更新渲染状态。
- 如果当前任务是一个draw call,GPU渲染网格。这是分阶段进行的,由着色器代码的不同部分定义。这部分渲染是很复杂的,我们不会详细讨论它,但是理解一段叫做顶点着色器(vertex shader)的代码是很有帮助的。顶点着色器告诉GPU如何处理网格的顶点,然后一段称为片段着色器(fragment shader)告诉GPU如何画出每个像素。
- 这个过程重复进行,直到所有从CPU发送的任务都被GPU处理。
现在我们已经了解了 Unity渲染一帧时所发生的事情,让我们考虑一下渲染时可能出现的的问题。
渲染问题的类型
关于渲染最重要的一点是:为了渲染一帧,CPU和GPU都必须完成所有的任务。如果这些任务中的任何一个花费太长时间来完成,它将导致帧的渲染延迟。
渲染的问题有两个基本原因。第一类问题是由低效的管线引起的。当渲染管线中的一个或多个步骤花费太长时间无法完成时,就会出现低效率的管线,从而中断数据的顺畅流动。管线内的低效被称为瓶颈(bottlenecks)。第二种类型的问题是由于试图通过向管线推送太多的数据而导致的。即使是最有效的管线也有一个限制,即在一帧中可以处理多少数据。
当我们的游戏是因为CPU花费太多的时间去执行任务而导致需要花费很长的时间渲染一帧时,我们的游戏就是所谓的CPU边界(CPU bound)。当我们的游戏因为GPU执行它的渲染任务而花费太多的时间渲染一帧时,我们的游戏就是所谓的GPU边界(GPU bound)。
理解渲染问题
在进行任何修改之前,使用Profiler分析工具来了解性能问题的原因是非常重要的。不同的问题需要不同的解决方案。同样重要的是,我们要衡量我们所作的每一个修改的效果;解决性能问题是一种平衡的行为,改进性能的一个方面可能会对另一个方面产生负面影响。
我们将使用两个工具来帮助我们理解和修复渲染问题:Profiler和Frame Debugger。这两个工具都内置在Unity中。
Profiler Window
Profiler窗口允许我们查看关于游戏执行情况的实时数据。我们可以使用Profiler查看关于游戏的许多方面的数据,包括内存使用、渲染管线和用户脚本的性能。
如果你还不熟悉使用Profiler,那么Unity 手册是一个很好的介绍, 这里一篇文章将详细介绍如何使用它。
Frame Debugger
帧调试器允许我们一步步地查看帧是如何渲染的。使用帧调试器,我们可以看到详细的信息,例如每次draw call中绘制的内容、每次draw call的着色器属性以及发送到GPU的事件顺序。这些信息帮助我们理解游戏是如何渲染的,以及我们可以在哪里提高性能。
如果你还不熟悉帧调试器的使用,Unity手册中 关于帧调试器的部分是一个非常有用的指南。
发现造成性能问题的原因
在我们尝试提高游戏的渲染性能之前,我们必须确保游戏是由于渲染问题而运行缓慢的。如果问题的真正原因是过于复杂的用户脚本,那么尝试优化渲染问题是没有意义的。如果你不确定你的性能问题是否与渲染有关,请查看这一篇 教程。
一旦我们确定了我们的问题与渲染有关,我们还必须了解我们的游戏是CPU bound,还是GPU bound。这些不同的问题需要不同的答案,所以在尝试解决问题之前,我们必须了解问题的原因,这一点非常重要。如果你还不确定你的游戏是CPU bound还是GPU bound,你应该查看 这一个教程。
如果我们确定我们的问题与渲染有关,并且我们知道我们的游戏是CPU bound还是GPU bound,我们就可以继续下面的内容了。
如果我们的游戏是受到了CPU的限制
一般来说,CPU渲染一帧所必须完成的工作分为三类:
- 决定绘制什么
- 为GPU准备命令
- 向GPU发送命令
这几个类包含许多单独的任务,这些任务可以跨多个线程执行。线程允许独立的任务同时发生;当一个线程执行一个任务时,另一个线程可以执行一个完全独立的任务。这意味着工作可以做得更快。当渲染任务被分割到不同的线程时,这称为多线程渲染。
在Unity的渲染过程中有三种类型的线程:主线程,渲染线程和工作线程。主线程是我们游戏的大部分CPU任务发生的地方,包含一些渲染任务。渲染线程是一个专门的线程,它向GPU发送命令。每个工作线程执行单个任务,例如剔除或者网格蒙皮。哪个线程执行哪些任务取决于游戏的设置和游戏运行的硬件。例如,目标硬件的CPU内核越多,能分配的工作线程就越多。因此,在目标硬件上描述我们的游戏是非常重要的;我们的游戏在不同设备上的表现可能会非常不同。
因为多线程渲染是复杂的,并且依赖于硬件,所以在我们尝试提高性能之前,我们必须了解是哪些任务导致我们的游戏受到了CPU的限制。如果我们的游戏运行缓慢是因为在一个线程上的剔除操作花费太多的时间,那么它将无法帮助我们减少在另一个线程上向GPU发送命令所花费的时间。
注意:并非所有平台都支持多线程渲染;在撰写本文时,WebGL不支持这个特性。在不支持多线程渲染的平台上,所有CPU任务都在同一个线程上执行。如果我们的CPU限制出现在这样的平台上,那么优化任何CPU的工作都将提高CPU性能。如果这是我们游戏的情况,我们应该阅读一下所有部分并考虑哪些优化可能最适合我们的游戏。
图形工作
在Player Settings中的 Graphics jobs 选项决定了Unity是否使用工作线程来执行渲染任务,而这些任务在其他情况下是在主线程上完成的。在有此功能的平台上,它可以提供相当大的性能提升。如果我们希望使用这个特性,我们应该在启用和不启用Graphics jobs的情况下对游戏进行概要分析,并观察它对性能的影响。
找出哪些任务导致了性能问题
我们可以通过使用Profiler来确定哪些任务导致我们的游戏受到CPU的限制。本教程展示了如何确定问题所在。
现在我们已经了解了哪些任务导致我们的游戏受到CPU的限制,让我们来看看一些常见的问题及其解决方案。
向GPU发送命令
向GPU发送命令所花费的时间是游戏受到CPU限制的最常见原因。这个任务是在大多数平台上的渲染线程上执行的,尽管在某些平台上(例如paystation 4)这可能是由工作线程执行的。
向GPU发送命令时发生的代价最大的操作是SetPass call 。如果我们的游戏由于向GPU发送命令而受到CPU限制,那么减少SetPass call的数量可能是提高性能的最佳方法。
我们可以在Unity的Profiler Window中的Rendering profiler区域看到发送了多少SetPass call和批处理(batches )。在性能受到影响之前可以发送的SetPass call的数量在很大程度上取决于目标硬件;在性能受到影响之前,高端PC可以比移动设备发送更多的SetPass calls。
SetPass calls的数量及其与批处理数量的关系取决于几个因素,我们将在本文后面更详细地讨论这些主题。然而,通常情况是:
- 在大多数情况下,减少批处理和/或让更多的对象共用相同的渲染状态设置将减少SetPass call的数量。
- 在大多数情况下,减少SetPass call的数量将提高CPU性能。
如果减少批处理的数量并不能减少SetPass call的数量,那么它本身仍然可以提高性能。这是因为CPU处理单个批处理比处理多个批处理更有效率,即使它们包含相同数量的网格数据。
总的来说,有三种方法可以减少批处理和SetPass call的数量。我们将更深入地研究这些问题:
- 减少要渲染对象的数量可能会同时减少批处理和SetPass call。
- 减少每个对象必须渲染的次数通常会减少SetPass call的数量。
- 将必须渲染的更少批处理对象的数据组合起来将减少批处理的数量。
不同的技术将适用于不同的游戏,所以我们应该考虑多有这些选项,决定哪些可以适合应用于我们的游戏和试验中。
减少渲染对象的数量
减少必须渲染的对象数量是减少批处理和SetPass call数量的最简单方法。我们可以使用几种技术来减少渲染对象的数量。
- 简单地减少场景中可见对象的数量是一个有效的解决方案。例如,如果我们在人群中渲染大量不同的角色,我们可以简单地在场景中减少这些角色。如果场景看起来仍然很好,性能得到了改善,那么这可能是比更复杂的技术更快的解决方案。
- 我们可以使用相机的远裁剪面属性来减少相机的绘制距离。此属性是摄像机不再渲染对象的距离,如果我们想要掩盖遥远物体不再可见的事实,我们可以尝试用雾来掩盖遥远物体的缺乏。
- 对于基于距离的更细粒度的隐藏对象的方法,我们可以使用相机的Layer Cull Distance属性为位于不同层的对象提供自定义的裁剪距离。如果我们有很多小的前景装饰细节,这种方法是有用的;我们可以在比大型地形特征更短的距离内隐藏这些细节。
- 我们可以使用一种称为遮挡剔除(occlusion culling)的技术来禁用被其他对象遮挡的对象的渲染。例如,如果我们的场景中有一个大型建筑,我们可以使用遮挡剔除来禁用其背后的对象的渲染。Unity的遮挡剔除并不适用于所有场景,这可能会导致额外的CPU开销,设置起来也可能很复杂,但它可以大大提高某些场景的性能。这有一篇关于在Unity中采用遮挡剔除的最佳实践。除了使用Unity的遮挡剔除,我们还可以通过手动去使玩家看不到的对象变得无效来实现我们自己的遮挡剔除。例如,如果我们的场景包含用于过场动画的对象,但是在之前或之后不可见,我们应该停用它们。使用我们自己的游戏知识总是比要求Unity动态解决问题更有效。
减少每个对象必须渲染的次数
实时光照,阴影和反射为游戏添加了大量的真实性,但可能会非常耗费性能。使用这些特性会导致对象被多次渲染,这会极大地影响性能。
这些特性的确切影响取决于我们为游戏选择的渲染路径。渲染路径是在绘制场景时执行计算顺序的术语,在不同的渲染路径之间的主要区别在于它们如何处理实时光照,阴影和反射。一般来说,如果我们的游戏运行在高端硬件上,并且使用了大量的实时光照、阴影和反射,那么延迟渲染(Deferred Rendering)可能是一个更好的选择。如果我们的游戏运行在低端硬件上并且不使用这些特性的话,前向渲染(Forward Rendering)可能会更适合。然而,这是一个非常复杂的问题,如果我们希望利用实时光照、阴影和高光,最好研究一下这方面的主题和做不同的试验。Unity手册的 这个页面提供了更多关于Unity中可用的不同渲染路径的信息。这篇 教程包含了关于Unity中有关灯光为主题的有用信息。
无论渲染路径如何选择,实时灯光、阴影和反射的使用都会影响我们的游戏性能,因此了解如何优化它们时非常重要的。
- Unity中的动态照明是一个非常复杂的主题,深入讨论它超出了本文的范围,但是这个教程是对这个主题的一个很好的介绍,Unity手册的这一页详细介绍了常见的照明优化。
- 动态照明是昂贵的,当我们的场景包含不移动的对象时,例如风景,我们可以使用一种称为烘焙的技术来预先计算场景的光照,这样就不需要运行时计算光照计算了。这一篇教程介绍了这种技术,Unity手册中也有对烘焙光照这部分主题的详细介绍。
- 如果我们希望在游戏中使用实时阴影,这可能是我们需要提高性能的方面。Unity手册的这一页是一个很好的参考,可以调整阴影属性的质量设置,以及这些设置如何影响外观和性能。例如,我们可以使用阴影距离属性确保只有附近的对象投射阴影。
- 反射探针 可以创建真实的反射,但是在批处理方面花费的成本非常高。在考虑性能的情况下,最好将反射的使用控制在最低限度,并在使用反射的情况下尽可能地优化反射。Unity手册的这一页提供了一些有关优化反射探针的指导。
将对象合并成更少的批处理
当满足某些条件时,批处理可以包含多个对象的数据。要符合批处理条件,对象必须满足:
- 共享相同的材质实例
- 有相同的材质设置(例如,纹理,着色器和着色器参数)
批处理符合条件的对象可以提高性能,尽管与所有优化技术一样,我们必须仔细分析以确保批处理的成本不超过性能收益。
对于符合批处理条件的对象,有一些不同的技术:
- 静态批处理是这样一种技术,它允许Unity批处理附近不移动的符合条件的对象。可以从静态批处理中获益的一个很好的例子是一堆类似的对象,例如巨石。Unity手册的这一页包含了在我们的游戏中设置静态的说明。静态批处理会导致更高的内存使用,所以在分析我们的游戏时,我们应该牢记这个成本。
- 动态批处理是另一种允许Unity批处理合适对象的技术,不管对象是否移动。对于使用这种技术批处理的对象有一些限制。这些限制和说明一起列在Unity手册这一页上。动态批处理对CPU使用有影响,这可能导致它在CPU时间上的开销大于节省的时间。在试验这种技术时,我们应该牢记这一成本,并谨慎使用它。
- 批处理Unity的UI元素有点复杂,因为它可能会受到UI布局的影响。这个来自2015年Unite Bangkok的视频很好地概述了这个主题,还有这一个教程提供了关于如何确保UI批处理按照我们的意愿工作的深入信息。
- GPU instancing 是一种允许对大量相同对象进行高效批处理的技术。它的使用是有限的,并不是所有的硬件都支持它,但是如果我们的游戏在屏幕上同时有许多相同的对象,我们可能会从这项技术中收益。Unity的这一页包含了对Unity3d中GPU实例化的介绍,详细介绍了如何使用它,哪些平台支持它,以及在什么情况下它可能会对我们的游戏有帮助。
- 纹理图集(Texturing atlasing)是一种将多个纹理合并为一个较大纹理的技术。它通常用于2D游戏和UI系统,但也可以用于3D游戏。如果我们在为我们的游戏创造美术资源时使用这种技术,我们就可以确保共享纹理的对象可以进行批处理。Unity有一个内置的纹理图集工具,叫做Sprite Packer,用于2D游戏。
- 可以在Unity编辑器中或者在运行时通过代码手动组合共享相同材质和纹理的网格。当以这种方式组合网格时,我们必须意识到阴影、光照和剔除仍然会在每个对象上运行。这意味着,合并网格带来的性能提升可能会被抵消,因为不这样做,这些对象就无法被渲染。如果我们希望研究这种方法,我们应该去查看Mesh.CombineMeshes这个函数。Unity的Standard Assets资源包里面的CombineChildren就是应用了这项技术的一个例子。
- 我们在脚本中访问Renderer.material时必须非常小心。这会复制一个材质并且返回新副本的引用。这样做将打破批处理,如果渲染器不再具有对同一材质实例的引用。如果我们希望在脚本中访问批处理对象的材质,我们应该使用Renderer.sharedMaterial。
剔除、分类和批处理
对将要绘制的对象进行剔除、收集批处理数据和生成GPU命令都可能导致CPU限制(CPU bound)。这些任务可以在主线程上执行,也可以在单独的工作线程上执行,这取决于游戏的设置和目标硬件。
- 剔除(culling)本身不太可能是非常昂贵的,但是减少不必要的剔除可能有助于性能。对于所有活动的场景对象,甚至那些没有被渲染的图层上的对象每个对象都有一个相机开销。为了减少这种情况,我们应该禁用摄像机,并禁用当前未使用的渲染器。
- 批处理可以大大提高向GPU发送命令的速度,但有时会在其他地方增加不必要的开销。如果批处理操作导致我们的游戏受到CPU限制,我们可能希望限制游戏中手动或者自动批处理操作的数量。
Skinned meshes
SkinnedMeshRenderers 是用来当我们的网格变形动画而使用的技术,称为骨骼动画。它最常用于动画角色,根据我们游戏的设置和目标硬件,渲染蒙皮网格的相关任务通常会在主线程或单个工作线程上执行。
渲染蒙皮网格可能是一个昂贵的操作。如果我们能在Profiler中看到渲染蒙皮网格会导致我们的游戏受到CPU的限制,那么我们可以尝试做一些事情来提高性能:
- 我们应该考虑是否需要为当前正在使用的对象添加SkinnedMeshRenderer组件。例如,我们可能已经导入了一个使用SkinnedMeshRenderer组件的模型,但实际上我们并没有对它采用动画。在这种情况下,用MeshRenderer组件替换SkinnedMeshRenderer组件将有助于提高性能。在将模型导入Unity时,如果我们选择不在model Import Settings 中导入动画那么模型将会带有一个MeshRenderer而不是SkinnedMeshRenderer组件。
- 如果我们只是在某些时候在模型上使用动画(例如,只有在启动或只有当它是在相机的一定距离内),我们可以切换它的网格为一个低细节版本或把MeshRender组件替换SkinnedMeshRenderer。SkinnedMeshRenderer组件有一个BakeMesh函数,可以以匹配的姿态创建一个网格,这对于在不同的网格或渲染器之间进行交换非常有用,而不会对对象产生任何可见的改变。
- Unity手册中的这一页面包含了关于优化使用蒙皮网格的动画角色的建议,而在Unity手册在SkinnedMeshRenderer组件的页面包含了可以提高性能的调整。除了这些页面的建议外,值得注意的是,网格蒙皮的成本增加是基于每个顶点的;因此,在我们的模型中使用更少的顶点可以减少必须完成的计算量。
- 在某些平台上,蒙皮可以由GPU而不是CPU来处理。如果我们在GPU上有很大的容量,这个选项可能值得一试。我们可以在Player Settings中为current platform和quality target启用GPU蒙皮。
与渲染无关的主线程操作
理解许多与渲染无关的CPU任务发生在主线程是很重要的。这意味着,如果我们在主线程上遇到了CPU限制,那么我们可以通过减少在与渲染无关的任务上花费的CPU时间来提高性能。
例如,我们的游戏可能在主线程上执行昂贵的渲染操作和昂贵的用户脚本操作,使我们的CPU达到性能边界。如果我们在不丢失视觉效果的情况下尽可能地优化渲染操作,我们就有可能降低自己脚本的CPU成本,从而提高性能。
如果我们的游戏达到了GPU性能边界
如果我们的游戏达到了GPU性能边界,首先要做的就是找出是什么导致GPU瓶颈。GPU的性能通常是受到填充率的限制,尤其是在移动设备上,但是内存带宽和顶点处理也会受到影响。让我们检查一下这些问题,并了解导致这些问题的原因,并学会诊断以及修复这些问题。
填充率 Fill rate
填充率是指GPU每秒可以渲染给屏幕的像素数。如果我们的游戏受到填充率的限制,这意味着我们的游戏试图在每一帧中绘制比GPU所能处理的更多的像素。
检查填充率是否导致我们的游戏被GPU限制是很简单的:
- 部署运行游戏并记录GPU时间。
- 降低Player Settings中的显示分辨率。
- 再次部署运行游戏。如果性能得到改善,那么填充率可能就是问题所在。
如果填充率是问题的原因,有一些方法可以帮助我们解决这个问题:
片段着色器是着色器代码中告诉GPU如何绘制单个像素的部分。这段代码由GPU逐像素执行的,因此如果代码效率低下,那么性能问题就很容易堆积起来。复杂的片段着色器导致填充率问题的常见原因;
如果我们的游戏使用内置的着色器,我们应该尽可能使用最简单和最优化的着色器来获得我们想要的视觉效果。在分组为mobile的着色器是高度优化的;我们应该尝试使用它们,看看这是否能够在不影响游戏外观的情况下提高性能。这些着色器是为移动平台设计的,但是它们适用于任何项目。在非移动平台上使用“mobile”着色器来提高性能是完全可以的。如果它们提供了项目所需的视觉效果的话。
如果我们游戏中的对象使用Unity的标准着色器,理解Unity基于当前材质设置编译这个着色器是很重要的。只编译当前使用的特性。这意味着删除detail maps之类的特性可以得到更简单的片段着色器代码,从而大大提高性能。同样,如果我们的游戏是这样的情况的,我们应该尝试这么做,看看我们是否能够在不影响视觉质量的情况下提高性能。
如果我们的项目使用定制的着色器,我们应该尽可能优化它们。优化着色器是一个复杂的主题,但是Unity手册的这一页和这一页包含了优化着色器代码的参考。
过度绘制(Overdraw)是指同一个像素被多次绘制的情况。当对象被绘制在其他对象之上时就会发生这种情况,这对填充率问题有很大的影响,为了理解过度绘制,我们必须理解Unity在场景中绘制对象的顺序。一个对象的着色器决定它的绘制顺序,通常是通过指定对象所在的渲染队列。Unity使用这些信息以严格的顺序绘制对象,详见Unity手册的这一页 。此外,在绘制不同渲染队列中的对象之前,对它们进行不同的排序。例如,Unity在Geometry队列中对其中对象进行**从前到后排序,以最小化过度绘制,但在Transparent队列中对对象进行从后到前**进行排序,以达到所需的视觉效果。这种从后到前的排序实际上可以最大化透明队列中对象的过度绘制。过度绘制是一个复杂的课题,没有一个大小合适所有的方法来解决过度绘制问题,但是减少Unity不能自动排序的重叠对象的数量是关键。最好从Unity的场景试图开始调查这个问题;有一个绘制模式,允许我们看到我们的场景过度绘制,并在那里,确定我们可以从这个地方入手来减少过度绘制。过度绘制最常见的罪魁祸首是透明材质、未优化的粒子和重叠的UI元素,所以我们应该尝试优化或减少这些。Unity的学习网站上这篇主要有关Unity UI优化的文章,但也包含了关于过度绘制的一般性指导。
使用image effects会大大增加填充率的问题,特别是如果我们使用多个image effect。如果我们的游戏使用了Image effect,并且在填充率问题上遇到了困难,我们可能希望尝试使用不同的设置或image effect的更优化版本(如Bloom(Optimized)代替Boom)。如果我们的游戏在同一个相机上使用多个image effect,这将导致多个着色器Pass。在这种情况下,把我们的着色器代码和image effect合并到一个Pass中,比如在Unity's PostProcessing Stack 。如果我们优化了image effect,但仍然存在填充率问题,我们可能需要考虑禁用image effect,特别是在低端设备上。
存储带宽
内存带宽指的是GPU从其专用内存读取和写入数据的速率。如果我们的游戏受到内存带宽的限制,这通常意味着我们使用的纹理太大,GPU无法快速处理。
要检查内存带宽是否有问题,我们可以执行以下操作:
- 配置运行游戏并记录GPU时间。
- 在Quality Settings中降低纹理质量。
- 再次配置运行游戏并记录GPU时间。如果性能得到了改善,那么内存带宽可能就是问题所在了。
如果内存带宽是我们的问题,我们需要在游戏中减少纹理内存的使用。同样,对于每一款游戏最有效的技术也会有所不同,但是我们可以通过一些方式来优化我们的纹理。
- 纹理压缩是一种可以大大减少磁盘和内存中纹理大小的技术。如果我们的游戏中内存带宽是一个问题,使用纹理压缩来减少内存中纹理的大小可以帮助提高性能。Unity中有很多不同的纹理压缩格式和设置,每个纹理都可以有单独的设置。一般来说,只要有可能,就应该使用某种形式的纹理压缩;然而,尝试为不同的纹理进行不同的设置以达到最好的效果。Unity手册中关于不同压缩格式和设置的文章。
- Mipmaps是Unity中可以在远距离物体上使用的纹理的低分辨率版本。如果我们的场景包含远离摄像机的对象,我们可以使用mipmaps来缓解内存带宽的问题。场景视图中的mipmaps绘制模式允许我们查看场景中的哪些对象可以从mipmaps中受益,Unity手册中的这一页包含了更多关于为纹理启用Mipmaps的信息。
顶点处理
顶点处理是指GPU在网格中渲染每个顶点时所必须做的工作。顶点处理的成本受到两个因素影响:必须渲染的顶点数量和必须在每个顶点上执行的操作数量。
如果我们的游戏达到了GPU性能边界,并且我们已经确定它不受填充率或内存带宽的限制,那么顶点处理很可能是问题的原因。如果是这样,尝试减少GPU必须处进行的顶点处理的数量可能会带来性能的提高。
我们可以考虑几种方法来帮助我们减少顶点的数量或我们在每个顶点上执行的操作的数量:
首先,我们应该致力于减少不必要的网格复杂性。如果我们使用的网格具有在游戏中无法看到的细节,或者由于创建错误而具有太多顶点的低效率网格,这对GPU来说是浪费工作。减少顶点处理成本的最简单的方法是在我们的3D建模工具中创建顶点数较低的网格。
我们可以尝试一种叫做法线贴图的技术,在这种技术中纹理被用来在网格上创建更复杂的几何效果。尽管这种技术有一些GPU开销,但在许多情况下会带来性能的提高。Unity手册的这一页介绍了使用法线贴图来模拟复杂的几何网格。
如果我们的游戏中的一个网格没有使用法线贴图,我们通常可以在网格导入设置中禁用该网格的顶点切线。这减少了为每个顶点发送到GPU的数据量。
细节层次(LOD),是一种优化技术,其中原理摄像机的网格减少复杂性。这在不影响游戏视觉质量的情况下减少了GPU渲染顶点的数量。Unity手册中LOD页面包含了更多关于如何在游戏中设置LOD的信息。
顶点着色器是告诉GPU如何绘制每个顶点的着色器代码块。如果我们的游戏受到顶点处理的限制,那么减少顶着色器的复杂性可能会有所帮助。
如果我们的游戏使用内置的着色器,我们应该尽可能使用最简单和最优化的着色器来获得我们想要的视觉效果。例如,带有(mobile)的着色器是高度优化了的;我们应该尝试使用它们,看看这是否能够在不影响游戏外观的情况下提高性能。
如果我们的项目使用定制的着色器,我们应该尽可能优化它们。优化着色器是一个复杂的主题,但是Unity手册的这一页和这一页的着色器优化部分包含了优化着色器代码的有用信息。
总结
我们已经学习了Unity中渲染是如何工作的,在渲染时会出现什么问题,以及如何在游戏中提高渲染性能。利用这些知识和我们的分析工具,我们可以修复与渲染相关的性能问题,并构建游戏架构,使它们拥有一个流畅高效的渲染管线。
下面的链接提供了关于本文主题的进一步详细信息:
Unity Learn: A guide to optimizing Unity UI
Unity Knowledge Base: Why is my static batching breaking or otherwise not working as expected?
Fabian Giesen: A trip through the graphics pipeline
Gamasutra: How to choose between Forward or Deferred rendering paths in Unity
Gamasutra: Batching independently moving GameObjects into a single mesh to reduce draw calls
FlameBait Games: Optimizing SkinnedMeshRenderers for Unity 5
Pencil Square Games: Reducing draw calls (also named SetPass calls) in Unity 5