第二章:渲染流水线概述

大家好,我是你们的老朋友。今天我们来聊聊渲染流水线——说白了,就是GPU怎么把一堆3D数据变成你屏幕上那幅画面的全过程。

我刚开始接触图形学的时候,总觉得这玩意儿特别玄乎。后来在项目里调了无数次性能,才慢慢摸清楚它的脾气。嗯,今天我就把这些经验掰开了讲给你听。

2.1 固定功能流水线 vs 可编程流水线

先说说历史。早期的GPU,用的是固定功能流水线。什么意思呢?就是每个阶段做什么事,都是硬件写死的。你没法改,只能通过设置一些参数来微调。就像一台傻瓜相机——你按快门就行,别想调光圈、快门速度。

我记得第一次接触固定流水线时,想做个简单的光照效果,结果发现只能选几种预设的光照模型。想加点自定义效果?没门。那时候我就想,这玩意儿太死板了。

后来可编程流水线出现了。这就像从傻瓜相机换成了单反——每个阶段你都可以写自己的程序(叫shader)。顶点怎么变换、像素怎么着色,全由你说了算。

核心区别一句话总结:

  • 固定功能流水线:硬件固定,只能调参数,不能改逻辑
  • 可编程流水线:用shader自定义每个阶段的处理逻辑

你想想看,现在谁还用固定流水线?基本上都淘汰了。从DirectX 9到10的过渡,就是固定到可编程的转折点。我在一个老项目中还见过固定流水线的代码,那维护起来真是...一言难尽。

2.2 渲染流水线的各个阶段

好,现在我们来看现代可编程流水线的四个核心阶段。我习惯把它们分成两大部分:几何处理像素处理

现代可编程渲染流水线 顶点处理 Vertex Shader 光栅化 Rasterization 片元处理 Fragment Shader 输出合并 Output Merge 输入:顶点数据 → 输出:屏幕像素 顶点位置/颜色 三角形 → 片元 片元 → 颜色值 颜色 → 帧缓冲 ✅ 可编程 ✅ 可编程 🔧 固定功能 🔧 固定功能

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、大量纹理采样
输出合并 ❌ 否 每个片元一次 带宽不足、频繁帧缓冲切换

好了,这一章的内容就到这里。记住,理解流水线的每个阶段,是性能优化的基础。你只有知道瓶颈在哪,才能对症下药。


专注资料整理