前言

在上一篇中我们详细介绍了实时渲染中计算渲染对象的位置信息的过程,在这一篇中,我们将进入实时渲染的重要环节 —— 着色器和绘制调用

请检查以下概念是否了解,可以回顾上一篇的内容

  • 渲染过程是如何把整个世界的对象识别出来的
  • 渲染过程是如何计算视口/屏幕上应该显示哪些对象的

关键词

  • Prepass / Early Z pass 预通道/前期深度Z通道
  • Vertex ShaderS 顶点着色器
  • Pixel ShaderS 像素着色器
  • DrawcallS 绘制调用

看完本篇内容你将对以下概念心中有数:

  • 什么是 Shaders/着色器,怎么理解这个概念
  • 考虑顶点着色器的性能优化
  • 考虑像素着色器的性能优化


01 Shaders 着色器

着色器是渲染领域的专有技术,总之它是一种针对渲染的代码段,告诉GPU怎么去绘制每一个像素。

着色器目前主流的有三种语言:

  1. 基于 OpenGL 的 OpenGL Shading Language ,简称 GLSL
  2. 基于 DirectX 的 High Level Shading Language , 简称HLSL
  3. NVIDIA 公司的 C for Graphic ,简称 Cg

着色器根据在整个渲染管线中位置和功能不同,又大体区分了几块。包括

  1. 顶点着色器,用于修改顶点位置;
  2. 像素着色器,用于计算单个像素的颜色;
  3. 几何着色器,基于第四代显卡更为先进的计算位置。

我们将在下文展开顶点着色器和像素着色器。

在UE中,我们在材质编辑中的修改很大部分都是对 Shader 代码段的修改。


02 Prepass 预通道计算

在渲染过程中确定当前视口需要包含哪些对象后,我们还会遇到一个问题:渲染是逐对象进行的,但在一个像素点上,可能存在不同深度的对象,它们在这个像素点上都需要重新计算,而最终真正显示给用户的,只有最前面的一个,这样的重复计算是很浪费的。

这张图的中央两个人覆盖了一部分的汽车,也覆盖了更后面的建筑部分。按照我们之前将的渲染的处理逻辑,可视范围的对象列表会逐一渲染,它是”逐对象“渲染,这很重要。那么在渲染的过程中,让我们简化一下这张图的场景,一个背后的房子、马路中间的车、最前方的两个人。先渲染了后面的整个房子,然后渲染了整个车子,再然后是最前面人,他们之间是重叠的,中间有很多像素点在人物渲染前的那几次渲染都是没有必要的计算消耗。

预通道正是为了优化这一消耗的方案,它会计算出在深度上基于最终呈现需要渲染哪些像素点,渲染后面的像素点时会镂空掉被覆盖的部分,大大减少浪费。我们可以在 RenderDoc 监控中直观的看到每一步的计算。这样在后面的逐对象渲染计算过程中,就不会重复计算不涉及透明的像素点。


03 Vertex Shaders 顶点着色器

顶点着色器的目标是将对象的本地坐标转化为世界坐标,通过一系列的计算得出对象最终对应显示在哪些像素点上。

利用顶点着色器,可以处理边缘的柔化程度、颜色,处理出平滑过渡的效果;我们也会用顶点着色器来实现一些无干涉的位移,我的意思是,又顶点着色器处理的位移由于步骤在 cpu 计算之后,这个位移信息是 cpu 不知道的,这意味着不会影响前面步骤计算出的碰撞之类的效果,非常适合用来布料、水面、植被的物理波动效果。

针对顶点着色器的性能优化需要关注面数,

  • 优化面数,针对模型优化不同距离的 LOD
  • 越是面数多的模型,越要设计简单的顶点着色器
  • 在远距离上禁用掉顶点着色的相关动画

04 Pixel Shaders 像素着色器

像素着色器是整个渲染管线中至关重要的过程,在渲染的每一步计算中都会使用到,实现了包括材质系统、光照、雾、反射、后期处理、颜色矫正等等计算效果。

在 GPU 计算像素位的过程中,是按照 2x2 像素点为单位进行遍历的(参考上图的橙色块),对于每一个像素点来说最终只会呈现一个色块,这里就会遇到冲突的问题。比如图中的左侧的三角形通过计算需要占绿色位的三个点,有右上、右下、左下 三个2x2的单位被计算,右侧的三角形则占据包括绿色块的四个点,有右上和右下两个2x2的单位被计算,这里右上和右下的2x2被反复计算了,这就是过度着色的问题。如果很多面的着色点重叠,对于性能是很不必要的消耗,完全可以进行优化。

可以在引擎中进行检查,越接近红色的地方过度着色越严重。一个比较好的状况是,只有涉及半透明、动态光照等吃性能计算的物体到红色程度,全局没有超过深红色的情况,尽量减少红色占据画面太大。

优化点,

  1. 检查多边形的密集程度,按照目标和情况减少模型面数
  2. 远距离上减少模型面数,否则距离越远密度会不断增加
  3. 减少初始着色器的复杂程度,也就减少了过度着色开销 (对前向渲染有用,对延迟渲染影响不大)

05 Drawcalls 绘制调用

在 UE4 中,绘制调用是逐材质逐对象进行的,绘制调用是性能损耗的大头,想象有 1w 个对象材质,我们要达到60帧率,就需要每秒绘制 60w 次才能不掉帧 …

我们来看一下什么叫逐材质逐对象进行,
如下图有三根柱子,一共两种材质,白色的部分和红色部分,来算一下有几次 drawcall

  1. 第一张和最后一张是场景背景(这里我们先不在意它)
  2. 第二张是第三柱子的白色材质部分
  3. 第三张和第四张是第一柱子和第二柱子的白色部分
  4. 第五张是第三柱子的红色部分

三根柱子绘制的发生了 4次 drawcall,这个过程中每个材质每个对象都需要一次 drawcall。

从这个案例出发引出一个点 —— 对于渲染来说,切换的停顿才是性能损耗的关键。
这就是为什么引擎会逐个遍历引用材质这个”大”数据后,再遍历对象这个”小”数据,而不是逐个对象渲染一遍材质。对于 drawcall 的过程来说,就像拷贝文件一样,一个 1GB 的文件拷贝只需要几分钟,但是 100w 万个 1kb 的文件可能需要一整天。 —— drawcalls 是昂贵的。


我们通过指令 stat RHI 可以检查,
这个画面 draw primitive calls平均有 2k ~ 3k 次。
官方给出的建议是 2 ~ 3k 是合理的,超过 5k 有点高了,超过 1w 很可能会产生一些问题。如果是移动端或者 vr 标准就更严苛了,控制在 1k 以内才合理。

优化点,

  1. 剔除检查,减少不必要的对象渲染
  2. 检查使用多层渐进纹理,减少远距离模型面数
  3. 材质检查,近似材质是否可替换统一
  4. 合并模型,减少对象数 (这是一个平衡艺术)

06 总结

渲染性能的四大杀手:绘制调用次数、像素着色器、半透明、动态阴影。本篇文章重点描述了绘制调用和着色器的了解和性能关注点,总体来说,我们始终需要做的是

  1. 以目标帧率为导向,不是追求一味的高性能,要平衡呈现效果、性能和可维护性
  2. 减少渲染对象、渲染面数的浪费,采取的手段包括距离剔除、多层渐进纹理等
  3. 优化渲染对象、渲染面数,在效果和性能之间做平衡,采取的手段包括合并/拆分模型对象、纹理优化等
  4. 优化着色性能,包括减少着色器的计算复杂度、减少过度计算等