渲染流水线总共有三大概念性阶段:应用阶段、几何阶段、光栅化阶段。上一篇已经讲了应用阶段,这一篇开始进入 GPU 主导的部分。几何阶段和光栅化阶段通常被叫做 GPU 流水线,它们各自又可以细分成多个子流水线阶段,如图 2 所示。
我原本以为今天能把几何阶段和光栅化阶段一起讲完,但写起来发现还是大意了,所以这一篇先讲几何阶段,光栅化阶段留到下一篇。
图2
绿色:我们可以编程 黄色:可以配置 蓝色:不可以编程
1.几何阶段
以下涉及到坐标变换的时候,如从模型空间变换到齐次裁剪空间,屏幕映射......只需要知道有这么个过程即可,具体如何进行变换需要一些线性代数方面的知识,会在数学篇讲解。
1.1顶点着色器
1.1.1输入
顶点着色器的输入来自于上一阶段应用阶段,也就是来自于CPU,主要处理顶点信息,顶点着色器会将这些输入进来的每个顶点进行逐顶点操作
注意,顶点不只是一个点,它可以包含很多信息,列如顶点位置,顶点颜色,顶点法线,顶点纹理坐标等。
1.1.2主要工作
顶点着色器我们是可以进行编程的,我们可以对顶点中的信息进行修改计算,比如修改顶点的位置信息,顶点的颜色信息,顶点的纹理坐标等等。既然顶点信息包含很多属性,那么这完全可以理解成一个结构体,而我们编程则是对这些结构体进行修改,可以参考以下伪代码:
// 顶点着色器的输入:一个顶点信息结构体 struct VertexInput { float3 position; // 顶点位置,通常模型空间 float4 color; // 顶点颜色 RGBA float3 normal; // 顶点法线 float2 texCoord; // 纹理坐标 UV ...... }; // 顶点着色器:逐顶点执行--我们编程的地方 VertexOutput VertexShader(VertexInput v) { VertexOutput o; // 必须进行的操作:将顶点的位置模型空间转换到裁剪空间 o.position = mul(MVP, float4(v.position, 1.0)); // 其他属性可原样传递,也可修改 o.color = v.color; o.normal = v.normal; o.texCoord = v.texCoord; return o; }必须进行的任务:
- 将顶点位置进行坐标变换,必须将顶点位置从模型空间经过一系列变换到齐次裁剪空间,方便后续进行裁剪,如图3所示。(什么是裁剪参考下文)
图3
可选的任务:
1.修改顶点的颜色,通过顶点的颜色和光照来计算该顶点的光照颜色,如图4所示。
2.修改顶点的位置,模拟水面效果。
3.等等对顶点信息的修改。
图4
1.1.3输出
从1.1.2可以看出来,顶点着色器的输出主要是一些经过处理的顶点信息,其中顶点位置是必须处理的。
而顶点着色器的输出方式有两种,也就是说将这些处理好的数据发送给谁:
- 最常见的是经过光栅化后发送给片元着色器进行处理。
- 而在现代的 Shader Model 中,它还可以把数据发送给曲面细分着色器或几何着色器,感兴趣的读者可以自行了解
1.2 曲面细分和几何着色器以及被隐藏的固定阶段
1.2.1 了解曲面细分和几何着色器
这两个着色器不是本书重点,只需要简单了解即可
曲面细分着色器(Tessellation Shader)是一个可选的着色器,它用于细分图元(忙猜类似Blender的精简修改器减面数)。
几何着色器(Geometry Shader)同样是一个可选的着色器,它可以被用于执行逐图元(Per-Primitive)的着色操作,或者被用于产生更多的图元。
1.2.2 我的困惑
分析1.2.1后发现,这两个着色器处理的是图元,但是顶点着色器的输出的却是顶点信息?
后面查阅资料才知道,其实还有两个固定阶段被隐藏了,既图元装配和透视除法,其中图元装配解答了这个疑惑,所以实际情况应该是:
顶点着色器 →图元装配→曲面细分和几何着色器[可选] → 裁剪→透视除法→ 屏幕映射
图元装配:顶点着色器输出的是顶点信息,而图元装配将这些顶点按照当前图元拓扑——点、线、三角形、patch 等——组装成图元,建立顶点之间的连接关系,然后发送给后续阶段。
什么是图元?图元可以是一些点,线,面。而顶点是构成图元的基本单位,可以简单把图元理解为一个三角面片,三角面片是图元的一种,游戏中的模型基本上是由一个个三角面片组成的,如图5所示。
图5
1.3裁剪
1.3.1输入
裁剪的输入是顶点信息经过图元装配后的图元,裁剪是逐图元操作的。
1.3.2为什么要进行裁剪
一个图元在摄像机视野内可以有三种关系:
- 该图元完全在视野外
- 该图元完全在视野内
- 该图元有一部分在视野内,有一部分在视野外,也就是和边界相交,此时我们应该把在视野外的部分去掉,这就是裁剪。
1.3.3如何进行裁剪
顶点着色器已经把顶点的位置信息变换到齐次裁剪空间,坐标为 (x,y,z,w)。裁剪时以图元为单位,拿它的 3 个顶点分别对裁剪平面判断。以 OpenGL 为例,需要满足:
判断结果:
3 个顶点全在外:丢弃整个图元;
3 个顶点全在内:保留整个图元;
有内有外:求边与裁剪平面的交点,插值出新顶点,裁掉外面部分,如图6所示。
图6
1.3.4输出
输出是裁剪后的图元,可能生成新顶点并分割成多个图元。
1.4屏幕映射
1.4.1输入
注意,裁剪后不是立马进入屏幕映射,正如1.2.2中所说的,裁剪→透视除法→屏幕映射。
所以屏幕映射的输入是经过裁剪然后再进行透视除法后的图元,此时这些图元内各个顶点的位置信息现在是在NDC坐标(经过透视除法)下的。
1.4.2主要任务
屏幕映射的任务是把每个图元内各个顶点的NDC坐标转换到屏幕坐标系下,屏幕坐标系是一个二维坐标系,它和我们用于显示画面的分辨率有很大关系。
1.4.3如何进行屏幕映射
NDC 下输入的坐标范围通常在 −1 到 1,因此这个过程可以理解为一次缩放和平移:把 NDC 的 x、y 映射到屏幕的像素坐标,如图7。
图7
1.4.4 Z坐标如何处理
Z坐标不是不处理,而是它不负责决定屏幕上的位置,只负责决定这个顶点离摄像机有多远。
屏幕映射会把Z映射成深度值,留到后面做深度测试。如果两个片元落在同一个像素上,就比较它们的深度值,离摄像机更近的留下,更远的被挡住。后续的深度测试和深度写入会用到它。
1.4.5 输出以及要小心你的屏幕坐标系
屏幕映射输出的信息是图元,每个图元的各个顶点位置信息已经变换到屏幕空间下。
屏幕坐标系在 OpenGL 和 DirectX 之间存在差异如图8所示,事情就是这样发生了,我们能做的是要时刻小心。
图8