<font color="#9a9a9a">本文翻译自unity官方文档,标题为 Optimizing graphics rendering in unity games 。</font>
<font color="#9a9a9a">该译文旨在帮助客户端程序和美术了解unity的基本渲染流程以及一些优化思想,可以避免将来绕弯路。</font>
<font color="#9a9a9a">译文会跳过不必要的内容,力求精简,不懂之处可以询问。</font>
<font color="#9a9a9a">碍于水平问题,不足或错误之处请指出。</font>
<font color="#9a9a9a">转载请声明文章出处:https://blog.csdn.net/hbysywl/article/details/80369425</font>
<font size="4" style="line-height: 50px;"><strong>优化图形渲染</strong></font>
<font style="line-height: 50px;" color="Purple"><strong>关于渲染的一二事</strong></font>
在开始之前,我们有必要知道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)。
l 如果当前任务是绘制调用(draw call),GPU渲染网格。渲染发生在不同着色代码片段,由于该部分过于复杂在此不做详述,只需要知道一段称之为顶点着色器(vertex shader)的代码告诉GPU如何处理网格的顶点信息,而另一段称之为像素着色器(pixel shader)的代码告诉GPU如何绘制单个像素。
③ 这个过程反复执行直到GPU处理完所有任务。
我们已经对unity的渲染有所了解,接下来将探讨渲染过程中可能会遇到的问题。
<font color="Purple"><strong>分门别类(Types of rendering problems)</strong></font>
渲染最核心的点在于: CPU和GPU必须完成各自所有的任务,才能完整渲染一帧。只要任意一个任务处理花费时间过长,渲染将被延缓。
大多渲染问题可被划分成两种基本情况。第一种是由低效的管道(inefficient pipeline)引起的。当渲染管道中一个或多个步骤花费时间过长,便会阻断数据流导致低效的管道(inefficient pipeline)。低效的管道又被称之为瓶颈(bottlenecks)。第二种情况是推入大量的数据(data)通过管道(pipeline)。即便是最高效的管道(pipeline)处理数据的能力也是有上限的。
当游戏因为CPU执行某些任务花费时间过长而导致帧数降低,我们称之为CPU卡顿(CPU bound);相应地对于GPU,我们称之为GPU卡顿(GPU bound)。
<font color="Purple"><strong>一探究竟(Understanding rendering problems)</strong></font>
在采取措施前,我们必须使用分析工具查明问题,切忌生搬硬套。不同同问需要不同地解决方案。对于每一点修改我们都要测试其效果;修复性能问题是一门平衡之道,对某方面的优化可能会对其他方面产生负面影响。
我们将会使用两个工具帮助我们分析渲染问题:Profiler window 和 Frame Debugger。这里稍微介绍下Frame Debugger。
Frame Debugger允许我们逐步查看每一帧是如何渲染的。在Frame Debugger里,我们能看到更详细绘制调用(draw call)信息,比如绘制了什么,对应的着色器属性以及被发送至GPU的事件顺序。这些信息有助于我们找出需要优化的地方。
<font style="line-height: 50px;" color="Purple"><strong></strong></font>查明病因(Finding the cause of performance problems)
在优化渲染性能之前,我们必须确认游戏卡顿是由渲染问题导致的,而不是过度复杂的脚本使用(overly compex user scripts)。
一旦知道问题是由渲染引起的,我们必须明确到底是CPU卡顿(CPU bound)还是GPU卡顿(GPU bound),然后才能对症下药。
<font style="line-height: 50px;" color="Purple"><strong>CPU卡顿(CPU bound)</strong></font>
通常来说,在渲染里CPU执行的工作大致分成三类:
• 决定哪些需要绘制
• 为GPU准备指令
• 发送指令给GPU
这些大类包含多个单独任务,而这些任务会被多个线程(threads)交叉执行。线程(Threads)允许不同的任务同时执行,这意味着任务能被更快完成。当渲染任务被分散至多个线程(threads)时,我们称之为多线程渲染(multithreaded rendering)。
Unity的渲染流程里有三种线程:主线程(the main thread)、渲染线程(render thread)和工人线程(worker threads)。主线程(the main thread)是游戏核心CPU任务执行的地方,包括一些渲染任务;渲染线程(render thread)是专门用来发送指令(command)给GPU的;工人线程(worker threads)执行单个任务,比如剔除或网格蒙皮。哪些线程执行哪些任务取决于我们的游戏设置和硬件环境。例如多核心CPU意味着更多的工人线程(worker threads)。
由于多线程渲染(multithreaded rendering)非常负责而且跟硬件相关,在优化前,我们必须知道是哪个任务导致CPU卡顿(CPU bound)。如果游戏卡顿是因为剔除操作过于耗时,那么降低发送指令(command)给GPU的时间将会毫无帮助。
*注意,并非所有软硬件平台都支持多线程渲染(multithreaded rendering)
在Unity的Player Settings里有个Graphics jobs选项用于决定是否可以使用工人线程(worker threads)执行原本需要在主线程(main thread)或者渲染线程(render thread)完成的任务。如果某些软硬件平台该特性可用,那将会带来可观的性能提升。如果我们希望使用该特性,可以开启/关闭graphics jobs来监控对性能的影响。
<font color="Purple"><strong>找出害群之马(Finding out which tasks are contributing to problems)</strong></font>
通过Profiler Window我们可以找出哪些任务导致CPU卡顿(CPU bound)。下面将探讨问题所在。我们会介绍一些常见的问题以及相应解决方案。
<font color="Purple"><strong>向GPU发送指令(Sending commands to the GPU)</strong></font>
向GPU发送指令所花费的时间是最常见导致CPU卡顿(CPU bound)的原因。在多数平台上,该任务在渲染线程执行(render thread),在某些特定平台可以被工人线程(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)数量并没有降低通道设置调用(SetPass call)次数,但这依然对优化性能产生作用。因为比起多个批处理(batches),CPU处理起单个来效率更高。
一般来说,依然有3种方式降低批处理(batches)数量和通道设置调用(SetPass call)次数:
• 减少渲染“对象“(objects)可能同时降低批处理(batches)数量和通道设置调用(SetPass call)次数。
• 减少每个“对象“(object)的渲染次数通常可以降低通道设置调用(SetPass call)次数。
• 合并网格数据至更少的批处理(batches)有助于减少批处理(batches)数量。
不同的技术适应不同类型的游戏,我们需要全面考虑这些方案并决定在游戏中采用哪种技术。
<font color="Purple"><strong>减少渲染“对象“数量(Reducing the number of objects being rendered)</strong></font>
减少渲染“对象“(objects)数量是减少批处理(batches)数量和通道设置调用(SetPass call)次数最简单的方法。有好几种技术可以实现该方法。
• 简单地减少场景里可见地物体。如果减少可见物体数量同时并不会影响体验,那么该方案远比复杂的技术快速有效。
• 我们可以通过摄像机的远裁剪面(Far Clip Plane)属性来降低摄像机的绘制距离。超过该距离的物体将不被渲染。
• 对于一个基于详细距离来隐藏物体的方法,我们可以使用摄像机的层级剔除距离(Layer Cull Distances)属性来定制不同层级(layers)物体的剔除距离。比如一个拥有众多装饰细节的地表,当我们远离地表时,可以适当隐藏装饰细节。
• 还可以使用一种称之为遮罩剔除(Occlusion Culling)技术,即关闭(disable)被遮挡的物体,但会增加CPU的负荷。
<font color="Purple"><strong>减少“对象“的渲染次数(Reducing the number of times each object must be rendered)</strong></font>
实时光照、阴影和反射带来更真实的效果同时也加重资源开销。使用这些特性会导致“对象“(objects)被渲染多次,影响性能。
具体的影响取决于我们所设置的渲染路径(rendering path)。渲染路径(rendering path)是指在渲染场景时计算(calculations)执行的顺序,而不同的渲染路径(rendering paths)之间最大的区别在于如何处理光照、阴影和反射。作为通用准则,延迟渲染(Deferred Rendering)适合于高端硬件,并且对实时光照、阴影和反射有更高需求。向前着色(Forward Rendering)则用于低端硬件,同时对上述特性没有需求。
不管我们采用哪个渲染路径(Rendering Path),实时光照、阴影和反射都会对性能产生影响,而如何优化这些特性就显得格外重要。
• 在Unity里,动态光照(Dynamic lighting)是一个非常复杂的话题而且深入讨论则超出本文范围,有兴趣可以去unity官网或网上查阅资料。
• 动态光照(Dynamic lighting)是昂贵的。当场景里有许多静态物体时,可以采用烘焙(Baking)技术提前计算光照信息。
• 如果希望使用实时阴影,那么在Quality Settings里调整阴影参数有助于优化性能。
• 反射探针(Reflection probes)可以创建真实的反射效果,但会大幅增加批处理(batches)。尽量减少对反射探针(Reflection probes)使用,以确保对性能的影响。
<font color="Purple"><strong>合并“对象“至更少的批处理中(Combining objects into fewer batches)</strong></font>
在某种情况下,一个批处理可以包含多个对象(objects)所需的数据。
了执行批处理,对象(objects)必须:
• 共享同一个材质(material)实例
• 拥有完全相同的材质设置(例如,纹理、着色器和着色器参数)
虽然批处理“对象“可以提升性能,但前提是批处理(batching)本身所带来的消耗没有超过性能的提升,否则得不偿失。
目前有多种批处理(batching)技术:
• 静态批处理(Static batching)允许Unity批处理(batching)静止”对象”。
• 动态批处理(Dynamic batching)允许Unity批处理(batching)运动的对象。动态批处理(Dynamic batching)有许多限制条件,<a href="https://docs.unity3d.com/Manual/DrawCallBatching.html?_ga=2.217638166.1…;点此跳转查阅</a>。而且该技术对CPU开销有负面影响,可能导致CPU花费更多时间进行批处理(batching),在使用时需多加注意。
• 批处理(Batching)UI元素是一个复杂的话题,详细参考官方<a href="https://unity3d.com/cn/learn/tutorials/topics/best-practices/guide-opti…;另外一篇文章</a>。
• GPU实例化(GPU Instancing)可以用来快速绘制相同的物体。但并不是所有软硬件平台都支持该功能。
• 纹理图册(Texture atlasing)用于将多张纹理合并成一张更大的纹理。主要用于2D游戏和UI系统。
• 在Unity中,我们可以将共享同一个材质和纹理的网格进行手动合并,不管时编辑模式还是运行时候。在合并网格时,我们必须意识到阴影、光照和剔除依然会作用于每一个“对象“,这就意味着合并网格带来的性能提升会被本应被隐藏的对象却因为合并而显示带来的消耗所低效。
• 我们必须非常谨慎的访问Renderer.material。这会拷贝材质(material)并返回拷贝的引用,导致批处理(batching)失效。
<font color="Purple"><strong>剔除、梳理和批处理(Culling, sorting and batching)</strong></font>
剔除(Culling),搜集将被绘制“对象“(objects)的数据,梳理数据至批处理(batches)中和生成GPU指令都会导致CPU卡顿(CPU bound)。这些任务既可以在主线程(main thread)或工人线程(worker thread)上执行,具体取决于软硬件平台。
• 剔除(Culling)本身不太可能消耗性能,但减少不必要的剔除(Culling)操作依然可以提升性能。对于所有活跃的场景“对象“(objects),依然存在每对象-每摄像机的间接开销,即便因为层级(layers)不同而没被渲染出来。为了减少这些不惜要的开下,我们需要关闭不再使用的摄像机和”对象“(objects)。
• 批处理(batching)可以大幅提升向GPU发送指令的速度,但有时也能增加意想不到的间接开销。如果批处理操作(batching operations)导致CPU卡顿(CPU bound),我们则要慎重使用该技术。
---------------------
来源:CSDN,作者:布尔君de二次方
原文:https://blog.csdn.net/hbysywl/article/details/80369425