想象一家繁忙的餐厅后厨:食材从进货口进入,经过洗、切、炒、装盘,最后端到顾客面前。这条固定工序就是图形渲染的“流水线”。在这条流水线上,大多数厨师都老老实实待在自己的岗位上,但有两位员工格外特别。一位是站在切菜和摆盘之间的“创意配菜师”——几何着色器,他可以在装盘前临时加料、改刀,把一块豆腐变成一簇小方块;另一位是“自由杂工队”——计算着色器,他们根本不排流水线,哪里忙就冲到哪里,能同时洗几百个杯子、揉几百个面团。几何着色器的创造力让画面更丰富,但动作太猛会把出菜口堵死;计算着色器的灵活与并行让 GPU 干更多杂活,但要调度得当才不会互相拥挤。搞懂了这两位员工的分工和脾气,你就掌握了现代 GPU 编程的一对重要搭档。
对于 AI 从业者来说,理解几何着色器和计算着色器能帮助你判断“哪些视觉效果该在 GPU 上生成、哪些计算该用并行加速”,是从事图形学、游戏引擎、实时渲染相关岗位的基础功。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
渲染管线全家福
把 GPU 想象成一家餐厅后厨的流水线。食材从进货口进入,按固定工序被清洗、切块、炒制、装盘。图形渲染也有这样一条“出菜流水线”,我们叫它渲染管线。每一个步骤都像固定工位,数据必须依次通过,直到最终画面的像素端上屏幕。
渲染管线从输入装配开始,把三角形、线段等“食材”送入,依次经过顶点着色器、几何着色器、光栅化、片元着色器。顶点着色器负责逐顶点变换,把 3D 坐标映射到屏幕空间;光栅化把数学三角形变成像素碎片;片元着色器为每个像素涂上颜色。GPU 真正厉害的地方在于,成千上万个顶点可以同时通过这些工位,这就是并行。
要理解这条流水线的“食材量”,最简单的起点是顶点数量。一个三角形由三个顶点组成,所以送入顶点着色器的顶点总数等于三角形数乘 3。
后厨的切菜数目永远是锅数乘三。理解了这份“家底”,再看几何着色器时,就会明白它为何能突然打破这种稳定的节奏。
- 三角形图元数$N_{\text{triangle}}$ 表示送入管线的三角形数量,是几何数据的基本单位。
- 单个三角形顶点数每个三角形由 3 个顶点组成,所以顶点数是三角形数乘以 3。
- 顶点总量$N_{\text{vertex}}$ 就是最终顶点着色器需要处理的顶点个数。
几何着色器定位
创意配菜师站在切菜和摆盘之间。食材已经切成块,但还没摆上盘;他可以临时改刀,把一块豆腐切成更多小块,也可以把两片肉合成一份。这就是几何着色器的角色:它在顶点着色器和光栅化之间,拥有对图元“增删改”的能力。
几何着色器是一个可编程阶段,输入是一个完整的图元——点、线段或三角形,输出可以是零个、一个或多个图元。它最特别的地方是“一对多”:输入一个点,可以输出一个四边形作为粒子;输入一个三角形,可以输出一圈新三角形形成轮廓。具体输出多少,完全由开发者编写的一段 shader 程序决定。
输入输出关系可以写成:
配菜师虽然自由,但“每份最多切成多少块”是有规定的,否则后厨的盘子储备和出菜通道都会出问题。
- 输入图元数$M_{\text{in}}$ 指进入几何着色器的图元个数。
- 最大生成倍数$k_{\text{max}}$ 是硬件或 API 允许的单个输入图元最大输出数量。
- 输出图元数$M_{\text{out}}$ 是几何着色器实际生成的图元总数,不能超过 $k_{\text{max}} \times M_{\text{in}}$。
图元增删魔法
配菜师有两招杀手锏:一招是“一菜变多菜”,把一个点变成一片飞溅的粒子;另一招是“给菜描边”,沿着三角形轮廓再贴一圈新三角形。这两招正是几何着色器最经典的应用:粒子发射与轮廓扩展。
粒子发射在游戏里用来做烟花、爆炸、喷泉:GPU 每收到一个输入点,就生成 m 个矩形小面片作为粒子,每个粒子再经过后续渲染变成动态画面。轮廓扩展用于卡通描边:把三角形每条边向外扩展,生成新的三角形形成边带,让物体轮廓看起来更粗、更有风格。
公式化描述:
配菜师每一次加料都有代价:加得越多,后面装盘师傅越累。所以 m 要根据场景精心设定,并不是越大越好。
- 输入图元数$N_{\text{in}}$ 是送入几何着色器的图元数量。
- 生成倍数$m$ 是每个输入图元输出的新图元数,粒子发射常设为几十,轮廓扩展常用 3。
- 输出图元数$N_{\text{out}}$ 是生成总数,光栅化阶段将收到这么多图元。
性能瓶颈警示
创意配菜师如果“一开百”,出菜口瞬间堆满盘子,后厨乱成一锅粥。几何着色器的麻烦正是输出量不可预测。它不像传统固定流水线那样每个阶段工作量稳定,而是允许一个线程生成一百个图元,另一个线程一个都不生成,这会让 GPU 的并行负载严重不均。
更麻烦的是,很多几何效果需要访问相邻图元的信息。比如轮廓扩展要找到三角形邻边,但几何着色器默认只拿到自己负责的那一个图元,想获取邻居数据就得跨线程通信,而跨线程通信在 GPU 上的成本非常高。这也是几何着色器逐渐被实例化渲染、曲面细分等方案替代的原因之一。
我们用放大率来衡量风险:
后厨总结:创意很好,但要克制。滥用创意配菜师,流水线会从“高效”变成“堵车”。
- 输入与输出图元数$N_{\text{in}}$、$N_{\text{out}}$ 分别是进入和离开几何着色器的图元数量。
- 比值 $R$表示几何膨胀倍数。
- 风险判断当 $R$ 远超 1 时,光栅化和缓存压力快速上升,容易成为瓶颈。
计算着色器破局
后厨里还有一支“自由杂工队”,不按流水线走,哪里缺人就冲到哪里。这就是计算着色器的设计哲学:不一定要排队经过顶点、光栅化,而是直接从 GPU 拿到一堆数据并行处理,干完活直接写回结果。
计算着色器不属于图形渲染固定管线的一部分。它可以读写纹理和缓冲区,执行任意数学与逻辑运算,是通用 GPU 计算的基石。它引入了两个核心概念:线程(thread)和工作组(workgroup)。一个任务被分成多个工作组,每个工作组里有若干个线程;线程之间可以通过共享内存协作。
总线程数可以用维度乘积表示:
自由杂工队不占出菜流水线,能干各种杂活,但人数规模要算清楚,不然调度器也会头大。
- 工作组数量$G_x,G_y,G_z$ 表示三维网格中每个方向的工作组个数。
- 局部线程数$L_x,L_y,L_z$ 表示一个工作组内每个方向的线程数。
- 总线程数两者相乘得到总共并行执行的线程数。
计算着色器实战
自由杂工队最常被请去干三类活:重新布置餐厅(后处理)、搬重物(物理模拟)、清理噪点(光线追踪降噪)。
后处理时,整张图像可以看作一个大数组,一个线程负责一个像素,并行执行滤镜或特效。物理模拟时,一个线程对应一个粒子或一个网格点,更新位置和速度。光线追踪降噪时,计算着色器用时空滤波器平滑蒙特卡洛渲染产生的噪声,让画面快速变得干净。
衡量并行收益的标准是加速比:
杂工队干活要分工合理、排队有序,才能真正跑赢一个人埋头苦干。这也是 GPU 编程的魅力与挑战。
- 串行执行时间$T_{\text{serial}}$ 是一个线程/核心顺序处理所有任务所需时间。
- 并行执行时间$T_{\text{parallel}}$ 是 GPU 上所有线程共同处理所需时间。
- 加速比$S$ 大于 1 代表并行有效,越大效果越好。
AI 应用场景
游戏实时粒子特效
阶段:图形渲染前处理/动画生成
解决的问题:传统做法把大量粒子顶点提前存储在 CPU 中,占用显存且逐帧更新慢。几何着色器在 GPU 端实时从一个点扩展到多个粒子,减少 CPU 传输,可快速生成烟花、喷泉、爆炸碎片。
具体案例:某开放世界游戏中,玩家点燃篝火,粒子系统用几何着色器从每个火源点生成数十个火星粒子,再配合计算着色器更新粒子速度,实现上百处篝火同时燃烧而不掉帧。
物理模拟与场景交互
阶段:物理仿真/碰撞计算
解决的问题:布料、水体等物理模拟需要大量并行运算,CPU 串行处理很难实时。计算着色器让每个线程负责一个质点或网格单元,并行更新位置、速度与约束。
具体案例:虚拟展厅中,窗帘、旗帜和角色衣摆使用计算着色器驱动,每帧更新数万质点位置,交互时拖动布料也能实时变形。
光线追踪降噪
阶段:渲染后处理
解决的问题:光线追踪每像素样本数少时噪声大,样本数多则耗时大。计算着色器对噪点图像做时空滤波,把多个历史帧数据合并,快速得到干净画面。
具体案例:建筑可视化软件用计算着色器实现降噪:每像素 4 个样本的噪点画面,经过约 10ms 滤波后输出接近 64 样本的干净结果,大幅缩短渲染时间。
Python 演示
import numpy as np def geom_shader_expand(points, m=6, radius=0.1): """模拟几何着色器: 每个输入点发射 m 个粒子, 返回形状 (n*m, 2)""" n = points.shape[0] angles = np.arange(m) * 2.0 * np.pi / m offsets = np.column_stack([np.cos(angles), np.sin(angles)]) * radius expanded = points[:, None, :] + offsets[None, :, :] return expanded.reshape(n * m, 2) def compute_shader_update(particles, velocity, dt=0.01): """模拟计算着色器: 所有粒子的位置同步更新""" particles[:, 0] += velocity[:, 0] * dt particles[:, 1] += velocity[:, 1] * dt return particles def run(): np.random.seed(42) input_points = np.random.rand(8, 2) # 8 个输入点 particles = geom_shader_expand(input_points, m=6, radius=0.15) velocity = np.random.randn(len(particles), 2) * 0.5 for _ in range(50): compute_shader_update(particles, velocity) print("粒子数量:", particles.shape[0]) print("前三个粒子位置:\n", particles[:3]) if __name__ == "__main__": run()
angles = np.arange(m) * 2.0 * np.pi / m把圆周均分成 m 份,得到粒子发射方向。offsets = np.column_stack([np.cos(angles), np.sin(angles)]) * radius生成 m 个方向上的单位偏移量。expanded = points[:, None, :] + offsets[None, :, :]用广播把每个输入点复制成 m 个,实现“一对多”。particles[:, 0] += velocity[:, 0] * dt模拟每个线程并行更新自己负责的粒子 x 坐标。compute_shader_update(particles, velocity)一次调用更新全部粒子,体现 GPU 并行。
常见误区
误区很多初学者以为几何着色器像“无限打印”一样,想要多少粒子直接生成多少,而且不影响性能。
正解每个输入图元的输出数量有上限 $k_{\text{max}}$,而且生成越多,光栅化和缓存压力越大。就像配菜师虽然创意无限,但后厨的盘子和灶台有限;无节制“一开百”会让出菜口堵死。恰当做法是配合实例化渲染或计算着色器生成数据。
为何常见:几何着色器的示例常展示从 1 个点生成大量粒子的惊艳效果,容易让人忽略背后的硬件代价。
误区一听“计算”着色器,就觉得它只适合做矩阵乘法、物理公式之类的纯数值计算,跟图形渲染没什么关系。
正解它虽然不经过传统图形管线,但可以读写纹理、缓冲区,是后处理、粒子系统、光线追踪降噪等图形任务的利器。好比自由杂工队虽然不炒菜,却能重新布置餐桌、清洗餐具、搬运物料,样样关乎餐厅运营。
为何常见:“计算”这个词太有误导性,让人忽略了它操作图形资源的能力。
误区有人觉得计算着色器功能那么强,以后渲染不再需要顶点/片元着色器,管线会消失。
正解计算着色器是图形管线的补充而非替代。最终画面的生成仍然需要光栅化、深度测试等固定硬件流程。自由杂工队可以帮忙备菜,但不能取代洗切炒的整套流水线。
为何常见:看到计算着色器能做很多渲染任务,就误以为它是“万能替代品”。
误区两者都能增加几何细节,初学者容易把它们画上等号,认为相互替换没问题。
正解曲面细分按固定拓扑均匀增加顶点,由硬件控制,负载可预测;而几何着色器允许任意增删图元,灵活但低效。好比一个是用模具把面团分成统一大小的小球,另一个是手工捏各种形状,后者更自由但更慢。
为何常见:两者都在“增加几何体”,功能边界容易被混淆。
误区把任务丢给 GPU 就默认“并行=更快”,结果某些场景反而变慢,于是怀疑 GPU 不行。
正解并行性能依赖访存模式、负载均衡、同步开销。大量线程空转、全局内存频繁访问都会抵消并行收益。就像杂工队都挤在一个门口进进出出,人越多反而越堵。需要优化内存访问、合并写入,并衡量加速比。
为何常见:单纯从“人多力量大”的直觉出发,忽略了并行系统的调度成本。
面试题
Q1. 渲染管线的正确顺序中,几何着色器位于哪个阶段之间?
- A.顶点着色器与光栅化之间← 正确答案
- B.光栅化与片元着色器之间
- C.片元着色器与混合器之间
- D.输入装配与顶点着色器之间
答案:A。几何着色器在顶点着色器和光栅化之间,负责对图元增删改。A 正确;其他位置都错误。用后厨比喻,配菜师站在切菜与摆盘之间。
Q2. 下列哪项不是几何着色器的合法输入图元?
- A.单个点图元
- B.一条线段图元
- C.一个三角形图元
- D.一个像素颜色值← 正确答案
答案:D。几何着色器输入是点、线、三角形等基本图元;像素颜色值是光栅化后的产物,不能作为几何着色器的输入。就像配菜师只能处理“食材块”,不能处理已经上桌的菜。
Q3. 计算着色器的核心设计思路是什么?
- A.完全替代固定渲染管线
- B.绕过图形管线做并行计算← 正确答案
- C.自动优化所有算法
- D.无需管理线程资源
答案:B。计算着色器不经过固定图形管线,直接用 GPU 并行执行通用计算,可读写缓冲和纹理。它不能替代管线,也不会自动优化,线程资源仍需程序员管理。
Q4. 轮廓扩展中,一个输入三角形通常生成几个新三角形?
- A.只生成 1 个新三角形
- B.只生成 2 个新三角形
- C.只生成 3 个新三角形← 正确答案
- D.只生成 4 个新三角形
答案:C。典型轮廓扩展沿三角形三条边各生成一个新三角形,所以 m=3。配菜师给三角形“描边”,绕一圈正好三个。
Q5. 几何着色器最主要的性能瓶颈来自何处?
- A.输出图元被大量放大← 正确答案
- B.输出图元被立即缓存
- C.输出图元被自动压缩
- D.输出图元被延迟处理
答案:A。几何着色器可让输出图元数量暴增,导致光栅化和缓存压力剧增,还可能造成负载不均衡。后厨里“一道菜变一百道”就会堵住出菜口。
Q6. 以下哪些任务适合交给计算着色器完成?
- A.后处理滤镜与特效← 正确答案
- B.形状边缘轮廓扩展← 正确答案
- C.物理碰撞与粒子模拟← 正确答案
- D.仅限整数数学运算
答案:ABC。计算着色器适合图形后处理、粒子物理、轮廓提取等并行任务;但它不限于整数运算,浮点/位操作都能做。D 错误。自由杂工队能干各种杂活,但不等于只搬一类货。
Q7. 计算着色器中“总线程数”如何计算?
- A.工作组数乘以每组线程数← 正确答案
- B.工作组数除以每组线程数
- C.工作组数加上每组线程数
- D.工作组数减去每组线程数
答案:A。总线程数=(各方向工作组数量乘积)×(各方向局部线程数乘积)。一维可理解为“组数×每组人数”,所以选 A。
Q8. 关于几何着色器和计算着色器的说法,正确的是?
- A.几何着色器在顶点与光栅化之间← 正确答案
- B.计算着色器能访问共享内存← 正确答案
- C.几何着色器适合无限制生成图元
- D.计算着色器可做后处理与降噪← 正确答案
答案:ABD。A 和 D 是两种着色器的典型特征;B 正确,工作组的共享内存可加速协作;C 错误,几何着色器输出有上限且受性能约束。创意配菜师不能无限制加菜。
Q9. 每个输入点最多发射 m 个粒子,输入 N 个点时输出粒子数上限是多少?
- A.N 加上 m
- B.N 乘以 m← 正确答案
- C.N 除以 m
- D.m 的 N 次方
答案:B。几何着色器输出总数 = 输入图元数 × 每个输入的最大输出数,即 N×m。不是加减除,更不是指数爆炸;后厨一盘菜最多切 m 块,N 盘就是 N×m 块。
Q10. 哪些原因会导致几何着色器成为性能瓶颈?
- A.输出图元数量不确定← 正确答案
- B.跨线程交换顶点数据← 正确答案
- C.光栅化阶段负载过重
- D.屏幕分辨率设置过高
答案:AB。输出不可控会造成负载失衡,跨线程通信成本高;C 是瓶颈造成的下游表现而非原因,D 主要影响像素阶段而非几何阶段。配菜师的“自由发挥”和“借调料”都需要付出代价。