【转载】一文读懂什么是渲染管线

本文源地址:

01 | 渲染基础

渲染(Render)定义

渲染在电脑绘图中是指软件从模型生成图像的过程,通俗讲就是在计算机里面给虚拟世界"拍照"。渲染主要分为两种,一种是 预渲染(pre-rendering),它的计算强度很大,通常用于电影制作;另一种是 实时渲染(real-time rendering),多用于三维游戏,并且依靠显卡完成渲染过程.

渲染管线

渲染是一个非常复杂的过程,它需要从一系列的顶点、纹理等信息出发,把这些信息最终转换成一张人眼可以看到的图像,在这个过程中,通常是需要 CPU 和 GPU 密切配合,渲染管线具体的实现细节会严重依赖于所使用的软件和硬件,因此并不存在所谓通用的渲染管线。

tips 💁‍♂:虽然管线的划分粒度不一样,但是每个阶段的具体功能其实是差不多的,原理也是一样的,并没有太大的差异。

以 Unity 内置渲染管线 (Unity Build-in Rendering Pipeline)为例,可以将其分为 CPU 应用程序端渲染逻辑GPU 渲染管线,下面是它的完整流程图,其中蓝色区域是 CPU 端要做的工作,绿色部分是 GPU 端需要处理的内容。

这些流程看起来很复杂,其实我们可以类比成现实中的拍照。电视剧《隐秘的生活》中,张东升老师带着他的岳父岳母去爬山,到山顶的时候说要给他们拍照留念,拍照分为三部分,首先是找到要拍照的人摆好 POS,然后摄像机找好角度,对准焦距,最后按下快门,其中按快门这个操作对应的是 Unity 摄像机调用 Render()方法的过程 。在拍照的时候,如果物体超出了摄像机视野范围或者被其他物体给挡住了,在图像中都不会显示,这也就是剔除;背后的山在人的后面,这就对应不同的渲染顺序;岳父岳母的位置、穿什么衣服、太阳光照等信息都会被摄像机捕获,这些就是要打包的数据。最后调用 SetPassCall、DrawCall,将渲染图元传递给 GPU 渲染管线进行处理。比喻并非十分恰当,主要还是希望能够加深印象。

剔除、设置渲染顺序等操作我们已经记住了,但这些具体是什么意思呢?

CPU 渲染逻辑

该阶段通常是由 CPU 负责实现,作为开发人员,可以对这个阶段进行控制。在这个阶段主要包含以下几个步骤:

  • 进行剔除(Culling)工作:剔除主要分为三类,分别是

    视锥体剔除(Frustum Culling):如果场景中的物体和在视锥体外部,那么说明物体不可见,不需要对其进行渲染.。在 Unity 中可以通过设置 CameraField of view, Clipping Planes 等属性修改视锥体属性。 层级剔除(Layer Culling Mask):通过给物体设置不同的层级,让摄像机不渲染某一层,在 Unity 中可以通过 Culling Mask 属性设置层级可见性 遮挡剔除(Occlusion Culling):当一个物体被其他物体遮挡而不在摄像机的可视范围内时不对其进行渲染

  • 设置渲染顺序:渲染顺序主要由 渲染队列(Render Queue) 的值决定的,不透明队列(RenderQueue < 2500),根据摄像机距离 从前往后排序,这样先渲染离摄像机近的物体,远处的物体被遮挡剔除;半透明队列(RenderQueue > 2500),根据摄像机距离 从后往前排序,这是为了保证渲染正确性,例如半透明黄色和蓝色物体,不同的渲染顺序会出现不一样的颜色 。
  • 打包数据: 将数据提交打包准备发送给 GPU,这些数据主要包含三部分,分别是
    • 模型信息:顶点坐标、法线、UV、切线、顶点颜色、索引列表...
    • 变换矩阵:世界变换矩阵、VP 矩阵(根据摄像机位置和 fov 等参数构建)
    • 灯光、材质参数:shader、材质参数、灯光信息
  • 调用 SetPass Call, Draw Call
    SetPass Call: Shader 脚本中一个 Pass 语义块就是一个完整的渲染流程,一个着色器可以包含多个 Pass 语义块,每当 GPU 运行一个 Pass 之前,就会产生一个 SetPassCall,所以可以理解为调用一个完整渲染流程。
    DrawCall:CPU 每次调用图像编程接口命令 GPU 渲染的操作称为一次 Draw Call。Draw Call 就是一次渲染命令的调用,它指向一个需要被渲染的图元(primitive)列表,不包含任何材质信息。GPU 收到指令就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出成屏幕上显示的那些漂亮的像素。

    💁‍♂Unity 中可以通过开启 Stats 查看 SetPass Call 和 DrawCall 调用的次数,它们可能会占用大量 CPU 资源,是性能优化中非常值得关注的一个点

CPU 渲染阶段最重要的输出是渲染所需的几何信息,即渲染图元(rendering primitives),通俗来讲,渲染图元可以是点、线、三角面等,这些信息会传递给 GPU 渲染管线处理。

GPU 渲染管线

GPU 渲染管线由许多步骤组成,比如 顶点处理图元装配及光栅化片元处理输出合并

顶点处理

  • 顶点着色器(Vertex Shader) 顶点着色器的处理单位是顶点,也就是说,输入进来的 每个顶点都会调用一次顶点着色器。它主要执行 坐标转换逐顶点光照的任务, 坐标转换是将顶点坐标从模型空间转换到齐次裁剪空间中,它是通过 MVP(Model、View、Projection)转换得到的,在 shader 代码中,可以使用 UnityObjectToClipPos()函数来实现。逐顶点光照得到的光照结果比较不自然,所以一般是在片元着色器中进行光照计算。
  • 曲面细分着色器 (Tessellation Shader) 这是一个可选的着色器,主要是对三角面进行细分,以此来增加物体表面的三角面的数量。借助它可以实现细节层次(LOD,Level-of-Detail)的机制,使得离摄像机越近的物体具有更加丰富的细节,而远离摄像机的物体具有较少的细节,如下图所示。
  • 几何着色器(Geometry Shader) 它也是一个可选的着色器,它以完整的图元(比如,点)作为输入数据,输出可以是一个或多个其他的图元(比如,三角面),或者不输出任何的图元。几何着色器的拿手好戏就是将输入的点或线扩展成多边形。下图展示了几何着色器如何将点扩展成多边形。

图元装配(Primitive Assembly)

经过顶点处理阶段,我们已经知道了顶点在裁剪空间的位置,接下来可以在裁剪空间中进行裁剪、背面剔除、屏幕映射等操作。

  • 裁剪(Clipping) 一些图元,它可能一部分位于摄像机视野内,另一部分在摄像机视野外部,外面这部分不需要进行渲染,可以将它裁剪掉. 例如,线段的两个顶点一个位于视椎体内而另一个位于视椎体外,那么位于外部的顶点将被裁剪掉,而且 在视椎体与线段的交界处产生新的顶点来代替视野外部的顶点(在裁剪空间中进行)。
  • 标准化设备坐标(Normalized Device Coordinates,NDC) 在裁剪空间的基础上,进行 透视除法 (perspective division)后得到的坐标叫做 NDC 坐标,将坐标从裁剪空间的(-w,-w,w)变换为(-1,-1,1),即除 w,获得 NDC 坐标是为了实现屏幕坐标的转换与硬件无关。
  • 背面剔除(Back-Face Culling) 背对摄像机的三角面剔除,上面我们讲到过模型数据中含有索引列表,列表中的三个点组成一个三角片,如果这三个点是顺时针排列的,认为是背面,否则认为是正面。 上图中的图元 t 1 ,它的三个顶点 v1,v2,v3 很明显是 逆时针(count-clockwise,CCW)排列的,因此认为它是正面,t 2 则是顺时针(clockwise,CW)排列,为背面。我们可以使用行列式 (determinant)来确定投影后的 2D 三角形到底是 CW 还是 CCW 顺序。行列式的第一行由顶点 v1 和 v2 坐标确定,而第二行由顶点 v1 和 v3 坐标确定。如果行列式的值为负数,那么该三角面是背面朝向;如果为正数,则是正面朝向。
  • 屏幕映射(ScreenMapping) 屏幕映射(ScreenMapping)的任务是把每个图元的 x 和 y 坐标转换到屏幕坐标系(Screen Coordinates)下。屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。 假设,我们需要把场景渲染到一个窗口上,窗口的范围是从最小的窗口坐标(x1,y1)到最大的窗口坐标(x2,y2),其中 x1< x2 且 y1< y2。由于我们输入的坐标范围在-1 到 1,因此可以想象到,这个过程实际是一个缩放的过程,如图 2.10 所示。你可能会问,那么输入的 z 坐标会怎么样呢?屏幕映射不会对输入的 z 坐标做任何处理。实际上,屏幕坐标系和 z 坐标一起构成了一个坐标系,叫做窗口坐标系(WindowCoordinates)。这些值会一起被传递到光栅化阶段。

光栅化(Rasterization)

该阶段主要是将变换到屏幕空间的 图元离散化为片元的过程。

  • 三角形设置(Triangle Setup)

我们从上一个阶段获得图元的顶点信息,也就是三角面每条边的两个端点,但如果要得到整个三角网格对像素的覆盖情况,我们就必须 计算每条边上的像素坐标。 为了能够计算边界像素的坐标信息,我们就需要得到三角形边界的表示方式。这样一个计算三角网格表示数据的过程就叫做三角形设置。

  • 三角形遍历(Triangle Traversal)

三角形遍历阶段会根据上一个阶段的计算结果来判断一个三角网格覆盖了哪些像素,并使用三角网格 3 个顶点的顶点信息对整个覆盖区域的像素进行插值。

这一步的输出就是得到一个片元序列。需要注意的是,一个片元并不是真正意义上的像素,而是包含了很多状态的集合,这些状态用于计算每个像素的最终颜色。这些状态包括了(但不限于)它的屏幕坐标、深度值 Z、顶点颜色,以及其他从几何阶段输出的顶点信息,例如法线、纹理坐标等。

片元着色器(Fragment Shader)

它最主要的任务就是着色,光栅化阶段实际上并不会影响屏幕上每个像素的颜色值,而是会产生一系列的数据信息,用来表述一个三角网格是怎样覆盖每个像素的。而每个片元就负责存储这样一系列数据。着色有两种最常见的技术,分别是纹理贴图和光照技术。

纹理贴图(Textures)

纹理贴图也称为纹理映射,是将图像信息映射到三角形网格上的技术,以此来增加物体表面的细节,令物体更具有真实感。纹理技术有很多,常见的是凹凸贴图(bump mapping)、法线贴图(normal mapping)、高度纹理(height mapping)、阴影贴图(shadowmap)等。例如图中左边地球仪是一个球形,但我们也可以将地图绘制在右图一张二维的平面上,那么它们之间就存在着纹理映射的关系,我们想要获取地球仪上任意一点的信息,都可以从贴图中寻找。

纹理贴图是片段着色器的主要操作,通过贴图技术可以实现很多高级的效果。我们将贴图上的每个像素称为纹素(texel,纹理像素 texture pixel 的意思,用于和像素进行区分), 纹理映射其实就是进行纹素和像素对应的过程

在上图右边是一副 32*32 的贴图,它由一格一格的像素组成,每个像素都有一个地址,这个地址就叫做纹素地址。纹素地址可以使用一个二维数组来存储,这个二维数组就称为 纹素数组

我们一般使用一个二维的坐标(u,v)来表示纹理坐标,其中 u 是横坐标,v 是纵坐标,因此纹理坐标一般也被称为 UV 坐标。UV 坐标一般被归一化到0,1之间,但是如果 UV 超出这个范围,我们就需要指定纹理坐标的寻址方式,也叫作平铺方式。常见的寻址方式有:重复寻址(repeat)、边缘钳制寻址(clamp,拉伸纹理边缘)和镜像寻址(mirror)。在 Unity 中,可以通过设置贴图的 Wrap Mode 来修改,其中 per-axis 可以单独控制 Unity 如何在 U 轴和 V 轴上包裹纹理。下图展示了 Unity3d 中纹理的重复寻址和钳制寻址方式。

纹理采样

是指给定一个坐标,去寻找它在纹素数组中的值。由于纹素和像素通常不是一 一对应的(例如将 10x10 的图片映射到 50x50 的屏幕中),所以我们需要决定像素所对应的纹素信息时,需要用到纹理的滤波方式。

Unity 中的滤波主要有三种,可以通过 Filter Mode 进行设置,

  • Pointer,点过滤,纹理在靠近时变为块状,会产生较为明显的失真。
  • Bilinear,双线性过滤,pixel 对应的纹理坐标为中心,采该纹理坐标周围 4 个 texel 的像素,再取平均,以平均值作为采样值。
  • Trilinear,三线性过滤,以双线性过滤为基础。会对 pixel 大小与 texel 大小最接近的两层 Mipmap level 分别进行双线性过滤,然后再对两层得到的结果进生线性插值。

如果我们要把一个很大的贴图映射到很小的一块区域里面,可以想象效果肯定会很差,这时可以使用 Mipmap 纹理链,也就是根据原图生成很多个大小不同的图片,然后根据映射区域的大小,选择使用哪一张图片。

在 Unity 中可以通过勾选 Generate Mip Maps 属性来启用 Mipmap,由于生成多张图片,这会占用一定的内存。勾选后可以通过滑动条来预览不同等级的贴图。

光照计算(Lighting)

光照由直接光和间接光组成,计算光照最常用的就是 phong 模型了,它是一个经验模型,参数信息都是经验得到的,并没有实际的物理意义,所以利用 Phong 模型会出现违背物理规则的时候。Phong 模型将物体光照分为三个部分进行计算,分别是: 漫反射、镜面反射和环境光。

  • 漫反射 Difuse 漫反射是投射在粗糙表面上的光向各个方向反射的现象。当一束平行的入射光线射到粗糙的表面时,表面会把光线向着四面八方反射,所以入射线虽然互相平行,由于各点的法线方向不一致,造成反射光线向不同的方向无规则地反射。在漫反射中,视角的位置是不重要的,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。但是,入射光线的角度很重要。
  • 镜面发射 Specular 镜面反射是指若反射面比较光滑,当平行入射的光线射到这个反射面时,仍会平行地向一个方向反射出来。
  • 环境光 Ambient 环境光分量是用来模拟全局光照效果的,其实就是在物体光照信息基础上叠加上一个较小的光照常量,用来表示场景中其他物体反射的间接光照。

    光照模型计划在下一节进行详细讲解,并通过代码实现。

输出合并(Output-Merger)

终于到了渲染流水线的最后一步,在 DirectX中,该阶段被称为输出合并阶段,而 OpenGL将其称为逐片元操作( Per-Fragment Operations),从称呼中就可以看出,这个阶段主要是对每一个片元进行一些输出合并操作,包括 Alpha 测试、模板测试、深度测试和混合,它有一下几个主要任务:

  • 决定每个片元的可见性。这涉及了很多测试工作,例如深度测试、模板测试等。为什么要进行测试呢?因为屏幕上的一个像素可能存在多个片元进行竞争,通过测试等规则,可以决定哪个片元最终能够渲染到屏幕上
  • 如果一个片元通过了所有的测试,就需要把这个片元的颜色值和已经存储在颜色缓冲区中的颜色进行合并,或者说是混合。

Alpha 测试

通过片元数据,可以获取该片元的 alpha 值,如果 alpha 值小于某个数的话,则直接将该片元丢弃,不进行渲染,这是非常"粗暴"的(即只渲染透明度在某一范围内的片元),可以用来做一些树叶镂空的效果。 模板测试(Stencil Test)

模板测试默认是不开启的,我们可以通过 glEnable(GL_STENCIL_TEST)指令将其打开,这是一个开发者可以高度配置的阶段。如果开启了模板测试,GPU 会首先读取模板缓冲区中该片元位置的模板值,然后将该值和读取到的参考值进行比较,这个比较函数可以是由开发者指定的,例如小于时舍弃该片元,或者大于等于时舍弃该片元。如果这个片元没有通过这个测试,该片元就会被舍弃。

深度测试(Depth Test)

根据日常经验,近处的物体会遮挡远处的物体,这种效果我们可以通过深度测试来模拟实现。它通过将深度缓存中的值和当前片元的深度进行比较,计算是否需要更新深度缓存和颜色缓存,如果不需要则将该片元丢弃,这于模板测试比较类似。我们在渲染半透明物体时, 需要开启深度测试而关闭深度写入功能。 混合(Blend)

一个片元经过层层测试,总算来到了混合功能面前,对于不透明物体,开发者可以关闭混合(Blend)操作。这样片元着色器计算得到的颜色值就会直接覆盖掉颜色缓冲区中的像素值。但对于半透明物体,我们就需要使用混合操作来让这个物体看起来是透明的。下面是一个简化版的混合操作流程图。

这个阶段也是高度可配置的,开发者可以选择是否开启混合功能。如果没有开启混合功能,就会直接使用片元的颜色覆盖掉颜色缓冲区中的颜色,因此是无法得到透明效果的。

提前深度测试(Early-Z)

由于深度/模板测试是在片段着色器之后进行的,所以导致着色器计算资源的浪费,因为这些被遮挡的片段对我们终的画面是没有任何贡献的,而我们还花费了大量的资源对它们进行了复杂的光照等一系列计算。 Early-Z Culling正是在这种情况下出现的,它发生在顶点着色器和片元着色器之间。不过我们需要注意的是 Early-Z Culling本不是管线标准,只是硬件厂商用来加速渲染的一种优化手段,所以在不同的硬件上会有不同的实现,而且 Early-Z Culling并不保证一定有效,它需要硬件的支持。

帧缓存(Frame Buffer)

可以简单理解为一个临时画布,GPU 渲染完成的信息会存放在帧缓存区,等待使用,上述各种测试也是在帧缓冲区进行的

帧缓冲区主要包含 颜色缓冲区(Color Buffer)深度缓冲区(Depth Buffer),假设下面蓝色线框是一个帧缓冲区,需要对蓝色和红色三角形的片元进行渲染,红色片元的深度值是 0.8,蓝色片元的深度值是 0.5。渲染红色片元时,由于它的深度值小于帧缓冲区的初始深度值,所以它的 z-buffer 值和 color buffer 值会覆盖帧缓冲区对于位置的值。渲染蓝色片元时,由于它的深度值比帧缓冲区的深度值都要小,所以可以覆盖缓冲区中的内容。

当模型的图元经过了上面层层计算和测试后,就会显示到我们的屏幕上。我们的屏幕显示的就是颜色缓冲区中的颜色值。但是,为了避免我们看到那些正在进行光栅化的图元,GPU 会使用 双重缓冲 (Double Buffering)的策略。这意味着,对场景的渲染是在幕后发生的,即在后置缓冲 (Back Buffer)中。一旦场景已经被渲染到了后置缓冲中,GPU 就会交换后置缓冲区和前置缓冲 (Front Buffer)中的内容,而前置缓冲区是之前显示在屏幕上的图像。由此,保证了我们看到的图像总是连续的。

总结

下一节开始实战,计划使用 Unity Shader实现经典的 phong光照模型。

引用

1 UnityShader 入门精要

2 细说图形学渲染管线

3 GAMES101-现代计算机图形学入门-闫令琪_哔哩哔哩_bilibili