第二章 图形渲染管线总览:从顶点到像素的完整流程

大家好,我是你们的老朋友。今天我们来聊聊图形渲染管线——说白了,就是显卡怎么把一堆数学点变成你屏幕上那个漂亮画面的全过程。

我个人习惯把渲染管线比作一条流水线。你想想看,工厂里造一辆车,得经过冲压、焊接、涂装、总装。显卡也一样,从顶点数据进去,到像素颜色出来,中间要经过好几个固定步骤。每个步骤都有专门的硬件单元在处理。

2.1 渲染管线的宏观结构

先给个全景图。整个管线大致分四个阶段:

  • 顶点处理——处理每个顶点的位置、颜色、法线等信息
  • 光栅化——把三角形变成屏幕上的像素点
  • 像素处理——计算每个像素最终的颜色
  • 输出合并——处理深度测试、透明度混合等

嗯,这里要注意,我说的只是简化版。实际硬件里每个阶段还能再拆成好几个子阶段。我在项目中遇到过有人把整个管线背得滚瓜烂熟,但一调优就抓瞎——因为他不知道哪些阶段是瓶颈。

2.2 顶点处理阶段

这个阶段干的事,说白了就是把3D模型里的顶点坐标,从模型自己的局部坐标系,一路变换到屏幕坐标系。

具体来说:

  • 模型变换:把顶点从模型空间挪到世界空间
  • 视图变换:从世界空间挪到相机空间(也就是以相机为原点)
  • 投影变换:把3D坐标压成2D坐标(透视投影或正交投影)
  • 视口变换:映射到实际的屏幕像素坐标

我曾经调试过一个bug,模型在屏幕上显示的位置总是不对。查了两天才发现,是顶点着色器里矩阵乘法的顺序写反了。记住,在大多数图形API里,变换矩阵是左乘的:最终位置 = 投影矩阵 × 视图矩阵 × 模型矩阵 × 顶点位置

关键点:顶点处理阶段是逐顶点执行的。如果你的模型有100万个顶点,那顶点着色器就要跑100万次。所以顶点数越少,这阶段越快。

2.3 光栅化阶段

这个阶段很有意思。它把三角形“拆碎”成屏幕上的像素点。你想想看,一个三角形在3D空间里是连续的,但屏幕上的像素是离散的。怎么决定哪些像素属于这个三角形?

硬件里用的是扫描线算法或者半平面测试。简单说,就是判断每个像素的中心点是否落在三角形内部。如果是,就生成一个片元(fragment)。

我记得刚入行时,总觉得光栅化很简单。直到有一次做移动端优化,发现GPU的像素填充率成了瓶颈。后来才明白,光栅化阶段不光要判断覆盖,还要做属性插值——把顶点的颜色、纹理坐标等信息,插值到每个像素上。

避坑指南:我曾经在项目中遇到过一个诡异现象——三角形边缘出现锯齿。原因就是光栅化时没有做多重采样抗锯齿(MSAA)。如果你对画质有要求,记得开启MSAA,硬件会帮你做额外的采样和混合。

2.4 像素处理阶段

片元生成之后,就轮到像素着色器(也叫片元着色器)上场了。这个阶段决定每个像素最终长什么样。

像素着色器能干的事很多:

  • 采样纹理贴图
  • 计算光照(漫反射、镜面反射、环境光)
  • 应用法线贴图、高度贴图等
  • 做透明度测试

这里有个容易忽略的点:像素着色器的执行次数。它并不是每个像素只执行一次。如果你开启了多重采样,每个像素可能要执行2x、4x甚至8x次。我见过有人把复杂的计算放在像素着色器里,结果帧率直接掉到个位数。

警告:像素着色器是渲染管线中最耗性能的阶段之一。尽量把计算量大的逻辑移到顶点着色器或者计算着色器里。记住一个原则:能少算一次就少算一次。

2.5 输出合并阶段

这是管线的最后一步。所有像素着色器算出来的颜色,要在这里做最后的处理:

操作 说明
深度测试 比较当前片元的深度值和深度缓冲区里的值,决定是否覆盖
模板测试 根据模板缓冲区的内容,决定是否丢弃片元
透明度混合 把当前片元的颜色和帧缓冲区里的颜色混合
颜色写入 把最终颜色写入帧缓冲区

嗯,这里要注意深度测试的顺序。默认情况下,深度测试是在像素着色器之后执行的。但如果你开启了早期深度测试(Early-Z),硬件会在像素着色器之前就做深度比较。这样可以提前丢弃被遮挡的片元,省掉不必要的像素着色器计算。

我曾经在一个项目中,场景里有很多重叠的透明物体。结果发现渲染顺序不对,透明效果完全乱套。后来才意识到,透明物体需要从远到近排序渲染,而且不能开启深度写入。这些细节,文档里都有,但你不踩一次坑,很难真正记住。

2.6 各阶段的数据流

整个管线里,数据是这样流动的:

  1. 输入装配器:从顶点缓冲区读取顶点数据
  2. 顶点着色器:处理每个顶点,输出变换后的位置和属性
  3. 几何着色器(可选):可以增删改图元
  4. 光栅化器:生成片元,插值属性
  5. 像素着色器:计算每个片元的颜色
  6. 输出合并器:深度测试、混合、写入帧缓冲区

你可能会问,为什么要有这么多阶段?直接算不行吗?其实每个阶段都有它存在的理由。比如几何着色器,虽然现在用得少了,但在做粒子系统、草地渲染时,它能帮你动态生成几何体,省掉CPU的负担。

总结一下:图形渲染管线不是一条直线,而是一条有分支、有反馈的流水线。理解每个阶段在干什么、数据怎么流动,是优化渲染性能的基础。我建议你下次写渲染器时,先在纸上画出数据流图,再动手写代码。

好了,这一章就到这里。下一章我们会深入顶点着色器,聊聊矩阵变换的那些坑。到时候我会分享一个我踩过的、关于法线变换的经典bug。