第三章: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模型是怎么变成屏幕像素的:

  1. 顶点着色器:把模型的每个顶点从3D世界变换到屏幕空间
  2. 光栅化:把三角形离散成像素片段,并插值属性
  3. 片段着色器:为每个片段计算最终颜色

这三个阶段是流水线并行的。顶点着色器处理完一批顶点,光栅化就开始工作,同时顶点着色器继续处理下一批。这样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万个片段。

每个片段都带有插值后的颜色。比如三角形中心的像素,颜色是红绿蓝的混合(大致是灰色)。

片段着色器收到这些片段,直接输出插值颜色。最终屏幕上就出现了一个红绿蓝渐变的三角形。

关键点总结:

  • 顶点着色器:逐顶点,做坐标变换
  • 光栅化:逐三角形,生成片段
  • 片段着色器:逐片段,计算颜色
  • 三个阶段流水线并行,提高吞吐量

好了,这一章就到这里。下一章我们继续讲渲染管线的下半段——深度测试、模板测试、混合,以及最终的帧缓冲区输出。到时候你会看到,片段着色器算出来的颜色,还不一定是最终显示的颜色呢。

有什么问题,欢迎在评论区交流。我个人最喜欢回答关于光栅化器设计的问题——毕竟那是我踩坑最多的地方。