Skip to content

门泊吴船亦已谋

渲染管线浅析

渲染管线会把内存中的三维模型输出到二维的屏幕上,它具有明显的先后顺序,每一个阶段的输出都会作为下一个阶段的输入。它也可以翻译为渲染流水线

大致上,渲染管线有数据预处理、顶点着色器、图元组装、光栅化和片元着色器这几个阶段
不过不同的游戏引擎可能阶段划分不尽相同也没有统一的说法,另外也可能会增加其他阶段以实现更好的效果

数据预处理

由 CPU 负责完成,假设要渲染一堆模型,就需要准备好这些模型的所有点、三角面、法向量、材质、变换矩阵等信息

然后可以做一些简单的剔除,把不可能被渲染的模型直接排除掉,比如排除掉远处模型的距离剔除、根据摄像机剔除不可能看见的模型的视锥剔除等等
需要注意的是一般只能剔除整个模型,而不会具体到模型的点和面

顶点着色器

首先进入这一阶段之前,需要把需要的顶点坐标、面、法向量、变换矩阵等准备好,并放入显存之中
其中,模型的顶点坐标是相对于模型原点的,也就是局部空间;而我们能在屏幕上看到的模型被拍扁成了二维图像,甚至可以有透视效果,这个叫做屏幕空间;从局部空间到屏幕空间的过程中会先转换为裁剪空间以方便视锥外的点的剔除
把模型的信息从局部空间转换到裁剪空间,就是这一阶段需要做的事情

具体说的话,有以下三个步骤

Model 变换,从局部空间转换到世界空间
一个模型的 transform 会有 translation、rotation、scale 这三个东西,经过这样的 transform,模型中的顶点和法向量就能转移到世界坐标

View 变换,从世界空间转换到视角空间
简单地说,就是通过旋转平移场景中的所有物体,把摄像机挪到原点,并且面向某个轴。其实跟第一个步骤很像
这个步骤是为了方便接下来的投影

Projection 变换,从视角空间转换到裁剪空间
这个比较关键,有透视投影和正交投影两种,透视投影会有近大远小的效果而正交投影不会

这三个步骤其实就是对所有的点依次做三个矩阵乘法,根据结合律可以直接先把它们乘起来(也就是上一个阶段中传入进来的变换矩阵)

图元组装

总的说就是把视锥外的点和面剔除掉,然后得到屏幕空间上的坐标

具体来说要做下面几件事情

剪裁
经过了上一个阶段,可以通过点的 z 和 w 坐标判断某个点是否在视锥内部
如果不在就进行剪裁,可以用 Sutherland-Hodgeman 算法来生成新的边界

背面剔除
根据三角面的环绕顺序可以判断面向摄像机的是正面还是背面
使用行列式(或者说二维向量的叉乘)的结果看正负号就行了

齐次除法
向量除以它的 w 坐标,得到 NDC 坐标
(只有透视投影才需要做齐次除法,因为正交投影后点的 w 坐标都是 1)

视口变换
把 NDC 坐标映射到屏幕上,只是简单的平移和缩放

光栅化

简单来说,我们上一阶段的点和面这些都是连续的,而电脑屏幕上的像素点是离散的,要把一个面或者一条边都变成一堆像素
所以光栅化也就是离散化

因为一个面被转换为了一个一个的像素点,我们需要为每一个像素确定它的法向量、贴图坐标等等
于是插值就行了,需要注意的是法向量直接线性插值会爆炸所以要用到透视校正插值

片元着色器

一个片元也就是光栅化中得到的一个像素
在这个阶段中,需要根据环境中的光源和每一个片元或者说像素的贴图、颜色、法向量、位置等计算出片元的最终颜色

光照的计算
比较经典的就是 Phong 光照模型,最终颜色=环境光+漫反射+镜面反射
材质信息需要提供环境光、漫反射和镜面反射的颜色或者贴图,另外还可以带上系数和透明度
其中环境光是常量;漫反射用光线与法向量夹角的余弦值乘上片元本身的颜色来近似模拟,因为余弦随着夹角增大而减小,可以很好的模拟漫反射;镜面反射则是需要根据光线与法向量得到光线的反射向量,再跟摄像机视线的方向求余弦

贴图
假设三维模型的壳就是一层纸,然后把纸撕碎(有一点夸张),全都贴到一个正方形里,这样这个模型上每一个点都可以映射到正方形上一个唯一的点,这个正方形就是贴图。(这个说法可能不太现实,因为贴图通常不会撕这么碎;而且面的大小也不一定是一比一,甚至可以不均匀)
那么模型中的每一个点都会有一个贴图坐标,光栅化之后的每个片元也会得到一个插值过后的贴图坐标
于是通过贴图坐标就能读取贴图中这个坐标上的颜色数据,比如说在漫反射贴图上我可以得到这个片元的漫反射颜色,法线贴图上可以得到这个片元的法线

并行性

渲染是一个高度并行的东西
渲染管线中主要的计算都集中在显卡上

显卡的结构我们知道它的核心数远远大于 CPU,比如目前千元级的 N 卡就能有大约一千五百个 CUDA 核心
并且渲染过程中的运算主要是对一堆向量做相同的运算(尤其是矩阵乘法),也就是会在每个 GPU 核心上都运行同样的逻辑

因此渲染计算在硬件层面上本身就能并行,而且由于模型中点的数量一般都远大于显卡核心数量,所以并行也非常充分

另外提一下流水线,我们很容易联想到 CPU 指令的流水线技术
于是我们就可以做类似的优化,比如说游戏主线程、CPU 上的渲染线程、GPU 上的渲染计算这三个阶段就有一点类似 CPU 指令的流水线技术

Draw call

由 CPU 发起,把需要渲染的原始数据扔给显卡,让显卡计算并输出到屏幕上

需要注意的一个问题是 CPU 需要为渲染准备数据,比如材质、点、法线等等,而在显卡上计算通常会很快,因此如果频繁的发起 draw call 可能会导致性能瓶颈被卡在 CPU 准备数据上

优化的思路主要是把多次 draw call 合并为一次;或者多次 draw call 可以共享相同的数据,比如静态网格或者共享材质