第三章:GPU渲染管线(上)——从顶点到像素的奇幻漂流
各位同学,今天我们来聊聊GPU最核心的部分——渲染管线。
说实话,我刚入行时,觉得渲染管线就是个黑盒子。模型进去,像素出来,中间发生了什么?完全不知道。直到我亲手设计过几个GPU模块后,才真正理解这条流水线的精妙之处。
这一章,我们只讲上半段:顶点着色器 → 光栅化 → 片段着色器。下半段(深度测试、混合、输出)留到下一章。
3.1 顶点着色器:3D模型的“变形金刚”
想象一下,你手里有一个3D模型。它由一堆顶点组成,每个顶点有位置、颜色、法线等信息。但这些顶点是在模型坐标系里的,屏幕可看不懂。
顶点着色器干的事,说白了就是:把每个顶点从3D世界变换到屏幕上的2D位置。
我个人习惯把顶点着色器看作一个“坐标转换器”。它接收一个顶点,输出一个变换后的顶点。中间可以干很多事——位移、旋转、缩放,甚至做点特效。
核心公式(MVP变换):
// 顶点着色器核心逻辑(伪代码)
vec4 worldPos = modelMatrix * vertexPos; // 模型→世界
vec4 viewPos = viewMatrix * worldPos; // 世界→观察
vec4 clipPos = projectionMatrix * viewPos; // 观察→裁剪
嗯,这里要注意:顶点着色器是逐顶点执行的。一个模型有10万个顶点,它就执行10万次。所以性能优化往往从这里开始。
我在项目中遇到过一个问题:一个角色模型有20万个顶点,顶点着色器里还做了复杂的蒙皮计算。结果帧率直接掉到15帧。后来我把蒙皮计算挪到计算着色器里做预计算,帧率回到了60帧。你看,减少顶点着色器的计算量,是优化的第一要义。
3.2 光栅化:把“数学”变成“像素”
顶点着色器处理完后,我们得到了一堆三角形(三个顶点一组)。但这些三角形还是数学上的抽象概念——它们由坐标和方程描述。
光栅化要做的,就是判断每个像素是否被三角形覆盖。说白了,就是把连续的三角形,离散成一个个像素点。
你想想看,一个三角形在屏幕上可能覆盖了1000个像素。光栅化器就要逐个判断:这个像素在三角形内吗?在的话,就生成一个片段(Fragment)。
避坑指南:
我曾经在设计光栅化器时,忽略了像素中心采样的问题。结果三角形边缘出现了严重的锯齿。后来加上了多重采样抗锯齿(MSAA),每个像素采样多个点,边缘就平滑多了。
光栅化的输出是什么?是一堆片段。每个片段包含:
- 屏幕坐标 (x, y)
- 深度值 z
- 插值后的顶点属性(颜色、法线、纹理坐标等)
注意,这里的属性是插值得到的。比如三角形三个顶点的颜色分别是红、绿、蓝,那么三角形内部的像素颜色,就是根据重心坐标插值出来的渐变效果。
3.3 片段着色器:给像素“上色”
光栅化之后,我们有了片段。但片段只是“候选像素”,还没决定最终颜色。
片段着色器,就是决定每个片段最终颜色的地方。它接收插值后的属性,输出一个颜色值。
最简单的片段着色器,就是直接输出插值颜色:
// 最简单的片段着色器(GLSL风格)
void main() {
FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 纯红色
}
但实际项目中,片段着色器要复杂得多。它要处理:
- 纹理采样:从纹理图中读取颜色
- 光照计算:根据法线、光源方向计算漫反射、高光
- 阴影判断:判断当前像素是否在阴影中
- 透明度处理:alpha混合
性能警告:
片段着色器是逐像素执行的。一个1080p的屏幕有约200万个像素。如果你的片段着色器里做了复杂的计算(比如循环、分支),性能会急剧下降。
我曾经见过一个项目,片段着色器里写了个for循环计算光照,结果GPU直接跑满。后来改成了预计算+纹理查找,性能提升了10倍。
3.4 三个阶段的流水线协作
好了,我们把三个阶段串起来,看看一个3D模型是怎么变成屏幕像素的:
- 顶点着色器:把模型的每个顶点从3D世界变换到屏幕空间
- 光栅化:把三角形离散成像素片段,并插值属性
- 片段着色器:为每个片段计算最终颜色
这三个阶段是流水线并行的。顶点着色器处理完一批顶点,光栅化就开始工作,同时顶点着色器继续处理下一批。这样GPU的吞吐量才能最大化。
我个人习惯用工厂流水线来类比:
- 顶点着色器 = 原材料加工(把铁矿石炼成钢坯)
- 光栅化 = 切割成型(把钢坯切成零件毛坯)
- 片段着色器 = 表面处理(给零件喷漆、打磨)
任何一个环节慢了,整条流水线都会卡住。这就是为什么GPU设计要追求负载均衡。
3.5 一个完整的例子
假设我们要渲染一个三角形,三个顶点分别是:
| 顶点 | 位置 (模型空间) | 颜色 |
|---|---|---|
| A | (0, 1, 0) | 红色 |
| B | (-1, -1, 0) | 绿色 |
| C | (1, -1, 0) | 蓝色 |
经过顶点着色器(MVP变换),这三个顶点被映射到屏幕坐标。假设屏幕是1920x1080,三个顶点变成了:
- A': (960, 100, 0.5) // 屏幕中心偏上
- B': (100, 980, 0.8) // 左下
- C': (1820, 980, 0.2) // 右下
光栅化器开始工作:它遍历所有像素,判断哪些像素在三角形A'B'C'内部。假设有10万个像素被覆盖,就生成10万个片段。
每个片段都带有插值后的颜色。比如三角形中心的像素,颜色是红绿蓝的混合(大致是灰色)。
片段着色器收到这些片段,直接输出插值颜色。最终屏幕上就出现了一个红绿蓝渐变的三角形。
关键点总结:
- 顶点着色器:逐顶点,做坐标变换
- 光栅化:逐三角形,生成片段
- 片段着色器:逐片段,计算颜色
- 三个阶段流水线并行,提高吞吐量
好了,这一章就到这里。下一章我们继续讲渲染管线的下半段——深度测试、模板测试、混合,以及最终的帧缓冲区输出。到时候你会看到,片段着色器算出来的颜色,还不一定是最终显示的颜色呢。
有什么问题,欢迎在评论区交流。我个人最喜欢回答关于光栅化器设计的问题——毕竟那是我踩坑最多的地方。