Unity性能优化-图形渲染优化

发表于2018-05-22
评论2 1.21w浏览

Unity性能优化-图形渲染优化

这里有一篇写得很好的文章,请看这个: 
【Unity技巧】Unity中的优化技术


参考文献:https://unity3d.com/cn/learn/tutorials/temas/performance-optimization/optimizing-graphics-rendering-unity-games

渲染流程简介

在本文中,将会使用“对象”指代游戏中需要被渲染的对象,任何含有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的指令全部处理完。

渲染问题的类型

引起渲染问题的基本原因有两个:

  1. 渲染管线(Rendering Pipeline)效率低,渲染管线中的一步或多步操作花费过多时间将导致数据流通不畅,这些低效问题被称为 瓶颈(Bottleneck)
  2. 在渲染管线中加入了太多数据,即使是在效率最高的渲染管线中,也有对一帧中处理的数据量的限制。

如果游戏中因为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中的每个顶点使所做的工作。顶点处理的开销与两个因素有关:需要进行渲染的顶点的数量和对每个顶点所要进行的操作。

优化顶点处理方法

  • 降低Mesh复杂度。
  • 使用 法线贴图(Normal Map),请参考法线贴图手册
  • 禁用不使用法线贴图的模型的 顶点切线,这可以减少每个顶点发送给GPU的数据量,请参考模型导入设置
  • 使用 LOD,让物体根据离相机的距离展示不同的细节,请参考LOD组手册
  • 降低 顶点着色器(Vertex Shadar)代码复杂度。顶点着色器是用于控制GPU如何绘制每个顶点的Shader代码段,优化方式请参考片元着色器优化方式的第1、3条。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引