【Unity】优化图形渲染

2018-04-14  本文已影响430人  hellokazhang

原文地址:
https://unity3d.com/cn/learn/tutorials/temas/performance-optimization/optimizing-graphics-rendering-unity-games?playlist=44069

英语不佳,尝试翻译如下:

介绍

在这篇文章,我们将会学到当Unity渲染一帧图像的时候屏幕后面会发生什么和渲染的时候会遇到什么性能问题以及如何解决渲染相关的性能问题。

阅读文章之前,首先要明白改善渲染性能问题没有一种适合所有情况的方法。我们游戏的渲染性能问题受很多方面的影响,并且高度依赖运行游戏的硬件和操作系统。要记住最有用的一点是,我们通过调查、实验和严格的分析来解决性能问题。

这篇文章包含最常见的渲染性能问题,并提供了如何修复它们的建议及进一步阅读的链接。也存在这种情况,我们的游戏有一个或多个问题这篇文章是没有提及的。这篇文章仍能帮助我们了解我们的问题,并给我们知识和词汇,以便可以有效的寻找解决方案。

渲染简介

在此之前,让我们简单快速的看一下Unity渲染一帧会发生什么事。理解接下来的事件和正确的术语将对我们理解、研究和努力修复性能问题有帮助。

在这篇文章,我们使用“对象”来代表我们游戏中可能被渲染的对象。任何游戏物品挂载了Renderer组件的,将会被当做这样一个对象。

在最基本层面上,渲染可以被描述成这样:
CPU负责什么是必须绘制的及它必须怎样绘制
CPU发送指令给GPU
GPU根据CPU的指令进行绘制

现在让我们仔细看看到底发生了什么。我们会在文章的后面提及更详细的步骤,但现在我们只理解这些用语,并且明白CPUGPU在渲染中起什么作用。

经常使用渲染管线来描述渲染,这是一个非常有用的概念。高效的渲染就是保持信息流畅。

对于渲染的每一帧,CPU执行了以下的任务:

  1. CPU检测场景中的每个物体,决定他们是否要被渲染。一个物体只有满足一定条件才会被渲染。比如,包围盒的一部分必须在摄像机的视锥之内。被剔除的物体不会被渲染。关于更多的视锥和剔除相关的信息,请点击这里
  2. CPU收集每个即将被渲染的物体信息并把数据按命令分类,这就叫做draw calls。一个draw call包含关于单个mesh与该mesh如何被渲染的数据。比如,应该使用哪张图片。在某些情况下,物体共享设置可能会被合并成同一个draw call。合并两个不同的物体为同一个draw call被称为批处理。
  3. CPU为每一个draw call创建一个数据包被称为批处理。批处理有时会包含draw calls外的数据,但是这种情况不太可能造成性能问题,因此我们在文章中不会考虑这种情况。

对于每个包含draw call的批处理来说,CPU必须执行以下操作:

  1. CPU可以向GPU发送一条命令来改变许多被统称为渲染状态的变量。这条命令被称为SetPass callSetPass call告诉GPU下一个要渲染的网格使用哪个设置。只有下一个渲染的网格需要改变前一个网格渲染状态的时候才会发送SetPass call
  2. CPU发送draw call到GPU。Draw call通知GPU使用最近使用的SetPass call定义的设置去渲染指定的网格。
  3. 在某种情况下,批处理可能有多个pass。pass是shader代码的一部分,一个新的pass需要渲染状态的改变。对于批处理中的每个pass,CPU必须先发一个新的SetPass call然后再发draw call

同时,GPU做以下的工作:

  1. GPUCPU发过来的顺序处理任务
  2. 如果当前任务是draw call,GPU就渲染网格。这个任务是分阶段的,由shader代码分开定义。这部分渲染比较复杂,我们不会详细介绍。但是对于理解一部分叫做顶点着色器的代码告诉GPU如何去处理网格的顶点,一部分叫做片段着色器的代码告诉GPU如何去绘制单个的像素很有帮助。
  3. 这个过程会重复直到GPU处理完从CPU发过来的所有任务。

现在我们明白了当Unity渲染一帧的时发生了什么,让我们考虑一下渲染时可能会出现的问题。

渲染问题的类型

理解渲染问题最重要是:渲染一帧,CPUGPU必须完成它们所有的任务。如果它们当中的任何一个任务完成时间太长,将会造成渲染延迟。

渲染问题有两个基本的原因:第一种是:低效的管道。当渲染管道中当一个或多个任务完成时间耗时过长会造成管道效率低,会中断数据的流程度。管道内的低效率,也被称为瓶颈。第二种是向管道推送了太多的数据。即使是最高效的管道,单帧处理的数据也会有一个上限。

当我们的游戏花费太长时间渲染一帧是因为CPU花费太长时间执行渲染任务,那我们的游戏是CPU密集。当我们的游戏花费太长时间渲染一帧是因为GPU花费太长时间执行渲染任务,那我们的游戏是GPU密集。

了解渲染问题

在我们做出调整之前,使用profiling工具去了解造成性能问题的原因是很重要的。不同的问题需要不同的解决方案。测量我们每次做出的修改的效果也是同样重要的。修复性能问题是一种平衡的行为,修复一方面的问题可能引起另一些问题。

我们将会使用两种工具帮助我们理解和修复渲染性能问题:Profiler工具和Fame Debugger。这两个工具都是Unity自带的。

Profiler工具

Profiler允许我们看到游戏实时执行的数据。我们可以使用Profiler看到我们游戏的许多方面的数据,包括内存使用、渲染管道和脚本的性能。

如果你对Profiler还不熟悉,请看这个介绍这个教程

帧调试器

帧调试器允许我们看到一帧是如何一步步渲染的。使用帧调试器,我们可以看到每个draw call绘制了什么,每个draw call的shader属性和发送给GPU的事件顺序等详细信息。这些信息帮助我们理解我们的游戏是如何渲染的以及我们可以改善那些性能问题。

如果你对使用Frame Debugger还不熟悉,请看这个介绍这个教程

查找造成性能问题的原因

在我们尝试改善我们游戏的渲染性能问题之前,我们必须先确定我们游戏运行缓慢是渲染问题造成的。如果造成我们问题的原因是过度复杂的脚本,尝试优化我们的渲染性能是没有意义的。如果你还不确定你的性能问题是否与渲染相关,请看这篇教程

一旦我们确定我们的问题与渲染相关,我们必须了解我们的游戏是CPU密集还是GPU密集。不同的问题需要不同的解决方案,在尝试修复问题之前,了解造成问题的原因是非常重要的。如果你还不确定你的游戏是CPU密集还是GPU密集,请查看这篇教程

如果我们确定了我们的问题与渲染相关,并且我们知道我们的游戏是CPU密集或GPU密集,我们接着往下读。

如果游戏是CPU密集

从广义上讲,CPU渲染一帧必须要完成的工作分成三类:

  1. 决定什么是必须绘制的
  2. GPU准备命令
  3. 发送命令给GPU

这些宽泛的类别包含许多单个任务,这些任务可以跨多个线程执行。线程允许多个独立的任务同时执行。当一个线程执行一个任务,另一个线程可以执行一个完全独立的任务。这就意味着工作可以更快的完成。当渲染任务被拆分跨多个独立的线程时,这就是多线程渲染。

在Unity渲染进程中会调用三种类型的线程:main thread,render thread和worker threads。主线程执行我们游戏的主要任务,包括一些渲染任务。渲染线程是一个特殊的线程,负责发送命令给GPU。每个worker线程执行一个单独的任务,比如裁剪或网格蒙皮。哪一个任务被哪一个线程执行依赖于我们的游戏设置和游戏运行的硬件。比如,目标设备的CPU的内核越多,产生的 worker线程越多。因此,我们的游戏在目标设备上分析是非常重要的。我们的游戏在不同的设备执行结果会截然不同。

因为多线程选渲染是复杂且依赖硬件的,在我们尝试改善性能问题之前,我们必须明白哪个任务是造成CPU密集的原因。如果我们游戏运行缓慢是因为裁剪操作在一个线程耗时太长了,通过减少另一个发送命令给GPU的线程的耗时是没有什么帮助的。

不是所有的平台都支持多线程渲染。在编写这篇文章的时候,WebGL还不支持这个特性。在一个不支持多线程渲染的平台,所有的CPU任务都在同一个线程执行。如果CPU密集的运行在这样的平台,CPU上任何优化都会改善CPU的性能。这种情况下,我们应该阅读下面的章节并考虑哪种优化最适合我们的游戏。

图形处理

Graphics jobs选项在Player Settings。它决定Unity是否使用worker线程执行在main线程上或在某些情况下渲染现线程上的任务。在支持该性能的平台上可以提供相当大的性能提升。如果我们希望使用这个特性,我们应该观察使用了Graphics jobs和没使用Graphics jobs的性能效果。

找到是哪个任务引起的问题

我们可以使用Profiler工具确定是哪个任务造成CPU密集。本教程展示了如何确定问题所在。

现在我们明白了哪个任务是造成CPU密集的原因,让我们看下几种常见的问题和解决方案。

发送命令到GPU

发送命令到GPU的耗时是游戏CPU密集最常见的原因。这个任务在大多数平台上是在render线程执行,但是在某些平台上(比如PlayStation 4)是在worker线程执行的。

发送命令到GPU的时候,最消耗的操作是SetPass call。如果我们游戏的CPU密集是由发送命令到GPU造成,减少SetPass calls的数量可能是最好的改善性能的方法。

我们在Unity的渲染分析器中看到当前有多少SetPass callsbatches正在发送。在性能不下降的前提下,SetPass calls数量高度依赖目标硬件。在不降低性能的情况下,一个高端的PC机比一个手机能发送更多的SetPass calls

SetPass calls的数量以及它与batches数量的关系,取决于几个因素,我们稍后在文章进行详细的介绍。通常会有以下几种情况:

  1. 减少batches的数量或使更多的物体共用相同的渲染状态,在大多数情况下会减少SetPass calls的数量
  2. 减少SetPass calls的数量,在大多数情况下会改善CPU的性能

如果减少了batches的数量,没有减少SetPass calls的数量,这仍然导致自身性能的改善。这是因为CPU处理单个batch比处理几个batches要高效,即使他们的网格数据同样多。

大体上有三种减少batchesSetPass calls数量的方法。我们将会更深入的研究每个方法:

  1. 减少被渲染的物体数量,可能会减少batchesSetPass calls的数量
  2. 减少每个必须被渲染的物体的渲染次数,通常会减少SetPass calls的数量
  3. 把必须要渲染的物体合并成更少的batches将会减少batches的数量

不同的技巧适应不同的游戏,所以我们考虑这里所有的选项,决定哪些在我们的游戏和实验中能够发挥作用。

减少被渲染物体的数量

减少被渲染物体的数量是减少batchesSetPass calls数量的最简单的方法。这里有几种可以减少渲染物体的方法:

  1. 简单地减少我们场景中课件的物体的数量是一个有效的解决方案。比如说,我们在人群中渲染大量的不同的角色,我们可以在场景中简单的放几个角色。如果这个场景看起来还不错并且性能也提高了,这比那些复杂技巧来说是一个更快的解决方案。
  2. 我们可以使用摄像机的Far Clip Plane属性去减少摄像机的绘制距离。超过这个属性的距离的物体不再被摄像机渲染。如果我们希望掩盖远处不再渲染的物体这个事实,我们可以尝试使用雾去隐藏远处缺失的物体
  3. 基于距离的更细粒度的隐藏物体的方法,我们可以使用摄像机的Layer Cull Distances属性为每个单独的层提供自定义的裁剪距离。如果我们游戏有很多的装饰小物件,我们可以使用比大地图更短的裁剪距离去隐藏这些小物件,这是非常有用的。
  4. 我们可以使用一个叫occlusion culling(遮挡剔除)的方法去隐藏那些被遮挡的物体。比如,我们的场景中有一个大的建筑物,使用occlusion culling可以关掉被挡住的物体的渲染。Unity的occlusion culling并不适用于所有尝尽,可能会导致CPU消耗过大,并且设置可能会复杂,但是在一些场景上可以极大的改善性能问题。这篇文章针对这一主题进行描述。除了使用Unity的occlusion culling之外,我们也可以使用自己的方式去手动隐藏我们知道不会展示给玩家的物体。比如,我们场景中包含一些切场景前或切场景后不可见的物体,我们应该隐藏它们。使用我们游戏的知识总是比要求Unity动态解决问题更有效。

减少每个物体被渲染的次数

实时灯光、阴影和反射为游戏增加了很多真实性,但是这非常耗性能。使用这些特性可以导致物体被渲染多次,这会大大的影响性能。

这些特性的确切的影响取决于我们为游戏选择的渲染路径(rendering path)。渲染路径是绘制场景时计算的顺序的术语,渲染路径的主要差别在于它们如何处理实时灯光、阴影和反射。一般来说,如果我们的游戏运行在高端硬件上,并且使用了一些实时灯光、阴影、反射,那么延迟渲染(Deferred Rendering)可能是一个更好的选择。如果我们的游戏运行在低端设备上,并且没有使用这些特性,那么正向渲染(Forward Rendering)可能更合适。这是一个非常复杂的问题,如果我们希望使用好实时灯光、阴影、反射,最好是研究这个主题并实验。这个Unity手册介绍了不同的渲染路径的信息。这个教程包含了一些有用的Unity灯光的主题信息。

不管选择什么渲染路径,使用实时灯光、阴影和反射会影响我们游戏的性能,明白如何优化它们是很重要的。

  1. Unity的动态灯光(Dynamic lighting)是一个非常复杂的主题,深入的讨论这个主题超出了本文的范围,这篇教程是对这一个主题有极好的介绍。这篇手册包含了常见的灯管优化细节。
  2. 动态灯光非常昂贵。当我们场景包含不移动的物体时,我们可以使用一个叫做烘焙(baking)的技术,预先计算场景的灯光,这样运行的时候就不需要计算灯光了。这篇教程介绍了这个技术,这篇章节提及烘焙灯光的细节。
  3. 如果我们希望在游戏中使用实时阴影,这可能是我们可以改善性能的地方。这篇文章展示了如何调整阴影属性(Quality Settings)和它如何影响性能。比如,我们可以使用阴影距离(Shadow Distance)属性确保只有靠近的物体才会产生阴影。
  4. 反射探头(Reflection probes)创建了真实的反射,但是批量使用的话,消耗会比较大。最好少使用反射,尽可能优化使用的地方,性能不容忽视。这篇文章展示如何优化反射探头。

合并物体到更少的批处理

当条件满足的情况下,一个批处理包含的数据可以为多个物体使用。批处理要满足以下条件:

  1. 共享相同材质的相同实例
  2. 拥有唯一的材质设置(如图片,shader,shader参数等)
    批处理可以改善性能,尽管和其他所有优化技巧一样,我们必须细心的分析确保批处理的消耗不会超过性能的获取。
    以下有几个不同的技巧:

裁切,排序和批处理

裁切,收集将要绘制的物体的数据,将这些数据排序成batches并生成GPU命令,这些操作都可能会导致CPU密集。
这些任务会在main thread线程执行或单个wroker线程执行,这取决于我们的游戏设置和目标设备。

  1. 裁切本身不太消耗性能,但减少不必要的裁切可能会改善性能。场景中的物体都有每物体每相机(per-object-per-camera)的开销,即使在那些不显示的层(layers)。为了减少这些消耗,我们应该禁用当前不使用的摄像机或隐藏当前不渲染的物体。
  2. 批处理可以很大的提高发送命令给GPU的速度,但在有时候在某些地方可能会增加不必要的性能开销。如果批处理操作是造成我们游戏CPU密集的原因,我们可以在游戏中限制手动或自动批处理的操作。

网格蒙皮

当我们使用一个叫网格动画的技术来变形网格的时候,SkinnedMeshRenderers就会被用到。通常使用在角色动作上。渲染蒙皮网格的相关任务一般在main thread或单独个worker threads上执行,这取决于我们游戏的设置和目标设备。

渲染蒙皮网格可能是一个很消耗性能的操作。如果我们在Profiler窗口发现渲染蒙皮网格是造成CPU密集的原因,那我们可以通过一下几个方法来改善性能:

  1. 我们应该考虑当前使用的物体,是否每一个都要使用SkinnedMeshRenderer件。有可能我们导入的模型实际不会播放动画却使用了SkinnedMeshRenderer组件。像这种情况,使用MeshRenderer组件代替SkinnedMeshRenderer组件有助于提升性能。当导入模型的时候,我们在模型导入设置中the model's Import Setting)选择不导入动画,那么这个模型会有MeshRenderer组件而不是SkinnedMeshRenderer组件。
  2. 如果我们只是在某些时候(比如在开始的时候或在摄像机一定距离内)播放动画,我们可以切换网格到精简版(a less detailed version)或把SkinnedMeshRenderer组件替换成MeshRenderer组件。SKinnedMeshRenderer组件有一个BakeMesh函数,它可以创建一个姿势匹配的网格,这在不同的meshes或renderers切换是非常有用的,并且这种切换对其物体没有任何可见的变化。
  3. 这篇文章包含了优化蒙皮网格动画的建议,这个教程包含了SkinnedMeshRenderer组件对改善性能的调整。除了建议之外,牢记顶点数越多,网格蒙皮消耗越大。因此使用更少顶点的模型以减少必须完成的工作量。
  4. 在某些平台上,蒙皮可能是在GPU处理的而不是在CPU。如果我们的GPU有很多内存的话,这个选项是值得尝试的。我们可以在Player Settings为当前平台启用GPU skinning和质量指标。

与渲染无关的主线程操作

很多跟渲染无关的CPU任务会发生在主线程,明白这一点很重要。这意味着,如果我们主线程是CPU密集,我们减少CPU在非渲染任务上的耗时可能可以改善性能。

比如,我们游戏主线程在某些时候执行耗时的渲染操作和运行耗时的用户脚本造成了CPU密集。如果我们已经在尽可能不失真的情况下优化渲染操作,我们可能要减少用户脚本的消耗以改善性能。

如果我们的游戏是GPU密集

如果我们的游戏是GPU密集,首先要找出造成GPU瓶颈的原因。GPU性能问题通常由填充率(fill rate) 受限制引起的,特别是手机设备上,但内存带宽(memory bandwidth)和顶点处理也同样需要关注。让我们检查一些这些问题并了解是什么原因造成的、分析并修复这些问题。

填充率

填充率指是GPU每秒在屏幕上渲染的像素数量。如果我们游戏是填充率受限制,这意味着我们游戏尝试绘制的像素超过GPU每帧能处理的像素。
很容易检测我们游戏GPU密集是否是由填充率造成的:

  1. 使用Profiler工具,并注意查看GPU时间
  2. Player Settings降低分辨率
  3. 再次使用Profiler工具,如果性能有所改善,那填充率很可能是造成问题的原因

如果填充率是造成问题的原因,下面有几个方面可能能帮助我们修复问题。

  1. 片段着色器是shader代码的一部分,它告诉GPU如何绘制单个像素。GPU为每个必须绘制的像素执行这段代码,如果代码效率不高,性能问题很容易累积起来。复杂的片段着色器很经常造成填充率问题。
  1. 过度绘制(Overdraw)是同一像素绘制多次的术语。当物体被绘制到其他物体顶部的时候会发生Overdraw,并且可能引起填充率问题。为了弄明白Overdraw,我们必须了解Unity在场景绘制物体的顺序。一个物体的shader通常使用render queue决定了绘制顺序(draw order)。Unity使用这些信息来严格的绘制物体,这篇文章有更详细的细节。此外,不同的渲染队列在绘制之前进行了不同的排序。比如,Unity在Geometry队列中使用front-to-back的排序方式来减少overdraw,但在Transparent队列则使用back-to-front的排序方式来达到所需要的视觉效果。实际上在Transparent队列back-to-front的这种排序对overdraw有最大的影响。Overdraw是一个复杂的主题,找不到一种可以解决所有overdraw问题的方法。但是减少Unity不能自动排序的重叠物体是很关键的。开始调查这个问题的最好地方实在Unity的场景视图。那里有个绘制模式(Draw Model),从那里我们可以看到场景overdraw,我们便知道从哪里入手减少overdraw。最常见的过度overdraw罪魁祸首是透明材质、没有优化过的粒子特效、覆盖的UI元素,所以我们应该尝试优化或减少它们。这篇文章主要关注Unity UI,也包含对overdraw的一些引导。
  2. 图片特效的使用对填充率问题有一定的影响,特别是当我们使用不止一个图片特效。如果我么游戏使用了图片特效,并且被填充率问题困扰的时候,我们尝试使用不同的设置或更加优化的图片特效(比如Bloom(optimized)代替Bloom)。如果我们游戏在同一个摄像机使用了超过一个图片特效,会造成多个shader pass。这种情况下,将我们的图片特效shader代码合并成一个pass可能会有益处,这里有个教程。如果我们优化过图片特性后仍然有填充率问题,我们可能需要考虑禁用图片特效,特别是在低端设备上。

内存带宽(Memory bandwidth)

内存带宽是指GPU在专用存储器的读写速率。如果我们游戏被内存带宽限制,通常意味着我们使用的纹理太大以至于GPU无法快速处理。

检测游戏是否存在带宽问题,我们需要以下几个步骤:
1.使用Profiler工具,并注意查看GPU时间
2.在Quality Settings降低纹理的品质(Texture Quality)
3.再次使用Profiler工具查看GPU时间,如果性能有所改善,那内存带宽很可能是造成问题的原因

如果我们的游戏存在内存带宽问题,我们需要减少游戏中图片(Texture)内存的使用。我们可以通过下面几个方法优化我们的纹理:
1.图片压缩(Texture compression)可以极大的减少纹理在硬盘和内存的大小。如果内存带宽是我们游戏中问题,使用图片压缩可以减少图片内存大小来提升性能。Unity有很多不同的图片压缩格式和设置,每张图片都可以单独设置。一般来说,应该尽可能的使用某种形式的图片压缩。反复实验为每张图片找到最好的压缩格式。这篇文章包含了不同的压缩格式与设置的有用信息。
2.Mipmaps是图片的低分辨率版本,可以用在远处的物体上。如果场景中有距离摄像机很远的物体,可以使用mipmaps来缓解内存带宽问题。场景视图中的The Mipmaps Draw Mode可以看到那些地方受益于mipmaps,这篇文章包含了关于图片开启mipmaps的信息。

顶点处理

顶点处理是指GPU必须在网格中渲染每个顶点的工作。顶点处理的消耗受两个地方的影响:必须渲染的顶点数量和每个顶点必须执行的操作数量

如果我们的游戏确定是GPU密集,并且不是填充率造成的,也不是内存带宽造成的,那么可能是顶点处理造成的。如果是这种情况,尝试减少GPU必须处理的顶点处理数量,可能会获得性能的提升。

我们可以考虑几种可以帮助我们减少顶点数量或减少我们执行每个顶点的操作数量。

  1. 首先,我们应该致力于减少任何不必要的复杂网格。如果我们使用了在游戏中看不到细节的网格或由于创建错误导致顶点过多的低效网格,这是很浪费GPU资源的。减少顶点处理消耗的最简单方法是在3D美术工程中创建更少顶点数的网格。
  2. 我们可以尝试使用一种叫做normal mapping(法线贴图)的方法,在创建更大的几何复杂度网格时候可以使用到。尽管有些GPU开销比较大,但在多数情况下会带来性能的提升。这篇文章是在网格中使用法线贴图模拟复杂几何的有用指南。
  3. 如果游戏中的网格没有使用法线贴图,我们通常可以在import settingsvertex tangents(顶点切线)禁用。这会减少为每个顶点发送给GPU的数据数量。
    4.Level of detail(细节层级)也被称为LOD,是一个优化技术,远离摄像机的网格会降低复杂度。这减少了GPU必须渲染的顶点数量,而不影响游戏的视觉质量。这篇文章包含了如何设置LOD的更多信息。
  4. Vertex shaders(顶点着色器)是shader代码的一段,告诉GPU如何渲染每个顶点。如果我们的游戏受限于顶点处理,减少我们的顶点着色器的复杂度可能会有帮助。

总结

我们已经学习了Unity的渲染工作,渲染时会出现什么问题以及如何改善我们游戏的渲染性能。使用这些知识和分析关系,我们可以修复渲染相关的性能问题来使我们的游戏有一个更加平滑高效的渲染管线。

上一篇下一篇

猜你喜欢

热点阅读