Unity性能优化-图形渲染优化
Unity性能优化-图形渲染优化
这里有一篇写得很好的文章,请看这个:
【Unity技巧】Unity中的优化技术
渲染流程简介
在本文中,将会使用“对象”指代游戏中需要被渲染的对象,任何含有Renderer组件的GameObject都会被称为对象。
通常使用 渲染管线 来描述渲染流程,其过程可以大致描述为:
- CPU决定哪些事物需要绘制以及它们如何被绘制。
- CPU向GPU发送指令。
- GPU根据CPU的指令对事物进行绘制。
每渲染一帧画面,CPU都会进行如下工作:
- CPU检查场景中的每个对象来确定它是否需要进行渲染。只有满足指定条件的对象才会被渲染,例如:对象必须在相机的视锥体(Frustum)内并且没有被剔除(Culling)时才会被渲染。
- CPU将每个需要渲染的对象的Mesh渲染数据编排到 Draw Call 指令中。某些情况下,一些共享配置的对象可能被合并到同一个Draw Call中,这个过程称为 批处理(Batching),参考Draw Call批处理手册。
- CPU为每次Draw Call创建一个称为 Batch 的数据包。
每次Draw Call,CPU要执行下列操作:
- CPU可能会向GPU发送 SetPass Call 指令来修改一些被统称为 Render State 的变量。每个SetPass Call都会告知GPU在下次渲染Mesh时要使用哪个配置。只有在下次需要渲染的Mesh的Render State与当前的Render State不同时,才会有SetPass Call。
- CPU向GPU发送Draw Call指令。Draw Call指令告知GPU使用最近一次的SetPass Call的配置对指定的Mesh进行渲染。
- 在某些情况下,一个Batch可能需要不止一个 Pass。Pass是一段Shader代码,新的Pass会改变Render State。CPU必须为Batch中的每个Pass发送新的SetPass Call并再次发送Draw Call。
与此同时,GPU进行着如下的工作:
- GPU根据CPU的指令执行任务,执行顺序与指令发送顺序相同。
- 如果当前的任务是一个SetPass Call,则GPU更新Render State。
- 如果当前的任务是一个Draw Call,则GPU渲染Mesh。
- 重复上述过程直到GPU将来自CPU的指令全部处理完。
渲染问题的类型
引起渲染问题的基本原因有两个:
- 渲染管线(Rendering Pipeline)效率低,渲染管线中的一步或多步操作花费过多时间将导致数据流通不畅,这些低效问题被称为 瓶颈(Bottleneck)。
- 在渲染管线中加入了太多数据,即使是在效率最高的渲染管线中,也有对一帧中处理的数据量的限制。
如果游戏中因为CPU执行渲染任务耗时太久而导致帧渲染时间过长,我们称其为 CPU受限(CPU Bound)。
如果游戏中因为GPU执行渲染任务耗时太久而导致帧渲染时间过长,我们称其为 GPU受限(GPU Bound)。
处理CPU Bound
通常将帧渲染过程中的必须由CPU处理的任务分为三类:
- 决定什么必须被绘制
- 为GPU准备指令
- 向GPU发送指令
这几个大类中包含很多独立的任务,这些任务可能被分散到多个线程中同时执行。在多个线程中执行渲染任务被称为 多线程渲染(Multithreaded Rendering)。
在Unity的渲染过程中涉及了三中类型的线程:主线程(Main Thread)、渲染线程(Render Thread) 和 工作线程(Worker Thread)。主线程执行游戏中的大多数CPU任务,包括一些渲染任务;渲染线程专门用于向GPU发送指令;工作线程每次执行一个任务,例如剔除(Culling)或者蒙皮(Mesh Skining),CPU核数越多,可以创建的工作线程就越多。
并不是所有的平台都支持多线程渲染,比如,目前的(Unity 5.4)WebGL平台就不支持多线程渲染。在不支持多线程渲染的平台上,所有的CPU任务都在同一个线程中处理。
使用图形作业(Graphics Jobs)功能
Player Settings 中的Graphics jobs选项可以让Unity将那些本该由主线程处理的渲染任务分配到工作线程中,有些时候也会把渲染线程的任务分配到工作线程。在可以使用该功能的平台上,它将带来显著的性能提升。该功能目前(Unity 2018.1.0)仍处于试验阶段,可能造成游戏崩溃。
减少向GPU发送的数据量
向GPU发送指令的时间开销是引起CPU Bound的最常见原因。该过程在大多数平台上都会分配到渲染线程中执行,但在某些平台(比如PlayStation 4)上会分配到工作线程中执行。
向GPU发送指令的过程中,开销最大的操作是SetPass Call。如果游戏在向GPU发送指令的过程中发生了CPU Bound,那么降低SetPass Call数量可能是最好的提升性能方式。通常,降低Batch数目、让更多的对象共享相同的Render State会降低SetPass Call数目,进而提高CPU性能。即使降低Batch数目没能够让SetPass Call数目降低,它也能提升性能,因为CPU处理单个Batch的能力比处理多个Batch的能力更强,即使这些Batch中包含的Mesh数据量是相同的。
通常,有3种 降低Batch和SetPass Call数目的方法:
- 降低需要渲染的对象的数量,可以同时减少Batch和SetPass Call。
- 降低每个对象需要被渲染的次数,可以减少SetPass Call。
- 将对象合并到更少的批处理当中,可以减少Batch。
降低需要渲染的对象的数量
- 减少Scene中可见的对象数量。
- 缩短相机绘制距离。使用相机的 Far Clip Plane 属性控制相机的绘制距离,可以使用雾(Fog)掩饰远处缺失的对象。
- 在代码中设置相机的 Layer Cull Distances 属性来更细致地控制相机在不同层(Layer)地绘制距离。
- 使用相机地 遮挡剔除(Occlusion Culling) 选项关闭对被其他对象遮挡地对象地渲染。遮挡剔除并不适用于所有Scene,它会造成额外的CPU开销。这篇博客讲述了更多关于遮挡剔除地内容。
降低每个对象需要被渲染的次数
实时光照、阴影和反射可以增强游戏地真实性,但使用这些特性会导致对象被渲染多次,很影响性能。这些特性所造成地具体影响取决于游戏所使用的 渲染通道(Rendering Path)。渲染通道是指绘制Scene时的运算的执行顺序,不同渲染通道间的主要区别是处理实时光照、阴影和反射的方式。通常来说,运行在高性能机器上并且使用了较多实时光照、阴影和反射的游戏适合使用 Deferred Rendering,而运行在低性能机器上并且没有使用这些特性的游戏适合使用 Forward Rendering。
使用 烘培(Baking) 预先计算那些不会发生变化的对象的光照信息,减少实时光照的计算量。
在 Quality Settings 中调整阴影属性,控制阴影质量。
尽量少用 反射探针(Reflection Probe),它们会导致Batch数量大量增加。
Unity手册中对渲染通道进行了更多讲解。
光照与渲染介绍中对Unity的光照进行了详细的讲解。
将对象数据合并到更少的Batch当中
关于批处理优化的详细介绍,参考开头提到的文章:【Unity技巧】Unity中的优化技术 。
一个批处理中可以包含多个对象的数据,前提是这些对象满足下列条件:
- 共用同一个材质(Material)的同一个实例。
- 拥有相同的材质设置(例如:纹理、Shader和Shader参数)。
批处理可以提升性能,但是要小心使用,以免批处理过程的开销比其节省的开销还要高。
对于满足批处理条件的对象,有几种不同的优化技术:
- 静态批处理(Static Batching):用于处理临近的静态对象(被标记为Batching Static),会占用更多的内存。
- 动态批处理(Dynamic Batching):用于处理非静态对象,它的限制比较多,而且会占用更多的CPU时间。
- UI批处理:对UI的批处理比较复杂,它受UI布局的影响,请参考Unity UI优化指导。
- GPU实例(GPU Instancing):用于处理集中出现的大量的独立对象,例如粒子。GPU实例的限制比较多,而且需要硬件支持,请参考GPU实例手册。
- 纹理图集(Texture Atlasing):将多个纹理贴图合并到一张大的纹理贴图中。
- 手动合并Mesh:通过Unity编辑器或者代码将共用相同材质和纹理的Mesh合并。手动合并Mesh会影响对象的剔除,请参考Mesh.CombineMeshes。
在代码中要谨慎使用 Renderer。material
,他会将material对象复制一份并非返回新对象的引用。这会破坏对象的批处理条件,因为Renderer不再持有与其他对象共用的材质的实例。如果要在代码中获取批处理对象的材质,应该使用 Renderer.sharedMaterial
,参考Draw Call批处理手册。
剔除、排序和批处理优化
剔除、获取需要渲染的对象的数据、将数据排序到批处理中以及生成GPU指令都能导致CPU受限。这些任务可以在主线程或者独立的工作线程中执行,具体取决于游戏设置和目标硬件平台。
- 剔除操作本身的开销不算太高,但减少不必要的剔除仍可以提升性能。Scene中的每个激活(active)的相机和对象都会产生开销,应该禁用(disable)那些当前不适用的相机和Renderer。
- 批处理可以极大的提升向GPU发送指令的速度,但有时他可能造成意想不到的开销。如果批处理造成了CPU受限,就应该限制游戏中手动/自动批处理的数量。
蒙皮(Skinned Meshes)优化
SkinnedMeshRenderer 组件用于处理骨骼动画,常用在角色动画上。与蒙皮相关的任务可以在主线程或者独立的工作线程中执行,具体取决于游戏设置和目标硬件平台。
蒙皮渲染的开销比较高,下面一些 对蒙皮渲染进行优化的手段:
- 减少SkinnedMeshRenderer组件的数量。导入模型时,模型可能带有SkinnedMeshRenderer组件,如果游戏中该模型并不会使用骨骼动画,就应该将SkinnedMeshRenderer组件替换为MeshRenderer。在导入模型时,可以选择不导入动画,请参考模型导入设置。
- 减少使用SkinnedMeshRenderer的对象的Mesh顶点数,参考蒙皮渲染器手册。
- 使用GPU Skinning。在硬件平台支持并且GPU资源足够的条件下,可以在Player Setting中启用GPU Skinning,将蒙皮任务从CPU转移到GPU。
更多优化内容请参考角色模型优化手册。
减少主线程中与渲染无关的操作
很多与渲染无关的任务都主线程中执行,减少主线程中与渲染无关的任务的CPU时间消耗。
处理GPU Bound
优化填充率(Fill Rate)
填充率指GPU每秒能够渲染的屏幕像素数量,它是引起GPU的性能问题的最常见因素,尤其是在移动设备上。下面列出一些 与填充率相关的优化手段:
- 优化 片元着色器(Fragment Shader)。片元着色器是用于控制GPU如何绘制单个像素的Shader代码段,绘制每个像素点都需要执行这段代码。复杂的片元着色器是引起填充率问题的常见原因。
- 如果在使用Unity内置Shader,优先使用能够满足视觉效果要求的最简单高效的Shader。例如,Unity内置的几种适用于移动平台的Mobile Shader都经过高度优化,而且它们也可以于移动平台以外的其他平台,如果它们提供的视觉效果能够满足需求,就应该使用这些Shader。
- 如果在使用Unity提供的Standard Shadar,Unity会根据当前的材质设置对Shader进行编译,只有当前使用了的属性才会被编译。这意味着,移除材质中的一些可以省略的属性(例如Secondary Maps(Detail Maps))能够提升性能。
- 如果在使用自行编写的Shader,应该尽可能对其进行优化,请参考Shader优化提示。
- 减少 重绘(Overdraw)。重绘指对同一个像素点进行多次绘制,这种情况发生在对象被绘制在其他对象上层时,它会极大地增加填充率。最常见地引起过渡重绘地因素是透明材质、未经优化地粒子和重叠地UI元素,应该对这些内容进行优化或者减少使用量。请参考:渲染队列、子着色器标签和填充率、画布和输入。
- 减少 图像特效(Image Efficts)。图像特效是引起填充率问题的重要因素,尤其是在使用了多个图像特效时。在同一个相机上使用多个图像特效会产生多个Shader Pass,将多个图像特效的Shader代码合并到单个Pass中可以提升性能,参考后期处理。
优化显存带宽(Memory Bandwidth)
显存带宽指GPU对其专用的内存的读写速率。如果游戏出现带宽受限,通常是因为使用了太大的纹理(Texture)。
优化纹理的方法:
- 纹理压缩(Texture Compression)可以极大的减少内存和磁盘占用。
- Mipmap 是用在远处的物体上的低分辨率版本纹理。如果游戏中需要显示离相机很远的物体,则可以使用Mipmap减少显存带宽占用。
纹理优化的具体细节,请参考纹理手册。
优化顶点处理(Vertex Processing)
顶点处理指GPU渲染Mesh中的每个顶点使所做的工作。顶点处理的开销与两个因素有关:需要进行渲染的顶点的数量和对每个顶点所要进行的操作。
优化顶点处理方法: