unity图形渲染优化指导
Posted Unity3d游戏开发笔记
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了unity图形渲染优化指导相关的知识,希望对你有一定的参考价值。
该译文旨在帮助客户端程序和美术了解unity的基本渲染流程以及一些优化思想,可以避免将来绕弯路。
译文会跳过不必要的内容,力求精简。
碍于水平问题,不足或错误之处请指出。
优化图形渲染
关于渲染的一二事
在开始之前,我们有必要知道unity在渲染一帧时做了哪些工作。明白背后的机制有助于我们定位和解决问题。
*注意,全文所提及的“对象”(object)是指在游戏种被渲染出来的物体,并不简单等同于“游戏对象”(GameObject),但是挂有Renderer组件的“游戏对象”可以成为一个“对象”。
一般来说,渲染可以被描述成三步:
CPU负责处理哪些物体需要被绘制以及如何被绘制。
CPU发送指令给GPU。
GPU根据CPU指令进行绘制。
接下来会对每一步做深入探讨,但首先要明白CPU和GPU在渲染中扮演的角色。
我们经常用渲染管道(rendering pipeline)来描述渲染(rendering),而且有一点必须铭记在心:高效的渲染就是保证信息快速流通。(efficient rendering is all about keeping information flowing)
对于每一帧,CPU做了以下工作:
CPU检测场景里每一个“对象”并决定该对象是否会被渲染。一个“对象”只有达到某些条件才会被渲染出来,例如一部分包围盒(bounding box)出现在摄像机的视锥体(view frustrum)。没有被渲染的“对象”我们称之为被剔除(to be culled)。
CPU搜集所有可见“对象”的信息并将数据整理进指令的过程被称之为绘制调用(draw calls)。一个绘制调用包含的数据可以是单个网格以及该网格如何渲染,例如网格会使用哪张贴图。在特定的情况,多个共享设置的“对象”可以被组合进同一个绘制调用,即批处理(batching)。
对于每一个绘制调用(draw call),CPU都会创建一个相应的数据包(a batch)。数据包(batchs)包含的数据量有时会多于绘制调用(draw calls),但这不会导致性能问题,故在此不多做讨论。
对于每一个数据包所包含的绘制调用(draw call),CPU现在必须执行下述工作:
CPU会向GPU发送一条指令(command)用于修改多个参数,即渲染状态(render state)。该指令被称之为通道设置调用(SetPass call)。一个通道设置调用(SetPass call)告诉GPU该用哪种配置去渲染下一个网格。只有当下一个网格的渲染状态(render state)不同于上一个网格时,CPU才会发送通道设置调用(SetPass call)。
CPU将绘制调用(draw call)发送至GPU。绘制调用指示GPU使用最近的通道设置调用(SetPass call)来渲染指定网格。
在某些情况,数据包(batch)会需要多个通道(pass)。一个通道(pass)是一段着色代码,而新的通道(pass)调用需要修改渲染状态(render state)。对于数据包(batch)里的每一个通道(pass),CPU必须发送一个新的通道设置调用(SetPass call),然后再发送一次绘制调用(draw call)。
此时GPU执行以下工作:
GPU依次处理CPU派发过来的任务。
如果当前任务是通道设置调用(SetPass call),GPU刷新渲染状态(render state)。
如果当前任务是绘制调用(draw call),GPU渲染网格。渲染发生在不同着色代码片段,由于该部分过于复杂在此不做详述,只需要知道一段称之为顶点着色器(vertex shader)的代码告诉GPU如何处理网格的顶点信息,而另一段称之为像素着色器(pixel shader)的代码告诉GPU如何绘制单个像素。
这个过程反复执行直到GPU处理完所有任务。
我们已经对unity的渲染有所了解,接下来将探讨渲染过程中可能会遇到的问题。
分门别类(Types of renderingproblems)
渲染最核心的点在于: CPU和GPU必须完成各自所有的任务,才能完整渲染一帧。只要任意一个任务处理花费时间过长,渲染将被延缓。
大多渲染问题可被划分成两种基本情况。第一种是由低效的管道(inefficient pipeline)引起的。当渲染管道中一个或多个步骤花费时间过长,便会阻断数据流导致低效的管道(inefficient pipeline)。低效的管道又被称之为瓶颈(bottlenecks)。第二种情况是推入大量的数据(data)通过管道(pipeline)。即便是最高效的管道(pipeline)处理数据的能力也是有上限的。
当游戏因为CPU执行某些任务花费时间过长而导致帧数降低,我们称之为CPU卡顿(CPU bound);相应地对于GPU,我们称之为GPU卡顿(GPU bound)。
一探究竟(Understandingrendering problems)
在采取措施前,我们必须使用分析工具查明问题,切忌生搬硬套。不同同问题需要不同解决方案。对于每一点修改我们都要测试其效果;修复性能问题是一门平衡之道,对某方面的优化可能会对其他方面产生负面影响。
我们将会使用两个工具帮助我们分析渲染问题:Profiler window 和 Frame Debugger。这里稍微介绍下Frame Debugger。
Frame Debugger允许我们逐步查看每一帧是如何渲染的。在Frame Debugger里,我们能看到更详细绘制调用(draw call)信息,比如绘制了什么,对应的着色器属性以及被发送至GPU的事件顺序。这些信息有助于我们找出需要优化的地方。
查明病因(Finding the causeof performance problems)
在优化渲染性能之前,我们必须确认游戏卡顿是由渲染问题导致的,而不是过度复杂的脚本使用(overly compex user scripts)。
一旦知道问题是由渲染引起的,我们必须明确到底是CPU卡顿(CPU bound)还是GPU卡顿(GPU bound),然后才能对症下药。
CPU卡顿(CPU bound)
通常来说,在渲染里CPU执行的工作大致分成三类:
决定哪些需要绘制
为GPU准备指令
发送指令给GPU
这些大类包含多个单独任务,而这些任务会被多个线程(threads)交叉执行。线程(Threads)允许不同的任务同时执行,这意味着任务能被更快完成。当渲染任务被分散至多个线程(threads)时,我们称之为多线程渲染(multithreadedrendering)。
Unity的渲染流程里有三种线程:主线程(the main thread)、渲染线程(render thread)和工人线程(worker threads)。主线程(the main thread)是游戏核心CPU任务执行的地方,包括一些渲染任务;渲染线程(render thread)是专门用来发送指令(command)给GPU的;工人线程(workerthreads)执行单个任务,比如剔除或网格蒙皮。哪些线程执行哪些任务取决于我们的游戏设置和硬件环境。例如多核心CPU意味着更多的工人线程(worker threads)。
由于多线程渲染(multithreaded rendering)非常复杂而且跟硬件相关,在优化前,我们必须知道是哪个任务导致CPU卡顿(CPU bound)。如果游戏卡顿是因为剔除操作过于耗时,那么降低发送指令(command)给GPU的时间将会毫无帮助。
*注意,并非所有软硬件平台都支持多线程渲(multithreadedrendering)
在Unity的Player Settings里有个Graphics jobs选项用于决定是否可以使用工人线程(worker threads)执行原本需要在主线程(main thread)或者渲染线程(render thread)完成的任务。如果某些软硬件平台该特性可用,那将会带来可观的性能提升。如果我们希望使用该特性,可以开启/关闭graphics jobs来监控对性能的影响。
找出害群之马(Finding outwhich tasks are contributing to problems)
通过Profiler Window我们可以找出哪些任务导致CPU卡顿(CPU bound)。下面将探讨问题所在。我们会介绍一些常见的问题以及相应解决方案。
向GPU发送指令(Sending commands to the GPU)
向GPU发送指令所花费的时间是最常见导致CPU卡顿(CPU bound)的原因。在多数平台上,该任务在渲染线程执行(renderthread),在某些特定平台可以被工人线程(worker threads)完成,比如PlayStation 4。
这其中最费时的操作莫过于通道设置调用(SetPass call)。如果因此而导致CPU卡顿(CPU bound),降低通道设置调用(SetPass call)次数显然有助于提升性能。
通过Profiler window里的Rendering Profiler,我们可以清楚看到通道设置调用(SetPass call)次数和批处理(batches)数量。通道设置调用(SetPass call)数量和批处理(batches)之间的关系取决于多个因素,后面会谈及。然而通常的情况是:
降低批处理(batches)次数或者让“对象”(objects)共享同样的渲染状态(render state),在大多数情况下可以减少通道设置调用(SetPass call)次数。
通常来说,减少通道设置调用(SetPass call)次数可以提升CPU性能。
如果减少批处理(batches)数量并没有降低通道设置调用(SetPasscall)次数,但这依然对优化性能产生作用。因为比起多个批处理(batches),CPU处理起单个来效率更高。
一般来说,依然有3种方式降低批处理(batches)数量和通道设置调用(SetPass call)次数:
减少渲染“对象“(objects)可能同时降低批处理(batches)数量和通道设置调用(SetPass call)次数。
减少每个“对象“(object)的渲染次数通常可以降低通道设置调用(SetPass call)次数。
合并网格数据至更少的批处理(batches)有助于减少批处理(batches)数量。
不同的技术适应不同类型的游戏,我们需要全面考虑这些方案并决定在游戏中采用哪种技术。
减少渲染“对象“数量(Reducing thenumber of objects being rendered)
减少渲染“对象“(objects)数量是减少批处理(batches)数量和通道设置调用(SetPass call)次数最简单的方法。有好几种技术可以实现该方法。
简单地减少场景里可见地物体。如果减少可见物体数量同时并不会影响体验,那么该方案远比复杂的技术快速有效。
我们可以通过摄像机的远裁剪面(Far Clip Plane)属性来降低摄像机的绘制距离。超过该距离的物体将不被渲染。
对于一个基于详细距离来隐藏物体的方法,我们可以使用摄像机的层级剔除距离(LayerCull Distances)属性来定制不同层级(layers)物体的剔除距离。比如一个拥有众多装饰细节的地表,当我们远离地表时,可以适当隐藏装饰细节。
还可以使用一种称之为遮罩剔除(Occlusion Culling)技术,即关闭(disable)被遮挡的物体,但会增加CPU的负荷。
减少“对象“的渲染次数(Reducingthe number of times each object must be rendered)
实时光照、阴影和反射带来更真实的效果同时也加重资源开销。使用这些特性会导致“对象“(objects)被渲染多次,影响性能。
具体的影响取决于我们所设置的渲染路径(rendering path)。渲染路径(rendering path)是指在渲染场景时计算(calculations)执行的顺序,而不同的渲染路径(rendering paths)之间最大的区别在于如何处理光照、阴影和反射。作为通用准则,延迟渲染(Deferred Rendering)适合于高端硬件,并且对实时光照、阴影和反射有更高需求。向前着色(Forward Rendering)则用于低端硬件,同时对上述特性没有需求。
不管我们采用哪个渲染路径(Rendering Path),实时光照、阴影和反射都会对性能产生影响,而如何优化这些特性就显得格外重要。
在Unity里,动态光照(Dynamiclighting)是一个非常复杂的话题而且深入讨论则超出本文范围,有兴趣可以去unity官网或网上查阅资料。
动态光照(Dynamic lighting)是昂贵的。当场景里有许多静态物体时,可以采用烘焙(Baking)技术提前计算光照信息。
如果希望使用实时阴影,那么在Quality Settings里调整阴影参数有助于优化性能。
反射探针(Reflection probes)可以创建真实的反射效果,但会大幅增加批处理(batches)。尽量减少对反射探针(Reflection probes)使用,以确保对性能的影响。
合并“对象“至更少的批处理中(Combiningobjects into fewer batches)
在某种情况下,一个批处理可以包含多个对象(objects)所需的数据。
了执行批处理,对象(objects)必须:
共享同一个材质(material)实例
拥有完全相同的材质设置(例如,纹理、着色器和着色器参数)
虽然批处理“对象“可以提升性能,但前提是批处理(batching)本身所带来的消耗没有超过性能的提升,否则得不偿失。
目前有多种批处理(batching)技术:
静态批处理(Static batching)允许Unity批处理(batching)静止”对象”。
动态批处理(Dynamic batching)允许Unity批处理(batching)运动的对象。动态批处理(Dynamic batching)有许多限制条件,点此跳转查阅。而且该技术对CPU开销有负面影响,可能导致CPU花费更多时间进行批处理(batching),在使用时需多加注意。
批处理(Batching)UI元素是一个复杂的话题,详细参考官方另外一篇文章。
GPU实例化(GPU Instancing)可以用来快速绘制相同的物体。但并不是所有软硬件平台都支持该功能。
纹理图册(Texture atlasing)用于将多张纹理合并成一张更大的纹理。主要用于2D游戏和UI系统。
在Unity中,我们可以将共享同一个材质和纹理的网格进行手动合并,不管时编辑模式还是运行时候。在合并网格时,我们必须意识到阴影、光照和剔除依然会作用于每一个“对象“,这就意味着合并网格带来的性能提升会被本应被隐藏的对象却因为合并而显示带来的消耗所抵消。
我们必须非常谨慎的访问Renderer.material。这会拷贝材质(material)并返回拷贝的引用,导致批处理(batching)失效。
剔除、梳理和批处理(Culling,sorting and batching)
剔除(Culling),搜集将被绘制“对象“(objects)的数据,梳理数据至批处理(batches)中和生成GPU指令都会导致CPU卡顿(CPU bound)。这些任务既可以在主线程(main thread)或工人线程(worker thread)上执行,具体取决于软硬件平台。
剔除(Culling)本身不太可能消耗性能,但减少不必要的剔除(Culling)操作依然可以提升性能。对于所有活跃的场景“对象“(objects),依然存在每对象-每摄像机的间接开销,即便因为层级(layers)不同而没被渲染出来。为了减少这些不必要的开销,我们需要关闭不再使用的摄像机和”对象“(objects)。
批处理(batching)可以大幅提升向GPU发送指令的速度,但有时也能增加意想不到的间接开销。如果批处理操作(batchingoperations)导致CPU卡顿(CPU bound),我们则要慎重使用该技术。
蒙皮网格(Skinned meshes)
当我们使用骨骼动画对动画网格进行变形时,使用的正是SkinnedMeshRenderer组件。该组件通常用于角色动画。渲染蒙皮网格的相关任务一般在主线程(main thread)或独立的工人线程(worker threads)上执行,具体取决于软硬件平台。
渲染蒙皮网格是项开销较高的操作。如果在Profiler window发现渲染蒙皮网格导致CPU卡顿(CPU bound),下面几件事可以帮助我们提升性能:
对于每一个“对象”(object),仔细考虑我们是否真的需要SkinnedMeshenderer组件。否则可以用MeshRenderer组件取代,降低开销。
如果“对象”(objects)只在某些时刻执行动作,可以采用细节更少的网格或者使用MeshRenderer替代。SkinnedMeshRenderer组件包含一个BakeMesh函数用来拷贝相匹配动作的网格,保持”对象”(object)不变的情况下,在不同的网格或者渲染器间切换。
此外,还有一点需铭记在心:网格蒙皮的开销是随着顶点的增加而增加的,保持较少的顶点,可以减少渲染工作量。
对于某些平台,蒙皮可以在GPU上执行,而不是CPU。当GPU负荷较低时,尝试使用GPU蒙皮是个不错的选择。在Player Settings里勾选GPU Skinning即可。
主线程上无关渲染的操作(Main threadoperations unrelated to rendering)
除了渲染任务,还有很多其他操作在主线程(main thread)上执行。这就意味我们可以优化其他操作来降低CPU卡顿(CPU bound)。
例如,在某个时间CPU上同时执行昂贵的渲染和脚本任务,因而引起CPU卡顿(CPU bound)。在优化渲染操作后,减少脚本的开销可以有助于性能提升。
GPU卡顿(If our game is GPU bound)
如果游戏处于GPU卡顿(GPU bound),首要任务是找出引发GPU瓶颈(bottleneck)的原因。GPU性能通常受限于填充率(fill rate),而内存带宽(memory bandwith)和顶点处理(vertex processing)也应当引起注意。下面将会剖析和解决上述问题。
填充率(Fill rate)
填充率(fill rate)表示GPU每秒能在屏幕上渲染像素的数量。如果游戏受限于填充率(fill rate),就意味着我们尝试绘制的像素数量已经超出GPU处理能力。
检测是否由填充率(fill rate)引起GPU卡顿(GPUbound)是很容易的:
在Profile里查看GPU时间
在Player Settings里降低显示分辨率(display resolution)
再次查看Profile,如果性能提升,填充率(fill rate)很可能是问题所在。
确定填充率(fill rate)是问题元凶后,可以借助几个方法解决该问题。
片段着色器(fragment shader,即像素着色器)是一段用于告诉GPU如何绘制单个像素的着色器代码。对于每一个像素GPU都会执行片段着色器,如果该段代码效率低下便会导致性能问题。复杂的片段着色器是最常见导致填充率(fill rate)问题的原因。
1. 如果使用Unity内置着色器,采用最简单和高度优化的着色器来实现我们想要的特效,例如“Mobile”着色器。
2. 如果使用Unity Standard Shader,需要知道Unity是基于当前材质(material)设置来编译着色器。即使用的特性越少,编译出来的着色器会越简单。
3. 如果使用自定义着色器,尽可能优化所写代码。
透支(Overdraw)表示同一个像素被绘制多次。常发生在当“对象”(objects)绘制在其他“对象”(objects)上面,然后引发填充率(fill rate)问题。要明白透支,我们必须了解Unity在场景(scene)里绘制”对象”(objects)的顺序。“对象”(objects)的着色器通过渲染队列变量(render queue)决定其绘制顺序(draw order),Unity严格按照该顺序绘制“对象”(objects)。此外,在绘制之前,不同渲染队列(render queue)“对象”(objects)的排序方式也不尽相同。比如,为了最小化透支,Unity采用从前到后的方式对几何(Geometry)队列的“对象”(objects)进行排序。而透明(Transparent)队列刚好相反,导致透支最大化(UI层就是处于透明队列)。透支(overdraw)是一个复杂的话题并且没有万能的解决方案,但减少堆叠(overlap)的“对象”(objects)是关键之一。通过Draw Mode我们可以在Unity场景(scene)里查看透支的情况,标记出哪里可以进行优化,减少透支。透支的元凶大多是透明材质(transparent material)、未优化的粒子(unoptimizedparticles)和堆叠的UI元素(overlappingUI elements),所以在处理这些对象是,我们需要额外小心。
使用影像效果(image effects,类似PS图片处理)同样会导致填充率(fillrate)问题,尤其使用多个的时候。当游戏确实需要影像效果(image effects)时,我们应该采用优化过的版本(Bloom(Optimized)替代Bloom)。如果一个摄像机上挂有多个影像效果(image effects),这会导致多个着色器通道(shader passes)。在这种情况下,合并多个着色器代码至单个通道(pass)中会带来不少益处。在优化影像效果(image effects)后,依然存在填充率(fill rate)问题,我们就应该考虑关闭这些效果。
内存带宽(Memory bandwidth)
内存带宽(Memory bandwidth)表示GPU读取或写入特定内存块的速率。如果游戏受限于内存带宽(memorybandwidth),这往往意味着我们使用的纹理尺寸过大。
检测是否由内存带宽(Memory bandwidth)引起问题的方法如下:
在Profiler里查看GPU时间
在Quality Settings降低Texture Quality
返回Profiler,如果GPU时间减少,内存带宽(memory bandwidth)即是问题所在。
确定内存带宽(memory bandwidth)后,我们需要减少纹理的内存占用。下面两种技术可以帮助我们。
纹理压缩(Texture Compression)可以大幅减少纹理在存储和内存上的空间占用。目前Unity有多种压缩方式,根据具体情况和目标平台采用不同的压缩格式。
多纹理映射(MipMaps)是Unity在远距离“对象”(objects)上使用更低分辨率的纹理。当”对象”(objects)远离摄像机时,多纹理映射(MipMaps)有助于减缓内存带宽(memory bandwidth)问题。
顶点处理(Vertex processing)
顶点处理(vertex processing)表示GPU渲染时网格上每一个顶点所需执行的操作。顶点处理(vertex processing)的开销主要体现在两方面:顶点数量和每个顶点需要执行的操作。
如果GPU卡顿(GPU bound)不是由填充率(fill rate)和内存带宽(memory bandwidth),那顶点处理(vertex processing)很可能就是问题所在。如果确实如此,适当减少顶点数量有助于提升GPU性能。
有多个方法可以帮助我们减少顶点数量和顶点需要执行的操作。
首先,我们要着手降低网格不必要的复杂度。如果网格上的细节在游戏里不可见,或者创建模型时由于错误而产生多余的顶点,这都会拖累GPU性能。最简单的方法就是在建模软件里用较少的顶点数量创建模型。
法线贴图(Normal mapping)可以帮助我们在简单的网格上创建更复杂的虚拟几何细节(geometric complexity)。虽然会给GPU带来间接开销,但和提升的效果相比简直不值一谈。
如果模型网格不使用法线贴图(normal mapping)时,在导入模型设置(import settings)里关闭顶点切线(vertex tangents)。这可以减少顶点携带的数据量。
细节层次(Level of detail,缩写LOD)是一项优化技术,当 网格远离摄像机时复杂度会被降低,在不影响体验的情况下减少GPU需要处理的顶点数量。
顶点着色器(Vertex shader)是告诉GPU如何绘制顶点的着色代码段。如果游戏受限于顶点处理(vertexprocessing),降低顶点着色器(vertex shader)的复杂度能带来帮助。
结论(Conclusion)
在学习Unity的渲染机制(所有引擎的渲染机制基本相同,具体技术有差别)后,对可能导致性能的问题及解决方法做了基本探讨,运用这些知识和工具有助于我们切实地优化游戏。
以上是关于unity图形渲染优化指导的主要内容,如果未能解决你的问题,请参考以下文章
Unity3D 官方移动游戏优化指南8.图形和 GPU 优化