UE4学习笔记:实时渲染原理

本随笔介绍UE4引擎在渲染及其性能影响方面的工具和概念,同时也包括一些光照基础概念。该随笔整理自官方教程视频:实时渲染原理

本随笔作者还在学习阶段,对于UE4引擎的使用和理解还不是非常透彻,难免出现书写上和技术上的问题,若出现了类似的问题欢迎在评论区或私信与我讨论。

目录

实时渲染(Real—Time Redndering,RTR)

  • 只有在项目为空白项目的时候,实时渲染才能达到最高性能。

  • 从那以后在项目里做的任何事情都会让项目损失性能。

  • 实时渲染流程的本质是管理性能损耗。

  • 需要设计者在“最佳性能”、“最佳画质”和“最佳特性”三者之间寻找到一个合适的平衡点。

  • 设计者必须清楚性能会在哪里损耗,或会在哪里提升。

  • 例如你有内存和处理器,如果你很清楚你添加的内容会对处理器造成很大的负担,那么后续的工作流应该更加依赖内存,这样才能将负担分摊到所有可用的硬件上。

  • 渲染不仅和硬件组件有关,也和带宽有关。

  • 渲染在硬件上的基本过程:硬盘等类似的设备用于存储数据,然后数据被传输到内存里,内存负责存储我们要使用到的数据,之后数据会在CPU和GPU之间来回传输(内存与CPU互相传输数据和内存与GPU之间互相传输数据,这两部分多数时候是同步执行的)

    渲染之前发生的过程

    • CPU和GPU是同时(并行)处理的,但不是在同一个时间点处理同一帧的内容。
    • 渲染之前首先会由CPU进行画面内容的处理,然后交由绘制线程进行画面后续内容的处理,最后是GPU进行最终画面的处理。该过程各个硬件之间会以相同的速度和间隔同步执行,且严格以“CPU->绘制线程->GPU->CPU”的顺序执行。
执行时刻(30FPS) CPU 绘制线程 GPU
0毫秒 A帧    
33毫秒 B帧 A帧  
66毫秒 C帧 B帧 A帧

执行时刻0毫秒开始,第A帧开始处理对象逻辑及位置信息:

  • 我们最先需要知道对象的位置,这也是这一时刻CPU的工作:计算所有逻辑和变换。如动画、对象和模型的位置、物理效果、AI、生成和销毁,隐藏和取消隐藏等一切和改变对象位置有关的工作。
  • CPU处理完毕之后,我们现在知道了场景里面所有对象的位置,但是却不清楚我们能够看到什么,或者说哪些对象需要被看到哪些对象不需要被看到

执行时刻33毫秒开始,第A帧画面中对象逻辑及位置信息计算完毕,开始计算哪些对象需要被绘制:

  • 计算哪些对象需要被看到哪些对象不需要被看到,这就是绘制线程在这一时刻的工作,因为某些时候(比如说在玩家视角中隔着一面墙的对象)在玩家眼中并不会被看到,因此该对象并不需要被绘制。
  • 判断对象是否需要被绘制就涉及到引擎中的“遮挡过程(Occlusion Process)”。
  • 遮挡过程会为场景中所有可见对象和模型建立一张表,也会为所有不可见对象和模型建立一张表。遮挡会逐对象进行而不是逐多边形进行。
  • 遮挡过程一共分为四步且按照顺序依次执行(这四个步骤共构成了一个混合方案):
    1. 距离剔除(Distance Culling):
      • 将任何距离超过X的对象移除。
      • 该设置默认不会启用。
      • 每个可见对象都会包含最大绘制距离(Max Draw Distance)属性,不仅是模型,粒子也是如此。
      • 该属性的设置可在细节面板(Details)->渲染(Rendering)->LOD下可设置,在该属性里不仅可以设置最大绘制距离(超过该距离则该对象不进行绘制)还可以设置最小绘制距离(小于该距离则该对象不进行绘制)。需要注意的是最小绘制距离的设置即可以在编辑器模式也可以在运行模式里看到效果,而最大绘制距离只能在运行模式里看到效果(其实也能在编辑器模式下看到,但是需要按“G”快捷键将场景从编辑器视图切换为游戏视图,感觉应该是个BUG)。
      • 最大绘制距离分为“期望最大绘制距离(Desired Max Draw Distance)”和“当前最大绘制距离(Current Max Draw Distance)”,这两个的区别是后者是经由“剔除距离体积(Cull Distance Volume)”来控制,且该属性呈灰色不可手动调整,前者可通过手动调整并且会覆盖后者设置的距离。
    2. 视锥体剔除(Frustum Culling):
      • 视锥体剔除会检查摄像机能够看到什么内容,视野以外的对象会被自动剔除。
      • 该设置默认启用且不能被禁用。
    3. 预计算可见性(Precomputed Visibility):
      • 每个预计算可见性单元格(在编辑器模式下呈蓝色方格状,可视化的方式为勾选显示(Show)->可视化(Visualize)->预计算可见性单元格(Precomputed Visibility Cells))都保存了一张周围哪些物体是可见的哪些物体不是可见的表格,当摄像机进入到某个单元格的时候,摄像机会询问单元格“哪些物体被遮挡,我应该渲染什么,不应该渲染什么”,然后引擎就会根据当前单元格存储的表格回复“我记得这里应该渲染这个这个,不应该渲染那个那个”,通过提前计算的方式来确立渲染对象。
      • 该设置默认禁用。
      • 可以通过勾选世界设定(World Settings)->预计算可见性(Precomputed Visibility)分类下面的复选框“预计算可见性(Precomputed Visibility)”,然后添加一个“预计算可见性体积(Precomputed Visibility Volume)”到关卡中,让该体积立方体包括住需要预计算的内容,最后在构建(Build)按钮的下拉框里选择构建“预计算静态可见性(Precomputed Static Visibility)”,体积立方体内的所有内容都会被预计算可见性的单元格(呈蓝色方格状,大小可在勾选复选框同一位置的分类里进行设置,高度需要在Lightmass的ini文件里设置)填充。
    4. 遮挡剔除(Occlusion Culling,四个步骤中最为精确的一步,通常能处理大部分情况):
      • 遮挡剔除会精确地计算每个模型地可见性状态,例如玩家在一面墙的这一边,一个物体在那一边,因为物体被墙壁遮挡了,所以我们不会去渲染墙壁那一边的物体。
      • 默认启用,而且应该不能被禁用。
      • 这会消耗很多性能,因为需要询问每个对象是否需要被渲染。
      • 之所以要将最精确的剔除方案放到最后的步骤来进行处理,是为了负担其“最大的性能损耗”,因为经过前三个步骤之后场景中的大部分对象已经确立好了其自身的可见性,最后再使用遮挡剔除功能来计算剩余对象的可见性,就比一开始使用遮挡剔除功来得高效。因此也可得出该四个步骤的性能损耗程度是依次递增的。
  • 可通过引擎命令“Stat InitViews(Initialized Views)”可提供关于剔除对象相关的统计信息。
  • 优化渲染小贴士:
    • 设置距离剔除(Distance Culling),虽然该设置默认禁用,但是该设置也是非常容易设定的一个设置。
    • 当你的场景里面存在超过10,00或15,000个对象时,性能就会开始受到影响,需要开始着手优化了。
    • 这种性能损耗大部分都是CPU上的损耗,但是也会有部分的GPU损耗。
    • 大型开放式场景可能会没有办法很好地应用剔除,因为玩家将能看到一切对象,因此可以设法弄一些障碍物之类。
    • 几乎一切可见对象能够被遮挡,包括粒子对象。粒子对象是通过边界盒(Bounding Box)来确立遮挡的,在优化粒子的时候需要多注意这一点。
    • 大型模型因为很容易进入视野内,因此很少会被遮挡,且因为需要渲染的内容增加,会增加GPU的性能损耗。
    • 由部件拼凑出来的大型模型虽然可以实现遮挡,但是这会加大需要绘制的对象数量,因此会增加CPU的性能损耗。
    • 上述两点注意事项就要求我们根据实际项目的情况进行一个平衡。

执行时刻66毫秒开始,第A帧画面中对象位置及需要渲染哪些对象的需求已经计算完毕,开始进行如下所述的渲染工作。

渲染中发生的过程

几何体渲染

  1. 首先需要通过预通道(Prepass)或前期深度通道(Early Z Pass)来做一个深度检测。
    • 我们知道对象的位置以及需要渲染的对象,但是不清楚这些对象的渲染顺序,通过深度检测,当一个小型模型(如一个茶壶)“遮挡”到一个大型模型(如一面超大的墙,但是因为小型模型没有完全遮挡大型模型所以不会执行遮挡剔除操作)时,我们可以确立墙的模型只需要渲染除茶壶模型遮挡的部分以外的像素即可,就像一面墙上有一个茶壶的剪影,渲染部分就是除“剪影”以外的像素。如果像这样排列的模型很多的话,就会一个个计算每个模型在墙面上的“剪影”并叠加起来,最终确立的“剪影”就是不需要渲染的像素。
  2. 接下来就是实际的几何体渲染。
    • 几何体渲染会根据绘制调用逐个渲染。
    • 不能说一个模型算做一个绘制调用,一个模型上的一个材质也算是一个绘制调用。绘制调用就是一组拥有相同属性的多边形,很明显一个模型用到两种材质,用到两种材质的部分肯定不属于拥有相同属性,因此会算作两次绘制调用(因此不能说一个模型算作一个绘制调用,模型上不同材质的部分会被当成两个对象来渲染)。
    • 使用引擎命令“Stat RHI(Rendering Hardware Interface,渲染硬件接口)”可以显示绘制调用相关的统计信息。
    • 由于UE4引擎非常复杂,在渲染对象的背后有许多看不到的过程,因此就算你创建一个空白关卡,也会有大概100次左右的绘制调用(这里的绘制调用主要指“DrawPrimitive Calls”而不是“Mesh Draw Calls”,后者仅仅指模型的绘制调用,前者则指场景里全部对象的绘制调用,包括模型、阴影、粒子等)的基本损耗。
    • 2000到3000次左右的绘制调用是正常的,也是大部分游戏的标准,对于移动端和VR来说,这个标准值通常为几百或1000左右。5000次左右绘制调用高于硬件标准,但是仍能运行。10000次绘制调用则有可能会导致运行出现问题。
    • 对于UE4和现今的硬件来说,绘制调用及其他因素对性能产生的影响已经比多边形面数大得多
    • 渲染器(GPU等)每次渲染完一个绘制调用,都会开始请求下一次绘制调用,然后继续渲染,这样就会形成“渲染->请求->渲染”这样的停顿,该“请求停顿”正是绘制调用次数过高之后对性能造成影响的主要原因。需要注意的是组件也会导致绘制调用,组件也是逐个进行遮挡和渲染的,和单个的Actor没有区别。
    • 使用少量多面数的大模型而不是大量少面数的小模型可以降低绘制调用的次数,但是因此会造成大模型更不容易被遮挡、会加大光照贴图纹理所占用的空间、碰撞检测变得复杂(因为一个物体碰上小模型的立柱的话,只会去检测该立柱的碰撞网格体,但是如果碰上了大模型的立柱的话,会检测立柱、屋顶、地板等整个大模型组成的碰撞网格体)和加大内存的占用。
    • 发挥最好性能的方法就是衡量何时使用单个大模型何时使用多个小模型即何时合并模型:
      • 合并常用面数很低的模型。可以通过“窗口(Window)->统计(Statistics)”的统计窗口来快速获取每个模型的面数以及使用次数。这会减少绘制调用,且只增加了很少的面数。
      • 只合并同一个区域内的模型。如果你将第一个房间内的东西和第六个房间内的东西合并为一个模型的话,这对遮挡没有任何帮助,还会增加碰撞的复杂度。
      • 只合并使用同一种材质的模型。如果你的每个小模型使用的都是不同的材质,那么合并之后的大模型就会有多少种材质,这对减少绘制调用没有任何帮助。
      • 合并没有或简单碰撞的模型。这在能减少绘制调用的同时也让合并后的模型不会有太复杂的碰撞网格体。
      • 合并较小的或接收动态光照的模型,因为这类模型合并了之后其光照贴图也并不会很大。
      • 合并远距离的网格体。就算合并后的网格体体积很大,但是在远距离看起来也会很小,遮挡剔除的功能可以很好的起作用。
      • 在非常低端的硬件设备上(如移动端、VR和移动端VR),你可能需要合并所有对象。
    • UE4默认会在内存中实例化模型,即一个大小为400kb的模型,无论你在场景里面放置多少个该模型,该模型所占用的内存只有400kb,但是在渲染过程中并不会实例化,有多少个该模型的对象就需要有多少次渲染,因此可以使用实例化静态网格体(Instanced Static Mesh)来让该模型在渲染过程也进行实例化。实例化静态网格体是一种类似合并、且能有效降低绘制调用的方法,该方法是通过将模型添加到实例化静态网格体组件(Instanced Static Mesh Component),然后通过组件来生成模型的实例化,用这种方法生成四个实例化的模型的话,该四个实例化模型会被当成一个模型来渲染,只是大小有原来模型的四倍大,就像合并一样,但是每个该组件只支持实例化一个模型,不像合并模型的功能一样能将不同的模型都合并进来。实例化静态网格体的功能没有默认启用是因为,如果启用了的话,每次渲染一个新模型的时候,渲染器都要去询问引擎其他所有模型是否和该模型一致,这种询问会导致更加严重的性能损耗产生。实际上实例化静态网格体的功能就相当于是给引擎一张简表,简表上面记录了场景中哪些模型是完全一致的,这样就省去了渲染器询问引擎的过程,而直接在渲染过程上实现了实例化。
    • 使用LOD(Level Of Detail,细节层次)可以在摄像机距离模型较远的时候降低面数,这里有几个需要掌握的规则:
      • 确保每一个LOD面数是前一个LOD面数的50%或更低。至少需要确保我们优化出来的性能必须抵消因为使用LOD而造成的性能消耗。
      • 常规LOD已经足够好用,但是其存在一个问题,如果一个雕像及其下三个底座都设置了LOD,虽然这四个模型能够在特定的摄像机距离上切换LOD,但是这四个模型同时也产生了四次绘制调用。HLOD(Hierarchical Level Of Detail,分层细节层次)解决了这个问题,它会在远距离观察这四个模型的时候,将其合并为一个模型,近距离观察的时候又变成了原来的四个模型,这样当我们在远距离观察模型的时候,可以很好地消减绘制调用次数。只需要勾选“世界设置(World Settings)->LOD系统(LODSystem)->启用分层LOD系统(Enable Hierarchical LODSystem)”,然后点击“窗口(Window)->分层LOD大纲(Hierarchical LOD Outliner)”即可打开创建HLOD的界面。
      • 请在室外环境使用LOD和HLOD,否则在室内环境里很少会遇到摄像机能在LOD设置的距离外观察到对象物体的情况,且使用LOD和HLOD还产生了额外的消耗,变成得不偿失的情况。
      • 顶点着色器开始处理顶点着色也是在这个阶段进行的。
      • 骨骼网格体动画和顶点动画的取舍可以通过以下规则来进行判断:
      • 动画效果越复杂,效率就会越低下,如果涉及到性能损耗特别高的动画(例如大草原里有很大数量的草被风吹的动画),可以使用顶点动画来提高效率。
      • 面数特别多(也就代表顶点特别多)的模型,可以使用顶点动画来提高效率(因为不需要CPU来计算每个顶点的位置,而是通过GPU的顶点着色器来计算,将CPU的负担交给了GPU)。
      • 对于一些远距离的动画对象,可以通过禁用世界坐标偏移来提升效率。

顶点着色器(Vertex Shader)

  • 主要是在几何体渲染过程中使用到。
  • 顶点着色器主要是负责三件事:
    1. 将顶点的本地坐标转换为世界坐标。
    2. 处理平滑过渡、硬边缘、柔化边缘以及顶点的颜色。
    3. 额外偏移。例如材质节点中的世界坐标偏移(World Position Offset)就可以实现偏移的功能,世界坐标偏移就是通过顶点着色器来偏移并移动网格体
  • 顶点着色器可以用来偏移布料的世界坐标,布料是由顶点着色器驱动的。
  • 水面的位移、波浪的位移也是通过顶点着色器来驱动的。
  • 植被被风吹的动画也是由顶点着色器来驱动的。如果如果风吹草动的效果是由蓝图动画来驱动,那意味着我们需要设置骨骼网格体,也就意味着骨骼位置需要随时更新,这样就需要CPU来参与计算,如果使用顶点着色器则把计算的负担分摊给了GPU。
  • 需要注意的是,用顶点着色器实现的位移效果仅仅只是渲染的效果,实际上的模型并没有跟着移动,也就是说碰撞和物理效果并不会产生变化

渲染结束后,将图像呈现到屏幕上的过程

光栅化过程(Resterization Process)

  • 所谓光栅化过程,就是将3D数据(顶点数据)转换为像素的过程。光栅化基本上就是将前几个步骤确立好的模型渲染成图像。光栅化会使用许多像素格来渲染已经准备好的信息,像素格是方形的,这也意味着整张图像实际上是一张很大的网格。渲染多边形的时候,多边形边所代表的线条有时候会斜着跨越这些像素格,计算这些像素格哪些应该被渲染,计算这个结果的数学过程被称为光栅化过程。
  • 一个像素点永远只会同时用于显示一个多边形。可能是多边形的一部分,也可能是整个多边形。但是同时其表示的多边形只会是一个。
  • 光栅化按照绘制调用逐次进行。
  • 光栅化过程很多步骤你都无法改变,但是过度着色(Overshading)是你可以控制的。假如你有一个10万多边形的模型,当你在非常非常远的地方去观察这个模型的时候,它仅仅只会占用一个像素的大小,只会看到一个多边形,但是我们依然需要去计算这10万个多边形,这对性能来说是没有任何意义的,这样“过度”去计算不需要的多边形并渲染成图像的过程,就叫过度着色。过度着色一共会发生两次。
  • 光栅化的着色过程就是给像素点上色的过程,因为硬件设计的原因,着色的时候并不是逐像素点去着色,而是根据2x2的像素格去进行着色,这意味着如果一个多边形非常的细小非常的薄,显示出来的就只有一个像素点有颜色,但是实际上着色器会对其本身及周围共四个像素点进行着色,这就是第一次过度着色。第一次过度着色发生在对可见像素以外的其他像素进行着色的时候。
  • 在上面那个三角形旁边新生成一个三角形,如果这个新三角形需要渲染的像素格(即2x2的像素围成的方格)和之前的三角形需要渲染的像素格有重叠,那么之前已经渲染过的像素格需要再次被渲染一次,这就是第二次过度着色。第二次过度着色发生在其他多边形和模型覆盖相同的4像素区域的时候,它们会再次处理这些相同的像素区域。
  • 通常,引擎会计算如果像素的中点位于三角形的内部,那么整个像素将被着色。
  • 四边形过度绘制(Quad Overdraw)视图模式可以将场景中存在过度绘制的材质可视化,可以点击“窗口(Window)->性能视图(Optimization Viewmodes)->四边形过度绘制(Quad Overdraw)”来切换。距离摄像机越远的对象渲染的越慢,性能损耗也越高,因为会有更多的多边形重复覆盖更小范围的像素区域(参考上文的第二次过度着色)。该视图下面展示了什么样的颜色说明了是什么程度的过度绘制,一般对象能达到的最大等级过度绘制为绿色代表的4级,比这个等级高的对象只有可能是透明对象。
  • 为了解决过度绘制的问题,可以通过设置LOD或剔除,降低多边形数量的方式来解决。LOD有助于显著地解决多边形带来地过度绘制问题
  • 初始像素着色器通道(Initial Pixel Shader Pass)越复杂,过度着色损耗越大,因为前向渲染中像素着色器通道损耗(比起延迟渲染)更大,因此过度着色对前向渲染效率产生的影响比延迟渲染更大,在延迟渲染中,过度着色通常也会增加损耗,但并不会很大,甚至可以忽略,但是在前向渲染例如VR应用、手机应用等就需要着重去注意
  • 经过光栅化的这些步骤之后,引擎将会渲染出来一张张各种各样的图像,这些图像就被成为G缓存。

G缓存(G-Buffer)

  • 从现在开始的步骤我们不再依靠几何体计算结果,我们只用图片
  • G缓存用于合成各种内容,报错材质、光照、雾效等等。
  • G缓存知道每个像素的材质属性、知道像素颜色、知道像素的朝向、还知道每个像素离开摄像机的距离,这些就是G缓存中不同图像各自存储的信息。引擎会使用这些信息进一步渲染。
  • G缓存中另一个重要的概念是自定义深度(Custom Depth)会更进一步让我们将某个对象与其他对象分开,我们可以为任何模型开启自定义深度属性,只需要勾选“细节面板(Details)->渲染(Rendering)->渲染器自定义深度通道(Render Custom Depth Pass)”就可启用。自定义深度会用单独的渲染目标或者说G缓存来包含模型,然后可以用于各种特效,例如轮廓特效等等。可以在使用“视图设置(View Options)->高分辨率屏幕截图(High Resolution Screenshot)”里勾选使用自定义深度作为蒙版(Use Custom Depth as Mask)就能看到效果。
  • G缓存会占用很多内存和带宽,因此我们如果想把引擎扩展到拥有更多G缓存,好让引擎拥有更多信息来处理之后的特效,一定要考虑到这两个硬件(内存和带宽)带来的限制。

纹理(Texture)

  • 压缩纹理的原因主要是为了节省内存和带宽。
  • 纹理在被导入的时候总会被压缩,且在导入时候引擎都会自动压缩纹理。压缩格式会根据平台的不同而不同,所有不同类型的硬件都有着各自纹理的压缩格式。
  • BC(Block Compress,块压缩)格式,或更为熟知的被称为DXTC格式、DXT格式(是“DirectX纹理压缩(Direct X Texture Compress)”的简称)是用于PC上的纹理压缩格式,块压缩正如其名,通过生成像素块来压缩纹理,基于它在像素块(即上文中提到的2x2范围大小的像素集合)中找到的颜色,它会改变其中一些颜色以简化,然后逐块对纹理进行压缩,这就是块压缩。需要注意的是法线贴图使用特殊的压缩设置,法线贴图的压缩使用过保存红色和绿色颜色通道,再经过一些额外的过程来计算出蓝色通道的值,而不是通常的计算像素块来进行压缩。
  • UE4大多数时候都会在后台处理处理压缩。
  • 纹理的压缩和JPEG很相似,如果仔细观察纹理贴图,我们可以发现一些“块状结构”。
  • BC3(DXTC5)用于带有阿尔法通道的纹理,BC1(DXTC1)用于不带阿尔法通道的纹理。这两种是我们大多数时候都会用到的、非常重要的纹理压缩格式。任何时候只要纹理没有阿尔法通道它就会使用BC1(DXTC1)格式,只要有阿尔法通道它就会使用BC3(DXTC5)格式。
  • 在纹理编辑器页面里看到的资源大小(Resource Size)是压缩后的纹理大小而不是纹理原本的大小,格式(Format)也指出了该纹理是以什么格式进行压缩的。
  • 无论纹理有没有被压缩,每个着色器的纹理采样数量存在着最大限制。
  • 上文说纹理分辨率会影响内存和带宽,但它很少影响渲染效率。当你的项目存在延迟和卡顿的问题时(而不是持续性的帧率下降),很可能都是因为你的带宽或内存不够导致的。
  • 为了最大化内存的效率,我们可以使用多级渐进纹理(Mipmap),它是由许多只有原纹理四分之一大小的的纹理组成,它会不停地进行复制,后续纹理都是前者纹理的四分之一。这些多级渐进纹理会被保存到DDS文件和纹理本身内,纹理编辑器里的“细节面板(Details)->细节层级(Level Of Detail)->LOD偏移(LOD Bias)”表明了当前纹理编辑器视口里显示的是哪一级渐进纹理。多级渐进纹理是自动生成的,不需要我们去手动设置。使用多级渐进纹理的另一个原因是减少纹理在视口远处产生的噪点,另另一个原因就是为了处理纹理流送。
  • 纹理流送(Texture Streaming)就是确定引擎在何时需要哪张纹理以及哪些多级渐进纹理的过程,因为你肯定不会想要一次性将所有纹理及所有多级渐进纹理全部导入进项目里,这样的话硬件的内存和带宽很快就会用完了。
  • 为了让纹理能够正确地被流送,我们必须将纹理的边长设置为2的指数幂如果你的纹理的某一边长没有2的指数幂,那这张纹理将不会生成多级渐进纹理,也不会被正确流送。
  • 某些特殊处理会需要这种“劣势”,例如被UI使用到的纹理,因为我们永远不会从不同距离观察到这张纹理,也就是说这张纹理实际上是不需要流送和多级渐进纹理的,像这类永远不会从不同距离观察到的纹理可以不遵循“纹理边长必须为2的指数幂”这个规定。
  • 纹理大小相关的问题主要是延迟和卡帧(指在正常运行下突然卡顿,过一会儿又恢复正常运行的情况),而不是帧率丢失(指持续性的实际帧率小于目标帧率的情况)。当你启动游戏的时候看到的是一张模糊的纹理,过一阵子才会看到完整的纹理,这就是“延迟”,会发生这种情况是因为电脑没有足够的带宽或内存来快速传输完整分辨率的纹理,进而使用一张低分辨率并且很模糊的多级渐进纹理。纹理池是供纹理使用的一段内存,它会被游戏中的纹理塞满,如果这些纹理的大小超出了纹理池的大小,则会发生部分纹理不能显示完整分辨率,而只能显示低分辨率的情况。

材质(Material)

  • 引擎使用像素着色器来完成整个材质系统。

像素着色器(Pixel Shader)

  • 渲染流程当中的每一个步骤(从几何体渲染开始,直到渲染完成)几乎都是由用像素着色器来完成的:修改输入值->重新计算->输出结果,可用于实现实时光照、所有着色、材质、雾、反射、后期处理特效等等。
  • 例如当需要生成雾效的时候,像素着色器会得到屏幕中某个像素的输入值,获得该输入值之后就会根据该像素值距离摄像机的距离来计算出这个距离下的像素在产生雾效的时候的最终结果。
  • 像素着色器会遍历所有像素并运行计算。
  • 像素着色器并不一定会改变所有像素,例如我们可以设置在遍历过程中,当前像素如果是红色的话,就将其更改为绿色,这也是为什么我们有遮罩图像
  • 像素着色器使用像素着色器语言进行编写,着色器语言因平台而异,不同平台使用不同的着色器语言编码,在DirectX中使用(也是在UE4中使用)高级着色器语言(High Level Shader Language,HLSL)。通过在材质编辑器界面点击“窗口(Window)->着色器代码(Shader Code)->HLSL代码(HLSL Code)”即可浏览该材质实际的着色器代码内容。
    • 通常情况下,编写好的着色器代码会存在很多未定义的变量,例如要使用哪些纹理、定于光源如何与纹理互相影响、反射有多强烈,诸如此类,这些未定义的变量需要从外部输入,在外部指定好变量需要的内容之后,最终会在模型上渲染出来。这样的过程可以概述为:编写着色器代码->指定代码需要用到的外部资源->渲染到模型。
    • UE4的方法会更为复杂,因为它的设置更多,意味着自由度更大。首先也是编写着色器代码,也同样存在未定义变量,然后这些代码会被编译成为材质编辑器里面的节点或表达式,通过对节点和表达式进行赋值,这些节点和表达式又会组合形成一个更加复杂的着色器,通过这种层层叠加组合,最后才渲染到模型上。
    • 自从UE4.17以后,我们可以直接在项目里面创建着色器代码以实现我们自定义的效果。
  • 每个材质都会有自己的模板,每个模板都会有不同的输入需求,这也是为什么在材质编辑器里面最终节点上会有部分输入是激活状态,部分输入是灰色的未激活状态,激活状态就是当前着色器代码模板需要输入的内容。
  • 每个材质并不只会为自己生成一个着色器,而是会生成很多个,因为材质需要根据使用情况来生成不同的着色器,“使用情况”指的是在材质编辑器的细节面板里“用途(Usage)”分类里勾选的属性,一个属性(例如“与静态照明一起使用(Used With Static lighting)”、“与网格体粒子一起使用(Used With Mesh Particles)”)都会生成一个对应的着色器,之所以需要这样分类去勾选实际的用途,是因为如果我们默认让单一的着色器能够对应所有的情况,那么我们会生成一个特别复杂的着色器,会在一些不需要的地方损耗性能。
  • UE4中材质使用的是基于物理的渲染(Physically Based Rendering,PBR),PBR使用高光、金属色和粗糙度作为输入,它被用来计算环境中几乎所有的着色。
    • PBR属于同一着色,我们把它称之为统一着色因为所有图像——所有模型和材质——在地层中都建立在相同的PBR着色器系统上,这样做是为了获得最佳效率,因为如果我们知道我们预期的是什么样子、知道我们底层使用的是统一一套着色器,那我们就能够针对该着色器进行优化、设计。
    • G缓存包含了渲染一帧图片所需要的大部分信息,但不是全部信息,借助PBR提供的信息,结合G缓冲就能实现更好的渲染效果。
  • 着色模型(Shading Model,可在材质编辑器细节面板中材质分类里看到该属性)是由G缓冲生成的纹理,是一系列遮罩图像,可以识别哪些像素使用PBR之外的其他着色模型,这些(使用PBR之外的着色模型的像素)会采用另外一种渲染路径。
  • 现在我们已经拥有了渲染材质所需要的全部信息,但是还缺少材质必须的一部分——反射。
  • 材质或着色器能查看的纹理采样器拥有最大数量上限,通常最大值为16个,并且只有13个可用,但是你可以使用共享采样器(Shared Samples),这能使你使用多达128张不同的纹理,仅限于DX11或DX12。在材质编辑器界面下方的位置可以看到当前材质使用的纹理采样器个数,例如是“Texture Samplers: 2/16”样式的字段,说明该材质里最大允许16个纹理采样器,已使用了2个,当材质为空材质时候也会有采样器被占用的情况,是因为存在一些间接纹理(如光照纹理、阴影纹理等),所以该字段显示的采样器占用数不一定等于该材质正在使用的采样器数。
  • 材质编辑器界面下方的字段“Base pase shader:XXX instructions”,说明了该材质有XXX(整数值)条指令,该指令的数量通常在100到200次之间属于正常范围,高于这个范围的值属于性能损耗特别大的材质,需要对该材质进行优化。

反射(Reflection)

  • 实时反射非常难以实现,这可能是除光照外最难实现的效果之一,因为每出现一次反射都需要重新渲染整个场景,因此UE4使用三种不同的反射系统,彼此互相协同,将它们混合在一起使用来解决面临的挑战和难题,多数时候你看到的效果至少使用了三种(2D/立体场景捕获不算在内)中的两种作为组合。这三种系统是按顺序执行的,最先执行的系统优先级最低

    反射捕获(Reflection Captures)

    • 反射捕获会在一个特定位置(一般是反射捕获Actor所在的位置)捕获一张静态立方体贴图,这是预先计算出来的,这样的反射本质上只是一张混合到材质上的贴图,结果就是它非常快速,但不太精确,并且是只在固定范围内存在的局部效果。
    • 当摄像机位置和反射捕获Actor所在位置重合的时候,反射才是最精确的,当摄像机位置不再和反射捕获Actor重合的时候,反射会依然存在,但是会出现位置偏差。
    • 反射捕获Actor会在你打开关卡时重新捕获,点击“构建(Build)->构建反射捕获(Build Reflection Captures)”也能够进行捕获。运行游戏的时候它也会在加载关卡时更新捕获,但是当你打包游戏之后,反射捕获Actor实际上会将纹理烘焙进游戏,而不再会在关卡开始时更新捕获。

    平面反射(Planar Reflections)

    • 类似于反射捕获,都是从给定位置捕获内容,但是平面反射仅仅用平面捕获内容,因此反射仅限于在那个平面上(同样为局部效果)。
    • 平面反射在某些设置下损耗很大,但非常适合需要精确反射效果的平面,但是也只适合平滑表面的反射。

    屏幕空间反射(Screen Space Reflections,SSR)

    • 默认的反射系统。
    • 可在后期处理体积的“细节面板(Details)->渲染特性(Rendering Features)->屏幕空间反射(Screen Space Reflection)->强度(Intensity)”设置里更改反射效果的强弱,将该属性设置为0即可关闭SSR。
  • 当一个项目未经过烘焙时,反射捕获会在关卡加载时进行捕获,因此当你在关卡中添加过多反射捕获可能会导致加载变慢。

  • 当反射捕获发生重叠的时候,项目的性能损耗会变大,因为像素着色器会一遍遍地进行重复计算。

  • 在关卡中可以放置一个非常大的大型反射捕获Actor,再在一些需要反射程度高或者需要精确反射的表面附近放置小型的反射捕获Actor,使用这种方法来实现较好质量的反射效果。

  • 除非必要否则不要使用平面反射。

  • 如果硬件性能有限,请关闭三种反射系统中性能损耗最大的屏幕空间反射。

  • 如何硬件性能允许,可以将屏幕空间反射质量提升到标准之上以此来减少噪点。

  • 除了这三种反射系统外,还可以使用“天空光照(Sky Light)”提供的低成本备用反射捕获方案。天空光照根据“天空距离阈值(Sky Distance Threshold)”,裁剪掉该属性指定距离及距离内从而形成一个裁剪面,然后捕获场景剩余内容从而为整个游戏场景捕获一张立方体贴图,游戏场景里任何附近没有反射捕获Actor的对象都会转而使用天空光照反射,这对开发大型户外场景很理想,因为我们不需要在场景中到处放置反射捕获Actor。

光照和阴影(Lighting And Shadows)

  • 和反射一样,光照和阴影在实时渲染中也很难计算,计算光照和阴影需要大量硬件性能,并且计算速度很慢,因此我们会将光照的部分计算量分流到预计算或预渲染阶段,也就是我们常说的静态光照。静态光照是指所有预先计算而非实时渲染的光照。值得注意的是,虽然在讨论过程中经常讲静态光照和动态光照放到一起讲,但是从渲染流程上来看静态光照和动态并不是在同一时间进行计算的,而是静态光照会在第0步,也就是预计算阶段,甚至是在渲染之前

    静态光照和静态阴影

    • 流程的优缺点
      • 静态光照会在编辑器中进行预计算,并将大部分结果存储在光照贴图中,它在性能方面非常快速,但会增加内存占用量。
      • 预先计算光照需要花费很长时间,每当模型有变化时,都需要重新渲染光照。
      • 模型还必须有光照贴图UV,关于如何在UE4编辑器里生成光照贴图UV的知识可以在这里的第三点查询到。
    • 质量的优缺点
      • 静态光照可以处理辐射和全局光照。你可以获得真实且高质量的软阴影。
      • 可以获得比实时光照更好的质量,拥有更丰富更精确的效果。
      • 我们看到的画面质量实际上取决于光照贴图分辨率和UV布局,因为光照被烘焙进了纹理,是纹理大小和UV布局的质量决定了画面质量实际有多好。
      • 由于UV布局的关系,光照中还可能出现接缝。
      • 光照贴图分辨率会有一个上限(4096,如果你希望的话也可以是8192),因此巨型模型会缺乏足够的光照贴图UV空间,空间不足会导致光照质量变低,只能采用更低分辨率的光照贴图。
      • 一旦光照贴图计算完毕,我们就无法改变光照和阴影,除非我们重新计算整个模型。

    光照贴图(Lightmaps)

    • 光照贴图本质上就是一张纹理,是一张烘焙有光照和阴影信息的图片,然后纹理会和底色相乘,因此它看起来就像是会被照亮一样,但实际上没有。
    • 模型需要有UV光照贴图坐标才能使用光照贴图。
    • 在UE4中,光照贴图在光照重建过程中由称为Lightmass的系统流程生成。
    • 引擎里实际上是把许多光照贴图打包到一起,并不会单独创建图像。

    Lightmass

    • Lightmass是一个独立的应用,用于处理光源渲染和光照贴图烘焙,是编辑器之外的渲染器。
    • Lightmass支持网络分布式渲染。
    • 烘焙质量取决于光源构建质量以及Lightmass各个选项级别。
    • Lightmass可以有一个或多个Lightmass重要体积(Lightmass Importance Volume),任何位于该体积内部的东西都可 以获得更高质量的光照。
    • 放置Lightmass重要体积可以让引擎在计算光照的时候着重计算体积内的东西,而不需要着重计算体积外的东西,如果一个场景里面没有放置Lightmass重要体积,引擎会在整个场景范围内进行无差别光照计算,这样会提升性能损耗。

    间接光照缓存(Indirect Lighting Cache,ILC,官网说这个方法已经被体积光照贴图取代了)

    • ILC用来解决动态模型上的预计算光照。
    • 通过启用“显示(Show)->可视化(Visualize)->体积光照贴图(Volumetric Lightmaps)”来显示场景中的光照缓存。
    • UE4会在动态对象所在的位置,查找最接近该对象的光照缓存(即体积光照贴图可视化中的球形体),查询它的亮度,然后将其与动态对象插值混合,以此实现光照在动态的对象上的间接反射。
    • (在UE4.27中该属性是默认关闭的)所有可移动对象都有被称为“间接光照缓存质量(Indirect Lighting Cache Quality)”的属性,默认“ILCQ Point”会选取动态物体最近的一个点查询颜色,“ILCQ Volume”会选取附近555个点并从所有这些点中计算出颜色,“ILCQ Off”可以关闭间接光照缓存以提升性能。
    • 由于ILC大部分时候仅能计算平面上的光照缓存,在一间大房子里接近房顶的部分就不会计算缓存,所以可以使用Lightmass角色间接细节体积(Lightmass Character Indirect Detail Volume)覆盖需要生成光照缓存的区域,来迫使这些区域强制生成间接光照缓存。随笔作者注:该功能应该一并与间接光照缓存被体积光照贴图取代了,因为体积光照贴图现在可以在全部对象周围(而不仅仅是地面位置)生成光照缓存。
    • 可以在“世界设置(World Settings)->Lightmass->Lightmass设置(Lightmass Settings)”里设置这些间接光照缓存的密度,从而获取到更加平滑的动态间接光照效果。体积光照贴图的密度设置也是在这个地方

    • 静态光照总会以完全相同的速度渲染(指烘焙后的),无论你有一个光源,没有光源,还是有五万个灯源,这些都没有区别,你都会获得完全相同的性能。
    • 光照贴图分辨率会影响内存和文件大小,而不是帧率,这个和纹理大小是一样的。
    • 烘焙时间会随着光照贴图分辨率、模型和光源数量以及质量设置的提升而增加。
    • 衰减半径很大或者是源半径很大的光源会降低重新构建的时间。

    动态光照和动态阴影

    当我们在讨论动态光照和实时光照的时候,它们其实是同一个概念。
    • 流程的优缺点
      • 动态光照借助G缓冲实现实时渲染。我们使用来自G缓冲的图像来计算如何将光照混合上去。
      • 因为光照是全动态的,因此不需要像静态光照那样对模型或对象进行限制,只需要添加一个动态光源即可产生效果。
      • 然而阴影对于性能的影响非常大。
      • 渲染动态阴影有很多种方法,这意味着需要时间和实践才能找到合适的方法和混合方案。
    • 质量的优缺点
      • 阴影的性能损耗很大,通常需要降低渲染质量来补偿动态阴影巨大的性能损耗。
      • 动态光照不会对大部分内容产生辐射或全局光照,只能得到相当直接的基本光照和阴影。
      • 动态光照很难生成软阴影,即使生成了动态软阴影,该阴影也往往不急静态软阴影那样真实。
      • 动态光照通常比静态光照看上去更清晰更具“现场感”。感觉被照射物体更像是处在场景中,更加鲜活.
      • 动态阴影对模型的尺寸没有要求(静态阴影有要求是因为存在光照贴图大小的限制)。

    阴影

    • 阴影极度消耗性能,关闭一些光源的阴影投射会有很大的帮助。
    • 动态阴影主要有四种类型:

    常规动态阴影(Regular Dynamic Shadows)

    • 最常用并且最重要的一种动态阴影类型。
    • 常规动态阴影源(Regular Dynamic Shadows Source)是指那些移动性被设置为可移动(Movable)、并且正在投射动态阴影的光源,这些光源投射地阴影被称为“常规动态阴影”。当这类光源被移动了的时候,由该光源投射的所有阴影都会实时地移动。

    逐个对象阴影(Per Object Shadows)或称为固定光源阴影(Stationary Light Shadows)

    • 那些移动性被设置为固定(Stationary)的光源所投射的阴影被称为“逐个对象阴影”或“固定光源阴影”。
    • 固定阴影会混合使用完全依赖光照贴图的静态光源以及完全动态光源,两者兼而有之。

    级联阴影贴图(Cascaded Shadow Maps,CSM)

    • 级联阴影贴图会根据距离(指阴影到摄像机的距离)渲染不同的动态阴影
    • 因为CSM的特性(阴影随着与摄像机的距离增加而逐级质量下降),因此不适用于渲染大型室外场景,因为远距离的阴影已经消退到不存在了。

    距离场阴影(Distance Field Shadows)

    • 对于级联阴影贴图来说,因为其特性所以会造成不能够渲染远距离模型的阴影,为了弥补这个不足,我们需要一个新方案来实现能够远距离渲染阴影的同时还不会造成过多性能损耗的方法,这个方法就是距离场阴影。

    • 下面为引擎里存在但是不常使用的阴影类型

      插图阴影(Inset Shadow)

      • 和“逐对象阴影”类似,但是提供了更高的阴影分辨率,因此对于角色对象来说该阴影是默认启用的。即便附近没有合适的光源,启用了该阴影的对象也可以投射出高分辨率的阴影。
      • 可以通过勾选模型的“细节面板(Details)->光照(Lighting)->动态插图阴影(Dynamic Inset Shadow)”来启用该阴影。

      接触阴影(Contact Shadows)

      • 能在细小物体下投射效果不错的接触阴影。例如墙上的盆栽背后细微的阴影。
        -可以勾选“细节面板(Details)->光照(Lighting)->接触阴影(Contact Shadow)”来启用该阴影。

      胶囊阴影(Capsule Shadows)

      • 非常简单且损耗很低,它是用来渲染模型(主要是指骨骼网格体模型)下方阴影的系统。
      • 首先需要对“骨骼网格体(Skeletal Mesh)”赋值所需要的碰撞胶囊,可以在骨骼网格体下的“光照(Lighting)->阴影物理资产(Shadow Physics Asset)”设置该骨骼网格体所需要使用的碰撞胶囊(不需要完全准确,甚至只有一个椭圆的碰撞胶囊资产也是允许的,由此也可实现一个风格化的阴影),然后在“骨骼网格体组件(Skeletal Mesh Component)->光照(Lighting)”分类下勾选“胶囊直接阴影(Capsule Direct Shadow)”和“胶囊间接阴影(Capsule Direct Shadow)”即可启用该类型的阴影。

    渲染阴影

    • 我们目前知道了多种渲染阴影的方法,现在需要使用像素着色器来计算和应用阴影。为了简单起见,接下来的动态光源会以点光源为主要关注的对象。
    • 动态光源像球体一样渲染(这也是为什么引擎里面的光源都会有一个淡蓝色球形的范围),原理就像遮罩,球体内的任何像素都会进行经过像素着色器着色运算来混合光照。
    • 正因为动态光源会像球体一样渲染球体内的所有像素,也就是说如果两个动态光源的球体范围互相重叠了,那么重叠部分会经过重复的计算,从而造成性能损失。可以通过开启“光照(Lit)->优化视图模式(Optimization Viewmodes)->光源复杂度(Light Complexity)”来可视化每个光源的渲染范围,单个光源的渲染范围会呈现绿色,如果存在重叠则重叠部分颜色会依次以红色、紫色、白色的颜色过度,复杂度也依次增加,也说明该部分的重叠光源性能损耗越大(因为需要重复计算的次数变多了)。
    • 渲染阴影需要知道四样信息:从摄像机到几何体的距离(从摄像机生成的深度图)、摄像机的位置、光源的位置、几何体到光源的位置(从光源位置生成的360度深度图)。
    • 由上述4条信息我们可以生成一张阴影贴图,阴影贴图就是在特定光源的作用下在特定位置能够看到的阴影。
    • 阴影渲染的大致过程是:首先我们知道了某个位置有光源,且知道光源的颜色,光源的范围等信息,然后通过光源生成的深度图,知道我们应该照亮哪些模型不应该照亮哪些模型,然后再混合世界法线贴图生成更加细致的阴影,然后再混合之前生成的阴影贴图,为每个被照亮的模型生成了对应的阴影,从而实现了阴影的渲染。
    • 动态光照本身损耗相对较低,特别是在延迟渲染器和如今的正向渲染器中,光源本身并不是问题和损耗所在,阴影才是。损耗源于像素的着色运算,像素越多,运算速度越慢,这点和反射、材质的性能损耗规律是一样的。我们无法从直接的视觉上观察到一个光源所占用的像素是多是少,光源所占像素的多少是由其属性“衰减半径(Attenuation Radius)”显示的淡蓝色线条组成的范围决定的。
    • 因为动态光照中阴影造成的性能损耗很大,因此如果你不是必须使用的话,关闭阴影有助于提升性能。
    • 几何体的面数也会印象阴影的质量及性能损耗,几何体面数越多,计算其阴影就会造成更多的性能损耗,因此降低多边形面数有助于提升性能,如果必须使用高面数的多边形,可以使用距离场阴影来代替常规的动态阴影从而减少性能损耗。
    • 因为阴影是由动态光源生成的,因此可以通过关闭光源来实现减少性能损耗的要求,特别是光源对象都会在“细节面板(Details)->性能(Performance)”分类里有“最大绘制距离(Max Draw Distance)”属性值和“最大距离衰减范围(Max Distance Fade Range)”属性值,通过设置这两个属性可以让光源对象在一定距离外就不再产生光源,也不再产生阴影。
    • 根据我自己的测试来看,就算模型因为遮挡而被剔除的时候,其阴影依然会生成。因此对于某个房间里面模型来说,即使玩家站在房间外,而且视角和房间内之间存在一个门模型做遮挡,这样会导致房间内的模型被剔除,但是光照不会。最好的办法就是利用脚本(蓝图或C++都行)控制房间内的动态光源的开关,可以减少不必要的性能损耗。

混合使用静态光照和动态光照

  • 静态光照可以用于微弱和远距离的光照。
  • 静态光照还可以用于渲染摄像机附近的间接光照。
  • 动态光照可以同来突出着色和阴影。并且在前者效果上生成一张具有交互效果的图层。
  • 如果需要尽可能高的性能,就只使用静态光照。
  • 如果想随时修改光照效果,就只使用动态光照。

雾和半透明

雾(Fog)

  • UE4中有大气距离雾(Atmospheric Distance Fog)和指数距离雾(Exponentional Distance Fog)两种距离雾,以及一种局部体积雾(Local Volumetric Fog)。大气距离雾和指数距离雾会随着环境而改变颜色。
  • 距离雾(Distance Fog)意味着雾会随着距离(指距离摄像机的远近)消退,一般来说是随着距离的增长而消退。
  • 距离雾同时也是一种高度雾(Height Fog),和距离雾差不多,高度雾指的是随着高度消退的雾。
  • 所有这些雾效都是基于像素着色器来计算渲染的。
  • 雾效本质上是借助深度图来实现。因为知道深度,就能知道一切对象离摄像机有多远,知道雾的颜色,将该颜色和深度图混合,就能得到实现了雾的深度图,最后再混合之前已经渲染好了的图像,就能获得实现了雾效的图像。

透明度(Transparency)

  • 延迟渲染器在渲染透明度时会遇到严重的困难,因为延迟渲染只有G缓冲可以使用,G缓冲缺少足够的信息来正确渲染透明度。
  • 在前向渲染中渲染透明效果会容易得多。
  • 为了获得尽可能高的半透明质量,目前引擎的方法是在前向渲染中完成透明度渲染,然后和延迟渲染结果合并,但是这样会有额外损耗,因为现在需要运行两个流程然后将它们的结果合并到一起,但是好处就是避免了延迟渲染带来的缺少信息的问题。
  • 在材质编辑器界面的“细节面板(Details)->半透明(Translucency)->照明模式(Lighting Mode)”里列举了许多可供半透明材质使用的照明模式解决方案,引擎需要克服系统存在的弱点,因此需要为每种特殊情况制定不同的方案。体积无方向(Volumetric NonDirectional)、体积定向(Volumetric Directional)的“体积”表示它会划分表面和网格体(即在渲染模型时增加额外细分),然后用来和顶层的光照混合;体积逐个顶点无方向(Volumetric PerVertex NonDirectional)、体积逐个顶点定向(Volumetric PerVertext Directional)也一样,只是基于顶点,“无方向”总是比“定向”损耗更低,“定向”在半透明效果上更好一些,“无方向”基本上只是在顶层添加环境光照。表面正向着色(Surface ForwardShading)则会在这个材质上面开启表面正向着色。
  • 渲染透明度效果的时候首先会在G缓冲中标识出哪些像素稍后会变成透明。
  • 当以最佳质量渲染透明度时就像素着色器的运算次数而言损耗很大,所以可以使用前向渲染其实现。
  • 如果有许多图层覆盖了相同像素,并且有大量像素被覆盖的时候,透明度造成的损耗会特别大。
  • 除了像素着色器本身的损耗以外,渲染排序也会加重损耗,例如透明材质的重叠,会导致被重叠像素反复进行计算,从而加重损耗,重叠的层数越多,损耗就越大。
  • 在实现半透明效果的时候,材质的混合模式(Blend Mode)能使用蒙版(Masked)的话就尽量使用蒙版,因为该混合模式性能会好很多,如果必须使用半透明的(Translucent)的话,其着色模型(Shading Model)能选无光照(Unlit)就尽量使用无光照模式而不是默认光照(Default Lit)模式,因为无光照的性能也比默认光照好一些。我们需要去“仿造”半透明的效果,例如将材质颜色替换成对象的背景颜色,从而“仿造出”半透明的效果,因为这样即可以实现半透明的效果又不会造成太多的性能损耗。
  • 如果在实现半透明效果时候确定必须使用半透明的混合模式以及默认光照的渲染模型,那么就需要去确定在半透明(Translucency)分类下的属性设置正确,如果需要最佳性能,就在该分类下将光照模式(Lighting Mode)设置成表面正向着色(Surface ForwardShading)。

后期处理(Post Processing)

  • 后期处理就是在渲染流程末端应用的视觉特效,所以被称为“后期”处理。
  • 后期处理很大程度上依赖于像素着色器,并且也是通过合成实现,它通过再度使用G缓冲来计算各种效果。
  • 常见的后期处理特效有:泛光(Light Bloom)、景深/其他形式的模糊(Depth of Field/Blurring)、镜头眩光(Lensflares)、光束(Light Shafts)、渐晕图(vignette)、色调映射/颜色校正(Tonemapping/Color Correction)、曝光(Exposure)和运动模糊(Motion Blur)。

泛光(Bloom)

  • 引擎会查询当前画面里所有像素的亮度,然后过滤掉所有暗于特定颜色的像素(所谓过滤就是用黑色将其替代),增加剩下像素的亮度或对比度,然后以某种方式模糊这张图像,例如缩小图像再放大到原来尺寸,然后你可选地使用一张带有灰尘地遮罩图片或类似镜片灰尘之类的纹理,与模糊后地图像混合,就能得到该灰尘遮罩效果的泛光,最后混合到渲染完成的图像上,就能实现泛光的后期效果。

景深(Depth Of Field)

  • 景深可以通过将深度图和以某种方式模糊后的图像混合的方法来生成,以此生成的图像会在远距离的位置发生模糊,对象离摄像机越远,该对象则越模糊。

颜色校正(Color Correction)

  • 引擎颜色校正是用一种被称为“查找表(Look Up Table, LUT)”的方式将数据记录在一条彩色数据查找表(该表可以记录4096种不同的颜色偏移)里,然后应用到UE4中去查找那些匹配的像素,最后对这些匹配的像素进行对应的处理操作(例如提升亮度、提升红色、减少绿色等操作)。

着色器(Shader)概念

  • 渲染过程中会存在着一系列需要反复计算的过程,如果这些过程在CPU中进行处理的话效率会非常低下,“着色器(Shader)”就是被设计用来进行处理这些过程的软件和硬件,着色器本质上是在GPU特定区域进行重复计算,非常的高效,是实时渲染的核心,对于渲染方式和性能损耗有非常非常大的影响。

G缓冲区(GBuffer)概念

  • GBuffer是为每帧准备的一组图像,他们包含了渲染管线后期所需要用到的所有信息,例如渲染雾效时候GBuffer用于保存场景的深度。
  • 延迟渲染器需要使用到GBuffer。
  • 前向渲染器一般来说不需要GBuffer,但是也可以使用,只是会让渲染变得复杂。
  • 生成GBuffer的过程完全是在后台进行的。
  • GBuffer会创建渲染过程后期阶段使用到的所有信息(即上文提到的“一组图像”)。

混合预计算渲染和实时计算渲染的解决方案。

  • 实时计算渲染非常复杂,要求也非常高。
  • 因此UE4的渲染方案通常是将预计算部分和实时计算部分结合起来。
  • 即UE4的渲染分为预计算渲染(例如光照贴图等)和实时计算渲染(动态光源实现的动态阴影等)。
  • 本随笔作者觉得此概念隐含一个需要掌握的知识,就是当我们谈到“实时渲染”的时候,其实谈到的是“实时渲染”加上“预计算渲染”,而不单单只是指代“实时渲染”。或者可以说,“实时渲染”包含了“预计算渲染”和“即时渲染”。
  • 所谓“混合”,即指UE4引擎本身提供许多问题的解决方案,而我们需要通过选择合适的方案来“混合”使用,例如对于阴影,我们可以组合使用级联阴影贴图(近距离对象的阴影)和距离场阴影(远距离及大量对象的阴影)来实现对象阴影的功能。
  • 使用混合方案而不是单一方案,是因为混合方案往往能够提供更加高效的解决方案。

渲染质量的可延展性(Scalability)

  • 可随时随地地调整渲染质量(画面质量),类似于很多游戏都提供的画质选项。
  • 可延展性可让游戏内容移植到不同的设备的。
  • 可延展性已经融入到了引擎和渲染工具的每个部分里面了。
  • 性能对于实时渲染来说非常重要,要想维持高性能(并且不过多降低渲染质量),既要掌握可延展性设置,又要正确地去处理应用内容,所以需要知道怎么去用“r.”命令;如何去用可延展系统的自动设置;如何合理地去设置游戏内容以保证应用内容已经被充分优化。

渲染相关的命令

  • r.shoadowquality Value: 控制阴影质量,Value值为0时则关闭阴影。
  • r.Streaming.PoolSize Value: 设置纹理流送池的大小,Value值的单位为MB。
  • r.SSR.Quality Value:设置屏幕空间反射质量,默认为3。

渲染的两种方法:“延迟渲染(Deferred Rendering)”和“前向渲染(Forward Rendering)”

  • 不仅仅是在UE4引擎里面,几乎所有引擎、软件,使用的都是“延迟渲染”或“前向渲染”。
  • UE4引擎默认使用的是延迟渲染,但是也可以手动设置为前向渲染。大多数情况下都会选择延迟渲染。

延迟渲染

  • 能够让高要求的设备拥有稳定的性能(适用于当前市面上的许多游戏大作)。
  • 支持更多渲染功能。
  • 缺点在于抗锯齿,只能使用临时抗锯齿(Temporal Anti-Aliasing, TAA)。
  • 着色(Shading)发生在延迟环节中,而且渲染几何体模型的步骤是在渲染光照的步骤之前完成的,这些渲染阶段并不是同时完成,因此该渲染方法被称为“延迟渲染”。
  • 擅长渲染动态光照,擅长提供稳定、可预测的高质量效果,在涉及禁用部分渲染功能的时候会更加灵活。
  • 不擅长表面属性相关的功能,不擅长渲染半透明表面。

前向渲染

  • “前向渲染”能够让简单应用或者低端设备带来更流畅的性能(适用于手机游戏、VR游戏、VR应用、VR移动应用等)。
  • 仅支持有限的渲染功能。
  • 但是能提供更好的抗锯齿,因为可以使用多重采样抗锯齿(Multi Sampling Anti-Aliasing,MSAA)。
  • 着色、几何体及材质是在同一环节进行计算的。
  • 擅长渲染半透明表面。
  • 不擅长混合各种功能。
  • 前向渲染器会在基础通道过程执行比延迟渲染器要多得多的步骤,前者会使用更多基于合成的方案。

渲染指标:FPS、Game、Draw和GPU

  • FPS(Frame Per Second):帧率,即代表着渲染一帧画面所需要的时间,单位通常为“毫秒(ms)”。
  • Game:游戏线程(CPU渲染的进程),通常代表着CPU进行渲染所需的时间。该线程通常用于计算位置(Position)、旋转(Rotation)或变换(Transform)相关的计算,还有动画(Animation)、物理(Physics)、碰撞(Collision)、AI、生成/销毁(Spawning/Destroying)等。
  • GPU:渲染线程,通常代表GPU进行渲染所需的时间。该线程通常用于计算具体的渲染,如光照(Lighting)、模型渲染(Rendering of models)、反射(Reflections)、着色器(Shaders)等。
  • Draw:绘制线程,该线程大部分时候是由CPU负责,GPU也会处理小部分。
  • Game(CPU)渲染和GPU渲染是同时发生的。

四种常见的性能问题

  1. 半透明材质:屏幕当中半透明材质每叠加一层,就要多渲染一次半透明效果,就需要重新计算像素,因为像素是叠加在其他半透明层的上面。半透明像素在屏幕中的占比越多(例如距离半透明材质的物体越近),需要计算的像素越多,性能损耗越大。
  2. 像素着色器:越是复杂的材质,对性能的损耗越高,判断材质是否复杂可通过判断该材质所包含的指令(Instructions)数量来确定,指令数量越大的材质越复杂。像素在屏幕中的占比也会影响性能(因为占比越大的话,需要计算的像素的数量变多,计算量也会变大)。
  3. 绘制调用(Draw Call,简称DC):引擎渲染场景的时候是逐对象渲染,每渲染一个对象就是一次绘制调用,对于模型里拥有多个材质的单例模型来说,每个材质也是一次绘制调用。引擎是以绘制调用为单位进行渲染的。
  4. 动态阴影:动态阴影会随着场景中多边形的数量增加而产生更多的开销。一般来说,如果你打算在场景中使用大量动态阴影,那么就应该使用低面数的模型,如果要使用高面数的模型,那么就应该使用静态光照或不生成动态阴影。
posted @ 2022-06-14 10:25  U_N_Owen  阅读(2896)  评论(2编辑  收藏  举报