第二章:渲染流水线概述
大家好,我是你们的老朋友。今天我们来聊聊渲染流水线——说白了,就是GPU怎么把一堆3D数据变成你屏幕上那幅画面的全过程。
我刚开始接触图形学的时候,总觉得这玩意儿特别玄乎。后来在项目里调了无数次性能,才慢慢摸清楚它的脾气。嗯,今天我就把这些经验掰开了讲给你听。
2.1 固定功能流水线 vs 可编程流水线
先说说历史。早期的GPU,用的是固定功能流水线。什么意思呢?就是每个阶段做什么事,都是硬件写死的。你没法改,只能通过设置一些参数来微调。就像一台傻瓜相机——你按快门就行,别想调光圈、快门速度。
我记得第一次接触固定流水线时,想做个简单的光照效果,结果发现只能选几种预设的光照模型。想加点自定义效果?没门。那时候我就想,这玩意儿太死板了。
后来可编程流水线出现了。这就像从傻瓜相机换成了单反——每个阶段你都可以写自己的程序(叫shader)。顶点怎么变换、像素怎么着色,全由你说了算。
核心区别一句话总结:
- 固定功能流水线:硬件固定,只能调参数,不能改逻辑
- 可编程流水线:用shader自定义每个阶段的处理逻辑
你想想看,现在谁还用固定流水线?基本上都淘汰了。从DirectX 9到10的过渡,就是固定到可编程的转折点。我在一个老项目中还见过固定流水线的代码,那维护起来真是...一言难尽。
2.2 渲染流水线的各个阶段
好,现在我们来看现代可编程流水线的四个核心阶段。我习惯把它们分成两大部分:几何处理和像素处理。
2.3 顶点处理(Vertex Processing)
第一个阶段,顶点处理。这个阶段跑的是顶点着色器(Vertex Shader)。它的任务很简单:把每个顶点从模型空间变换到屏幕空间。
具体来说,顶点着色器要干这几件事:
- 坐标变换:模型矩阵 → 视图矩阵 → 投影矩阵,一套组合拳打下来
- 顶点属性传递:颜色、法线、纹理坐标等,传给下一阶段
- 逐顶点光照(可选):在顶点级别计算光照
我在项目中遇到过一个问题:一个场景里模型面数特别多,顶点数上千万。结果顶点着色器成了瓶颈。后来怎么解决的?我用了LOD(细节层次),远处用低模,近处用高模。帧率直接翻倍。
小技巧:顶点处理是逐顶点执行的。顶点越少,这阶段越快。但别为了性能把模型压得太狠——质量也很重要。
2.4 光栅化(Rasterization)
光栅化,这是整个流水线里最「硬核」的阶段。它把顶点处理完的三角形,转换成屏幕上的片元(Fragment)。片元是什么?你可以理解成「候选像素」——每个片元最终可能变成屏幕上的一个像素,也可能被丢弃。
光栅化是固定功能的,你没法编程。但你可以通过一些参数影响它:
- 视口设置:渲染到屏幕的哪个区域
- 裁剪:把视口外的三角形切掉
- 背面剔除:不渲染背对摄像机的面
我曾经犯过一个低级错误:忘了开背面剔除。结果一个简单的立方体,渲染了12个三角形而不是6个。性能直接腰斩。嗯,从那以后我每次建渲染管线,第一件事就是检查背面剔除开了没。
注意:光栅化阶段会做深度测试的早期版本(Early-Z)。如果你在片元着色器里修改了深度值,这个优化会被禁用。性能会下降不少。
2.5 片元处理(Fragment Processing)
片元处理,跑的是片元着色器(Fragment Shader)。这是整个流水线里最灵活、也最耗性能的阶段。
片元着色器要干什么?说白了,就是决定每个片元最终显示什么颜色。你可以做:
- 纹理采样:从纹理里取颜色
- 逐像素光照:计算每个像素的光照效果
- 阴影计算:判断当前片元是否在阴影里
- 后处理效果:模糊、边缘检测等
你想想看,一个1080p的画面有200多万个像素。每个像素都要跑一遍片元着色器。如果你的shader写得复杂,比如做了10次纹理采样,那性能开销就大了去了。
我记得有一次优化一个游戏场景,片元着色器里有个循环,每次循环都采样一次纹理。我把它改成了一次采样、多次复用,性能提升了30%。
性能关键点:
- 减少纹理采样次数
- 避免动态分支(if/else)
- 使用低精度数据类型(half/float)
- 尽量减少依赖纹理读取
2.6 输出合并(Output Merge)
最后一个阶段,输出合并。也叫渲染输出单元(ROP)。这个阶段把片元着色器算出来的颜色,写到帧缓冲里。
输出合并阶段要做的事:
- 深度测试:比较当前片元和已有片元的深度,决定谁在前面
- 模板测试:用模板缓冲做遮罩效果
- 混合:把新颜色和已有颜色混合(比如透明效果)
- 多重采样:处理MSAA的抗锯齿
这个阶段也是固定功能的。但你可以配置它的行为。比如混合模式,你可以设置加法混合、乘法混合,或者自定义混合因子。
我曾经在项目里遇到一个诡异的问题:透明物体渲染顺序不对,导致画面出现奇怪的遮挡。后来发现是深度测试和混合的顺序没搞对。透明物体应该先渲染不透明的,再渲染透明的,而且要关闭深度写入。
避坑指南:输出合并阶段是带宽大户。如果你频繁读写帧缓冲,带宽很容易打满。我建议尽量用合并写入,减少不必要的帧缓冲切换。
2.7 各阶段性能对比
最后,我整理了一个表格,方便你对比各个阶段的性能特征:
| 阶段 | 可编程? | 执行次数 | 性能瓶颈常见原因 |
|---|---|---|---|
| 顶点处理 | ✅ 是 | 每个顶点一次 | 顶点数过多、复杂变换计算 |
| 光栅化 | ❌ 否 | 每个三角形一次 | 三角形过多、大三角形覆盖大量像素 |
| 片元处理 | ✅ 是 | 每个片元一次 | 复杂shader、大量纹理采样 |
| 输出合并 | ❌ 否 | 每个片元一次 | 带宽不足、频繁帧缓冲切换 |
好了,这一章的内容就到这里。记住,理解流水线的每个阶段,是性能优化的基础。你只有知道瓶颈在哪,才能对症下药。