6.1 什么是渲染管线(渲染流水线)

  • 渲染管线或称渲染流水线实际上描述的就是存在在计算机内存中的数据如何渲染呈现在屏幕上的过程。 当下的大多数主流的设备终端都支持的一系列操作的总称,接下来我们将为各位分享详细的渲染流程,以及管线综述。

6.1.1 什么是SHADER?

  • Shader是着色器,是可编程渲染管线的一部分,有一定的着色器,顶点着色器(vertex Shader),片元着色器(Fragment Shader )等,我们可以通过Shader与GPU交流,控制渲染细节。主要分为可编程和可配置两类。

6.1.2 SHADER与Material的关系?

  • Shader与贴图等资源组合起来得到Material,材质好比商品,Shader是加工方法,而贴图是原材料

6.2 渲染管线的流程

6.2.1 渲染管线的三个主要阶段

  • 渲染管线就像工厂中的流水线,主要分为以下三个阶段
    • 应用阶段
    • 几何阶段
    • 光栅化阶段
  • 第一个阶段主要由CPU负责,而后两个阶段则由GPU负责

  • imag.png
    6.2.1 渲染管线大致流程划分
  • 应用阶段又可分为加载显存数据,加速算法,设置渲染状态,调用DRAWCALL。此部分由CPU负责,主要将应用软件里的数据整理调试,并最后打包发送给GPU。

  • 几何阶段可分为顶点变换(也就是我们之前所提到的MVP矩阵)裁剪,屏幕映射,这三个阶段,最终将渲染出来的图元存储在缓存(buffer)之中。此阶段对应的是曲面细分着色器和顶点着色器
  • 光栅化阶段则将上一阶段的数据进行三角形设置,遍历,传递给片元着色器。最后通过逐片元操作,进行一系列的测试,在将片元对应像素绘制到屏幕之上。
  • 由此便完成了基本的图像绘制流程,下面是这些流程的详细介绍与具体应用。

6.2.2 应用阶段

  • 应用阶段大致步骤
  • **准备场景数据
  • 场景物体数据
    • 物体的**变换数据,位置,旋转等
    • 物体的**网格数据,顶点位置,UV贴图等
  • 摄像机数据
    • 摄像机的**位置方向,远近裁剪平面
    • 正交透视FOV
    • **视口比例大小尺寸

  • 光源以及阴影数据
    • 光源**类型,位置方向,角度
    • 是否需要阴影,判断是否有**能够投射阴影的物体
    • 阴影参数,对应光源序号,**阴影强度,级联参数,
    • **深度偏移,近平面偏移等
    • 逐光源绘制阴影贴图:
      • 近平面偏移
      • 逐级联,计算当前**光源加级联对应的观察矩阵投影矩阵以及对应阴影贴图里的视口
      • 绘制到阴影贴图
  • 其他全局数据

  • 加速算法,颗粒度剔除
    • 碰撞检测
    • 加速算法
      • 可见光裁剪
      • 可见场景物体裁剪
        • 八叉树,BSP数,K-D树,BVH包围盒等
    • 遮挡剔除

  • 设置渲染状态,准备渲染参数
    • 绘制设置
      • 使用着色器
      • 合批方式
    • 绘制顺序(Render Queue)
      • 相对摄像机的距离
      • UICanvas等(Unity举例)
    • 渲染目标(RenderTarget)
      • 帧缓存(FrameBuffer)
      • 渲染纹理RenderTexture
    • 渲染模式(RenderMode)
      • 如ForwardBase等

6.2.3 DRAWCALL与OpenGL,DX等

  • 我们常说的OpenGL与DirectX是什么东西呢?
  • 这两种都是图形API,也就是图形接口,它是渲染硬件上的一层抽象,我们通过编写HLSL,GLSL/CG来调用这些接口,它们将向显卡硬件驱动发动相关指令,由硬件驱动翻译并让硬件执行,我们向硬件发出的命令被称为DRAWCALL。

  • image.png
    渲染操作的逻辑层次

  • 目前游戏中的着色器语言是针对多平台的,它会根据平台不同智能编译成对应的机器语言,并传递对应的信息,所以在游戏中经常会遇到“编译着色器”这个提示,比如黑神话中的这个开头,其实就是在将统一的着色器语言,翻译成对应API对应驱动程序版本的底层机器语言
  • image.png
    6.2.3 黑神话的着色器编译中~
  • 提个小问题,这部分的速度快慢主要依靠的指标是什么?

  • DRAWCALL
  • 这个步骤实际上就是CPU调用图像编程接口的过程,例如OpenGL中的glDrawElements命令,这也是造成性能瓶颈最大的原因所在。DrawCall中造成性能问题的根本不是GPU而是CPU,实际上,在渲染流程中,我们会在此构建一个缓冲区,它存储CPU发出的指令,由GPU读取,这样GPU快速的读取就不会因为CPU的缓慢而减速。

  • 我们要尽可能的优化渲染流程,因此DrawCall便是我们的一大优化目标,因为每次发送DrawCall指令,CPU都要向GPU发送大量信息,例如模型的顶点法线等参数,这就会大量消耗时间。
  • 怎么优化DrawCall?我们可以有很多种方式,这里主要讨论批处理方式(Batching)
  • Batching实际上可以理解为打包,而DrawCall可以理解为提交。

  • 顾名思义,这个方法就是让我们将很多的小DrawCall打包成为一个大的命令,来向GPU传输,以此降低批量传输的损耗,但是打包同样需要耗费时间,因此我们只针对静态的物体进行这个操作
  • 目前在实际引擎应用中,主要有四种优化方式,Static Batching ,Dynamic Batching ,GPU Instance , SRP Batcher。
  • 旧有的优化方式(StaticBatching静态批处理):
    • 避免添加大量的很小的网格(但是Nanite可以帮我们解决这点)
    • 避免添加大量的材质(但是虚拟材质可以帮我们解决这点)
    • 以上的优化方式在如今都可以不被考虑,但初学时依然需要记忆他们!

6.2.4 几何阶段

  • 顶点着色
    • 视图变换
    • 顶点着色
  • 这个阶段对应的着色器就是顶点着色器,主要处理的是顶点由模型空间变换到视图坐标系的一系列变换,也就是前文我们推导的MVP变换矩阵的前两部分
  • image.png
    6.2.4 顶点的坐标变换

  • 可选点处理
    • 曲面细分,通过插值增加顶点,实现曲面细分效果。
    • 几何着色器,操作图元,操作多个顶点组成的图元以生成更多图元。
    • image.png
      6.2.4.1 几何着色器
    • 什么是片元,图元?
    • 几何顶点被组合为图元(点,线段或多边形),然后图元被合成片元,最后片元被转换为帧缓存中的象素数据。片元是增加了着色,深度等信息的顶点,其阶段是等价的。

  • 投影
    • 正交
    • 透视
    • 至此完成顶点空间到投影空间的变换,变换的结果被称作归一化设备坐标系(NDC)
      image.png
      6.2.4.2 投影操作

  • 裁剪
    • CVV剔除,如果变化后的图元大于NDC时,则会进行裁剪,保留在NDC立方体内的图元
  • image.png
    6.2.4.3 片元裁剪剔除
    • 正反面剔除(可配置)
    • 剔除正面或者背面的片元,UnityShader中的Cull 指令
  • 图元被适当的裁剪,颜色和纹理数据也相应作出必要的调整,相关的坐标被转换为窗口坐标。最后,光栅化将裁剪好的图元转换为片元。

  • 屏幕映射
    • 由连续的线变为二维坐标上离散的点
  • 将NDC根据屏幕比例和大小变化为实际屏幕显示的位置
  • image.png
    6.2.4.4 屏幕映射

6.2.5 光栅化阶段

  • 三角形设置
    • 得到三角形边界信息
  • 三角形遍历
    • 寻找被三角形覆盖的像素,插值并填充三角形
  • image.png
    6.2.5. 三角形遍历
  • 同时还能进行的操作,抗锯齿处理

  • 抗锯齿MSAA
    • 有三种主流的抗锯齿类型,分别是
    • SSAA
      • 将片元放大并存入缓冲,对放大n倍的buffer采样
    • MSAA
      • 在光栅化阶段,计算多个覆盖样本
      • image.png
        6.2.5.1 抗锯齿MSAA
      • 这个操作是对片元进行深度测试和覆盖测试,来对像素进行叠加,计算多个覆盖版本,并且还可以将数据传递给后续逐片元操作中的混合操作。
    • FXAA,TXAA
      • 后处理技术,不在光栅化阶段

  • 逐片元操作阶段
    • 片元着色器
    • 颜色混合
    • 目标缓冲区
  • 片元首先要通过模版,深度测试,才能和缓冲区也就是上一帧素材混合进入到颜色缓冲区。裁剪会根据需求发生在深度测试之前
  • image.png
    6.2.5.2 逐片元操作流程

  • 片元着色
  • 使用三定点插值进行计算得到最终的着色结果
  • image.png
    6.2.5.3 差值实现三角形着色

  • 颜色混合
  • 透明度测试
    • 透明度小于某阈值被剔除
    • image.png
      6.2.5.4 透明度测试
  • 模版测试,深度测试
  • 存入深度信息和模版所需数据信息。这两个操作都是可配置的。

6.2.5 模版测试

  • 模版测试是位于逐片元操作中的一环,通过掩码读取模版缓冲区中的相关值,与参考值进行比较,这个比较结果可由开发者决定,不论通过与否,都可以修改缓冲区的值,主要用于设定渲染范围以及高阶的阴影边缘效果等

6.2.6 深度测试

  • 深度测试用于记录场景中片元深度,并且根据深度来调整渲染方案,可以省略不需要的片元来减少性能消耗,这个过程有时会在进入片元着色器阶段前进行。

6.2.7 混合模式

  • 混合(Blend)通过设置混合状态,来调节当前颜色值与颜色缓冲区中的颜色值之间的运算关系。
  • 不透明材质则直接覆盖掉颜色缓冲区中的值,而对于半透明等材质就可以对其进行混合操作来进行更多元化的更改,混合操作是高度可配置的。

  • 颜色混合中的测试的顺序是什么样的?详细可以查看:
    d65dbf5a8b7abadbb5899871c6c48b3.jpg
    6.2.5.3 测试顺序

6.3 固定渲染管线(已被淘汰)

  • 固定渲染管线如今几乎已经被抛弃,它是只有一定的配置能力而无法让我们掌控全局的渲染管线,我们只能通过控制管线流程的开关以此来实现局限的效果,现在已经被抛弃。目前主流的渲染管线为SRP(可编程渲染管线)

6.4 引擎中的渲染管线

  • 首先,我们需要区分两个概念,一个叫做渲染模式,也就是所谓的前向渲染,延迟渲染,另外就是我们这节课所提到的渲染管线。
  • 渲染模式更类似于处理方法,而渲染管线则是实际处理的对象,我们利用不同的处理方法处理渲染管线得到的数据。

  • 目前在Unity中,渲染管线主要分为Built-in 以及URP 和HDRP,后续Untiy大概率会合并URP 以及 HDRP。其中主要的区别是,Built-in是内置渲染管线,何为内置,也就是放在里面,它对于我们自定义的部分所提供的效果较少,适合完成一些对于定制需求效果没有那么高的项目。
  • 而URP则更加通用,这也是为何称其为URP,它为我们提供了更多可自定义的部分,比如RenderFeather就可以允许我们更自由的调用或修改管线中我们想要的部分。

6.5 结语

  • 至此,我们已经大致了解了一个软件中的资产是如何渲染到屏幕之上的,接下来我们将细致剖析各个流程之中的细节,并亲自动手实践,体会一个简单的三角片元是如何渲染的。

参考资料