文章来源: 博客园
原文链接: UE4学习笔记:实时渲染原理 | U_N_Owen原文写得很好,但是主题格式我不太喜欢,所以我对文章的格式进行了修整,方便自己日后阅读
渲染中发生的过程
几何体渲染
首先需要通过预通道(Prepass)或前期深度通道(Early Z Pass)来做一个深度检测。
我们知道对象的位置以及需要渲染的对象,但是不清楚这些对象的 渲染顺序 ,通过 深度检测 ,当一个小型模型(如一个茶壶)“遮挡”到一个大型模型(如一面超大的墙,但是因为小型模型没有 完全 遮挡大型模型所以不会执行遮挡剔除操作)时,我们可以确立墙的模型只需要渲染除茶壶模型遮挡的部分以外的像素即可,就像一面墙上有一个茶壶的剪影,渲染部分就是除 “剪影” 以外的像素。如果像这样排列的模型很多的话,就会一个个计算每个模型在墙面上的 “剪影” 并叠加起来,最终确立的 “剪影” 就是不需要渲染的像素。
接下来就是实际的几何体渲染。
几何体渲染会根据绘制调用逐个渲染。
不能说一个模型算做一个绘制调用,一个模型上的一个材质也算是一个绘制调用。绘制调用就是 一组 拥有相同属性的多边形,很明显如果一个模型用到两种材质,用到两种材质的部分肯定不属于拥有相同属性,因此会算作两次绘制调用(因此不能说一个模型算作一个绘制调用,模型上不同材质的部分会被当成两个对象来渲染)。
使用引擎命令 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)
主要是在几何体渲染过程中使用到。
顶点着色器主要是负责三件事:
- 将顶点的本地坐标转换为世界坐标。
- 处理平滑过渡、硬边缘、柔化边缘以及顶点的颜色。
- 额外偏移。例如材质节点中的世界坐标偏移(World Position Offset)就可以实现偏移的功能,世界坐标偏移就是通过顶点着色器来偏移并移动网格体。
顶点着色器可以用来偏移布料的世界坐标,布料是由顶点着色器驱动的。
水面的位移、波浪的位移也是通过顶点着色器来驱动的。
植被被风吹的动画也是由顶点着色器来驱动的。如果如果风吹草动的效果是由蓝图动画来驱动,那意味着我们需要设置骨骼网格体,也就意味着 骨骼位置需要随时更新 ,这样就需要 CPU 来参与计算,如果使用顶点着色器则把计算的负担分摊给了 GPU。
需要注意的是,用顶点着色器实现的位移效果仅仅只是渲染的效果,实际上的模型并没有跟着移动,也就是说碰撞和物理效果并不会产生变化。