想象你经营一家玩具工厂,老板(CPU)下达命令:批量生产十万只“发光恐龙”。工厂里有一条长长的传送带,叫作渲染管线。传送带的不同工位上站着四类师傅:摆位师傅守在入口,把每块恐龙零件按图纸放到三维空间的正确位置;装配师傅站在中段,能把一块大零件敲成好几块,也能直接把废料扔下传送带;上色师傅守在末端,拿着喷枪给恐龙表面的每一个小点涂上颜色——涂多少、涂什么色,全由他说了算;厂房二楼还有一间独立实验室,里面的计算员不碰传送带,专门用几千人同时开工,做物理模拟、粒子运动这类通用计算。 这四类师傅正是 GPU 上四种可编程的着色器:顶点着色器、几何着色器、片元着色器与计算着色器。他们使用的“工作指令语言”叫 HLSL,工厂的管理规章叫 ShaderLab。这篇文章带你走进这座玩具工厂,把 GPU 流水线的四道工序、两种语言,以及它们在程序化特效和 GPU 粒子系统中的真实用法,一次讲明白。
对 AI 从业者而言,Shader 是理解 GPU 并行计算的钥匙:图形渲染与神经网络训练共享同一套大规模并行架构,读懂着色器,你就读懂了 GPU 的底层思维。
可视化演示
展示GPU渲染管线VS-GS-RS-FS-FB阶段衔接、顶点着色器模型到裁剪空间变换矩阵链、片元着色器纹理采样光照逐像素计算及计算着色器线程组并行GPU粒子,含数据流背景动画。
本节配有交互动效(进入视口后自动加载,约 3KB)。
渲染管线总览
工厂效率的秘诀不是某一台机器有多快,而是整条传送带能同时开工:第一个零件还在摆位师傅手里时,第二个零件已上了传送带,上色师傅也没闲着。GPU 的渲染管线正是这样一条流水线,每一帧画面上百万个三角形鱼贯而过,各个阶段同时忙碌。早期 GPU 是“固定管线”——工位上的师傅是被焊死的机器人,只能做固定动作。后来 GPU 开放了“可编程管线”,把关键工位换成可以写代码的师傅,于是有了四类可编程着色器。
为什么 GPU 能扛住百万级三角形?因为它的“人手”极多。一块 GPU 有上千个计算核心,每个顶点分给一个核心同时处理。理想情况下:
回到玩具工厂:传送带在转,每个工位都有几十个师傅并排站岗,这就是 GPU 并行之美。接下来,我们去见四位师傅本人。
- 顶点总数 n一帧画面中所有三角形的顶点数量,常见量级是几十万到几百万。
- 单点耗时 t处理一个顶点所需的时间,比如完成坐标变换、光照计算。
- 并行核心数 mGPU 上能同时干活的计算核心数量,消费级显卡就有几千个。
- 总耗时 T 并行任务被平均分给所有核心后,消耗的总时间由“单批工作量”决定。
顶点着色器
摆位师傅的日常工作:图纸(模型文件)上写着每块零件的三维坐标,但图纸坐标系和车间坐标系不一样。师傅得先把零件搬到车间的大坐标系,再对齐到摄像机“眼睛”看到的方位,最后换算成屏幕上的位置。这一套“坐标搬家”,就是顶点着色器的核心。
顶点着色器是渲染管线的第一个可编程阶段,逐个处理每个顶点:输入一个顶点的属性(位置、法线、UV 坐标),输出变换后的顶点位置和其他数据。关键特性是一个顶点进,一个顶点出——它不能凭空增加或删除顶点。顶点变换的标准操作是“矩阵三连乘”:
摆位师傅只会“摆位置”,不负责涂颜色——涂色是后面的事。就算他想改颜色,也只能把颜色数据“托付”给下一个工位,这就是图中从顶点着色器流向片元阶段的插值数据。
- 本地坐标 v_local顶点在模型文件里的原始位置,四维向量 (x, y, z, w),w 通常为 1。
- 模型矩阵 M_model负责旋转、缩放、平移,把模型从自身坐标系搬到世界坐标系。
- 视图矩阵 M_view以摄像机为原点重新描述所有顶点,相当于把世界“重新对焦”进镜头。
- 投影矩阵 M_proj把相机坐标压成裁剪坐标,产生近大远小的透视效果。
- 矩阵乘法顺序矩阵乘法不满足交换律,最右边的矩阵最先作用于顶点。
几何着色器
装配师傅比较特别:他不一定按图纸来,可以发挥主观能动性。图纸说一块大板子,他觉得拼成三块小三角才好看,就“啪”地敲成三块;觉得是废料,直接扔下传送带。这种“改图元”的能力,就是几何着色器。
几何着色器位于顶点着色器之后、片元着色器之前。它处理的不是单个顶点,而是完整的图元(primitive)——点、线段、三角形。它可以把一个三角形变成多个三角形,可以把一个点变成一簇点,也可以完全不输出(删除图元)。顶点着色器是严格的 1:1,几何着色器是 1:N。设输入图元总数为 N_in,第 i 个图元发射出 k_i 个新图元,则:
几何着色器最著名的应用,是把一个三角形“爆炸”成三块,或把一根线放大成一片草地。装配师傅的创造力常常让上色师傅加班,所以开发时要小心别让它太“高产”。
- 输入图元数量 N_in进入几何着色器的点、线、三角形数量。
- 发射数量 k_i第 i 个输入图元通过发射操作输出的新图元个数,可以是 0 或任意正整数。顶点着色器永远做不到这一点。
- 求和符号 Σ把每个输入图元的产出全部累加起来。
- 输出总数 N_out送到下一阶段的图元总量,决定了后续片元着色器的工作量。
片元着色器
上色师傅是整条流水线上最忙的人:最终画面上每一个可见小点(像素)都需要他喷一次漆。一块 1080p 的屏幕有 1920×1080≈207 万个像素,意味着每帧他要喷 200 多万次漆。而且颜色不能乱喷:要看这个点是正面还是侧面,光线照不照得到,纹理贴图画了什么。
GPU 把三角形“敲碎”成一个个像素片元(fragment),这个敲碎过程叫光栅化,属于固定功能阶段;之后每个片元调用一次片元着色器(也叫像素着色器)。它输入的是插值后的顶点数据(法线、UV 等),输出的是颜色值。最简单的光照模型——兰伯特漫反射,描述了“表面法线与光线的夹角决定亮度”:
上色师傅不仅会按法线打光,还会“贴墙纸”:从纹理图(UV 映射的贴图)上取色,这就是纹理采样。在玩具工厂里,他手里永远攥着一本色卡,色卡对应恐龙身上的花纹图纸。
- 表面法线 N垂直于该像素所在表面的单位向量,代表“这个点朝哪个方向”。
- 光源方向 L从表面指向光源的单位向量。
- 点积 N·L两个单位向量的点积等于夹角余弦,衡量表面朝向与光线的对齐程度。
- max(0, ·)把负值(背光面)钳制为 0——背光面不该有漫反射。
- 光源强度与漫反射系数I_light 是入射光强,k_d 是材质反射光的比例。
计算着色器
二楼独立实验室里的计算员,不碰玩具传送带。他们的任务五花八门:模拟十万个气球在恐龙乐园里飘动,计算物理碰撞,甚至帮老板训练“AI 质检员”识别次品。唯一要求是:计算要到 GPU 上跑,而且要快。
计算着色器是通用 GPU 计算(GPGPU)的入口。它不在图形渲染管线的主路径上,而是通过 Dispatch 命令直接调度一大群“线程”并行执行同一段代码。它没有顶点、图元、像素的概念,只有“线程”和“数据”。它的调度模型是“分组”的:所有线程被划分为多个线程组(thread group),每个组里又有许多线程。为了在代码里定位“我是谁、该处理哪块数据”,需要计算全局线程编号:
GPU 粒子系统就是计算着色器的代表作:每颗粒子是一份数据,十万个线程并行更新它们的位置,再用顶点、片元流水线渲染出来。玩具工厂的“十万个气球”,就是这么被算出来的。
- localID线程在自己组内的编号,类似“班内学号”,范围是 [0, groupSize)。
- groupSize每个线程组包含的线程数,通常是 64、128 等 2 的幂。
- groupID线程组在整个调度网格中的编号,类似“班级号”。
- globalID全局唯一编号,用来直接索引要处理的数组或缓冲区。
HLSL语法基础
四位师傅要干活,必须有“工作指令书”——这就是 HLSL(High Level Shading Language),微软为 Direct3D 设计的高级着色语言,语法类似 C 语言。每份指令书写着“输入什么数据、怎么算、输出什么”。一个典型 HLSL 着色器由三部分组成:数据类型声明、输入输出结构体(通过语义绑定)、入口函数。
HLSL 中颜色通常用 float4 四维向量表示(r, g, b, a),四个分量取值范围是 0 到 1:
入口函数通常叫 main,每个阶段写一份;编译时用着色模型(如 Shader Model 5.0)指定能力等级。指令书写得好,师傅们才能高效配合——快递单写清楚,GPU 才知道把法线送给哪道工序。
- float4 类型HLSL 最常用的向量类型,装 4 个 float,常用 .xyzw 或 .rgba 访问分量。
- 分量范围 [0,1]颜色分量被归一化到 0~1,避免大数运算,GPU 上更快更稳。
- 与整数的换算显示到屏幕时,把 0~1 映射到 0~255 的 RGB 整数。
- 语义绑定结构体成员用 SV_Position、NORMAL、UV 等语义标记,让 GPU 知道数据流该往哪儿去。语义就是 HLSL 的“数据快递单”。
Shader Lab实战
HLSL 是一道道工序的“操作指令书”,可是一个完整车间还需要管理规章:生产线怎么配置、用哪几套操作手册、车间降级时怎么办。Unity 的管理规章就叫 ShaderLab——它不是一门新语言,而是一个“外壳”,内部包的还是 HLSL。
ShaderLab 用类似 YAML 的声明式语法组织一个 Shader 文件,核心结构是:Properties(对外暴露的属性面板)、SubShader(针对不同硬件档位的实现)、Pass(具体的渲染通道)、Fallback(降级方案)。程序化特效常配合 C# 脚本动态修改材质属性,一个经典操作是“颜色渐变”——在片元着色器里对两种颜色做线性插值:
把时间变量塞进 p,恐龙身上的花纹就会流动起来;把 UV 坐标塞进去,就能画出一个渐变天空。程序化特效的千变万化,底层就是 ShaderLab 搭好车间、HLSL 写指令、数学公式当配方。
- 起始色 c_start渐变的起点颜色向量。
- 结束色 c_end渐变的终点颜色向量。
- 混合权重 p控制过渡进度,通常来自 UV 坐标、时间变量或法线方向。
- 线性插值权重系数 (1-p) 与 p 之和恒为 1,保证颜色始终落在两端点之间。
AI 应用场景
计算着色器加速 AI 推理
阶段:模型部署与推理加速
解决的问题:神经网络推理包含大量矩阵乘法,在 CPU 上串行执行很慢;计算着色器可以把权重矩阵拆分给成千上万个线程并行乘加,就像二楼实验室全员同时开工。在游戏引擎里做实时 AI(机器人导航、NPC 决策)时,延迟必须低,GPU 并行计算是最佳解。
具体案例:一个无人车模拟器用 Unity 渲染场景,同时用 Compute Shader 跑轻量级卷积神经网络做障碍物识别,把 CPU 上 30ms 的推理压到 3ms,让虚拟车实时避障。
AI 生成纹理与片元着色器渲染
阶段:AIGC 内容生产与渲染
解决的问题:用 AI 生成程序化纹理(如扩散模型生成恐龙皮肤贴图)后,需要用片元着色器做纹理采样、颜色映射和光照叠加,才能把贴图“穿”到 3D 模型上。上色师傅手里的“色卡”就是 AI 生成的贴图。
具体案例:某游戏团队用 AI 生成风格化草地纹理,再写自定义片元着色器,把“草尖偏黄、草根偏绿”的规则编进采样逻辑,配合 UV 实现大规模程序化着色,省下大量美术手工调色时间。
强化学习可视化的 GPU 热力图
阶段:AI 训练过程可视化与调参
解决的问题:训练强化学习智能体时,难以直观看到它在环境中的“价值分布”。用顶点、片元着色器把状态价值画成热力图:网格顶点坐标交给顶点着色器变换,片元着色器按价值数值映射颜色,实现 60 帧实时刷新。
具体案例:机器人抓取训练中,用 Shader 热力图实时显示机械臂周围每个位置的成功率预估,工程师据此调整奖励函数,训练效率明显提升。
Python 演示
import numpy as np def vertex_shader(angle): # 模拟顶点着色器: 本地坐标 -> 模型矩阵旋转 -> 屏幕归一化坐标 v_local = np.array([ [-0.5, -0.3, 0.0, 1.0], [0.5, -0.3, 0.0, 1.0], [0.0, 0.5, 0.0, 1.0], ], dtype=float) c, s = np.cos(angle), np.sin(angle) model = np.array([ [c, -s, 0, 0], [s, c, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1], ]) clip = (model @ v_local.T).T ndc = clip[:, :2] / 2.0 + 0.5 return ndc def fragment_shader(uv, width, height): # 模拟片元着色器: 对三角形包围盒内每个像素做重心坐标插值上色 img = np.zeros((height, width, 3), dtype=np.uint8) x, y = uv[:, 0], uv[:, 1] v0 = np.array([x[1] - x[0], y[1] - y[0]]) v1 = np.array([x[2] - x[0], y[2] - y[0]]) denom = v0[0] * v1[1] - v1[0] * v0[1] for py in range(height): for px in range(width): p = np.array([px / (width - 1) - x[0], py / (height - 1) - y[0]]) u = (p[0] * v1[1] - v1[0] * p[1]) / denom v = (v0[0] * p[1] - p[0] * v0[1]) / denom w = 1.0 - u - v if u > -0.01 and v > -0.01 and w > -0.01: img[py, px] = (int(255 * w), int(200 * u), int(180 * v)) return img def main(): uv = vertex_shader(0.3) img = fragment_shader(uv, 64, 64) cnt = int((img.sum(axis=2) > 0).sum()) print("模拟 GPU 流水线完成: 64x64 渲染输出") print("三角形内部像素数量:", cnt) print("三个顶点屏幕坐标:", uv.round(3).tolist()) if __name__ == "__main__": main()
v_local = np.array([...])定义三角形的三个顶点坐标,相当于模型文件里的顶点数据输入。model = np.array([...])构造绕 Z 轴旋转的模型矩阵,对应 HLSL 里的 M_model。clip = (model @ v_local.T).T矩阵乘顶点,完成“模型矩阵变换”这一步,这正是顶点着色器的核心动作。ndc = clip[:, :2] / 2.0 + 0.5把 [-1,1] 坐标归一化到 [0,1],模拟从裁剪空间映射到屏幕。denom = v0[0] * v1[1] - v1[0] * v0[1]计算重心坐标的分母,用于判断像素是否落在三角形内。u, v, w重心坐标三个权重,三者之和为 1,像素在三角形内当且仅当它们都大于等于 0。img[py, px] = (int(255 * w), ...)用重心坐标给像素上色,等价于片元着色器输出颜色。
常见误区
误区我以为在顶点着色器里写个颜色值,整个模型的颜色就定了,毕竟“顶点”代表模型,改顶点就是改模型。
正解顶点着色器的本职是变换位置——计算顶点该出现在屏幕哪里。它虽然能输出颜色数据,但那只是“托付给下一个工位”的中间数据,真正决定像素最终颜色的是片元着色器。就像摆位师傅摆好零件后会传一张“要涂蓝”的小纸条给上色师傅,但喷漆的活还是上色师傅干。
为何常见:初学者看到“顶点着色器输出颜色”的示例,误以为输出即最终结果,忽略了顶点数据还要经历光栅化插值才会变成像素颜色。
误区既然顶点着色器和几何着色器都在处理顶点,应该都能增加或删除顶点吧?
正解顶点着色器是严格的 1:1——一个顶点进、一个顶点出,不能增删;几何着色器是 1:N——一个图元进,可输出 0 个、1 个或多个图元。两者定位完全不同:顶点着色器是“摆位”,几何着色器是“装配、拆改”。
为何常见:两个着色器都能操作顶点数据,名字又相近,初学者很容易混淆它们输入输出的粒度。
误区计算着色器那么强且通用,不如直接用它在屏幕上画像素,取代片元着色器。
正解计算着色器默认没有绑定渲染目标,也不经历光栅化,不能像片元着色器那样直接输出到帧缓冲。想显示计算结果,必须把结果写到纹理或缓冲区,再经由图形流水线(如全屏四边形加片元着色器)显示出来。它更像二楼实验室——算得再好,成品也得送回主流水线包装。
为何常见:“通用计算等于万能”的直觉,加上看到 Compute Shader 也能写纹理,误以为它接管了渲染管线的一切。
误区Unity 的 Shader 文件里写 Properties、Pass,这应该是一门叫 ShaderLab 的新编程语言,我得先学它才能写 Unity 的 Shader。
正解ShaderLab 是管理外壳(声明式组织文件),真正的计算代码用的还是 HLSL(或 GLSL/Cg)。你可以理解为:ShaderLab 是“车间管理规章”(怎么排产、贴标、降级),HLSL 才是“师傅手上的操作指令书”,两者是组织关系,不是并列的两种语言。
为何常见:.shader 文件语法风格迥异,容易让人误以为是独立语言,其实它的代码块内部还是 HLSL。
面试题
Q1. 图形渲染管线中,主要负责把三维顶点坐标变换到屏幕位置的是哪个阶段?
- A.顶点着色器← 正确答案
- B.片元着色器
- C.几何着色器
- D.计算着色器
答案:A。顶点着色器负责逐顶点变换位置(如 MVP 矩阵相乘),就像摆位师傅摆放零件。片元着色器算颜色,几何着色器改图元数量,计算着色器做通用并行计算。
Q2. 片元着色器的核心职责是什么?
- A.矩阵乘法变换顶点坐标
- B.计算每个像素的最终颜色← 正确答案
- C.发射新的三角形图元
- D.调度 GPU 线程组并行
答案:B。片元着色器在光栅化后的每个像素上运行并输出颜色,像上色师傅给每个像素喷漆。A 是顶点着色器的活,C 是几何着色器的活,D 是计算着色器的活。
Q3. 一个顶点进入顶点着色器,最多能输出多少个新顶点?
- A.零到无限多个
- B.输出两个顶点
- C.输出一个顶点← 正确答案
- D.由硬件配置决定
答案:C。顶点着色器是严格的 1:1 变换——一个顶点进、一个顶点出,不能增删。能增删图元的是几何着色器(1:N),两者定位不同。
Q4. HLSL 中语义 SV_Position 的主要作用是什么?
- A.声明纹理采样器类型
- B.标记顶点位置输出← 正确答案
- C.设置线程组的大小
- D.定义材质的混合模式
答案:B。语义是 HLSL 的“数据快递单”,SV_Position 告诉 GPU 这个输出是裁剪空间位置,必须供光栅化使用。纹理采样用 sampler,线程组大小用 numthreads,混合模式在 Blend 状态中设置。
Q5. 几何着色器与顶点着色器最本质的区别是什么?
- A.几何着色器只能在移动端运行
- B.几何着色器输出的是最终像素颜色
- C.几何着色器可发射多个图元← 正确答案
- D.顶点着色器能删除不想要的顶点
答案:C。几何着色器是 1:N——一个图元可发射 0 个或多个新图元(增删改);顶点着色器是 1:1,只能变换不能增删。B 是片元着色器的职责,A 和 D 与事实不符。
Q6. 计算着色器中,如何计算线程的全局唯一编号 globalID?
- A.localID × groupSize + groupID
- B.groupID × groupSize + localID← 正确答案
- C.groupID + localID × groupSize
- D.groupSize + groupID × localID
答案:B。全局编号 = 组编号 × 组大小 + 组内编号,对应“方阵编号 × 每阵人数 + 阵内序号”。A、C、D 把乘数和顺序搞反,会让不同线程组算出重叠编号,导致数据读写冲突。
Q7. 在“玩具工厂”比喻中,几何着色器对应哪类师傅?
- A.摆位师傅只摆零件位置
- B.装配师傅可拆改零件数量← 正确答案
- C.上色师傅逐像素喷漆上色
- D.实验室计算员通用并行计算
答案:B。几何着色器的核心能力是改图元数量——把一个三角形拆成多个或丢弃废料,对应装配师傅。摆位是顶点着色器,上色是片元着色器,通用计算是计算着色器。
Q8. 以下四个 GPU 阶段中,哪些属于可编程着色器阶段?
- A.顶点着色器← 正确答案
- B.光栅化插值
- C.片元着色器← 正确答案
- D.深度测试
答案:AC。可编程阶段是开发者能用 HLSL 写代码的阶段:顶点、几何、片元、计算着色器。光栅化(三角形变像素)和深度测试(判断谁遮挡谁)是固定功能阶段,由硬件完成,不可编程。
Q9. 关于 Unity 的 ShaderLab,下列说法正确的有哪些?
- A.Properties 块暴露材质属性给编辑器← 正确答案
- B.SubShader 可针对不同平台提供多种实现← 正确答案
- C.ShaderLab 是独立于 HLSL 的编程语言
- D.Fallback 指定硬件不兼容时的降级方案← 正确答案
答案:ABD。ShaderLab 是管理外壳:Properties 是属性面板,SubShader 分平台实现,Fallback 写降级 Shader。C 错——ShaderLab 内部的计算代码仍是 HLSL,它是组织外壳而非并列语言。
Q10. 关于计算着色器,以下说法正确的有哪些?
- A.用 numthreads 声明线程组大小← 正确答案
- B.常用于 GPU 粒子系统的位置更新← 正确答案
- C.必须绑定渲染目标才能执行
- D.通过 Dispatch 命令启动调度← 正确答案
答案:ABD。计算着色器通过 Dispatch 调度,用 numthreads 定义每个线程组的线程数,典型应用是 GPU 粒子系统(每线程更新一颗粒子)。C 错:计算着色器与图形管线解耦,默认不绑定渲染目标,结果需先写回缓冲区或纹理再显示。