可编程着色器与 GPU 管线
Programmable Shaders & GPU Pipeline
GPU 把第 2 章的概念管线实现为一组硬件阶段,分为完全可编程、可配置但不可编程、完全固定三类。现代 GPU 采用统一着色器架构:各类着色器共用同一套编程模型和核心,由 GPU 自行分配负载。着色器用 HLSL、GLSL 等类 C 语言编写,输入分为 uniform 和 varying 两类。
GPU 管线的各阶段
下图是 GPU 的逻辑管线,虚线框是可选阶段。
flowchart LR
VS["顶点着色器"] --> TS["曲面细分"]:::opt --> GS["几何着色器"]:::opt --> CL["裁剪"] --> SM["屏幕映射"] --> TR["三角形设置与遍历"] --> PS["像素着色器"] --> MG["合并"]
classDef opt stroke-dasharray: 5 5| 阶段 | 可控程度 |
|---|---|
| 顶点着色器、曲面细分着色器、几何着色器、像素着色器 | 完全可编程 |
| 屏幕映射(由窗口和视口设置决定)、合并阶段 | 可配置,不可编程 |
| 裁剪、三角形设置、三角形遍历 | 固定功能 |
曲面细分和几何着色器都是可选的,不是所有 GPU 都支持,移动设备尤其如此。
逻辑模型 vs 物理模型
这里讲的是 API 暴露给程序员的逻辑模型,硬件厂商怎么实现是另一回事:逻辑上固定的阶段可能被并入相邻的可编程阶段执行,一个着色器也可能被拆到不同单元甚至单独一个 pass 执行。逻辑模型能帮你推理哪些因素影响性能,但不要把它当成 GPU 的真实实现。
统一着色器架构 (Unified Shader Architecture)
- 顶点、像素、几何、曲面细分着色器共用同一套编程模型和指令集 (ISA)。DirectX 把这种处理器叫通用着色器核心 (common-shader core)。
- 好处是负载均衡:大量细小三角形需要更多顶点处理,大三角形需要更多像素处理。如果顶点核心和像素核心是分开的两池,分配比例就被固定死了;统一核心可以由 GPU 按需分配。
着色语言与编译
- 常用 HLSL(DirectX)和 GLSL(OpenGL),都是类 C 语言。
- HLSL 可以编译成与硬件无关的中间语言(IL 或 DXIL),也能离线编译保存;驱动再把它转换成具体 GPU 的 ISA。
- 主机开发通常跳过中间语言,因为只有一种 ISA。
- 基本数据类型是 32 位浮点标量和向量(向量只存在于着色器代码层面,硬件不一定直接支持),现代 GPU 也原生支持 32 位整数和 64 位浮点。
输入类型
一次 draw call 调用图形 API 绘制一组图元,从而驱动管线运行着色器。
| 输入 | 特点 | 例子 |
|---|---|---|
| uniform | 一次 draw call 内保持不变,draw call 之间可改 | 光源颜色 |
| varying | 来自三角形顶点或光栅化,每个顶点或像素不同 | 表面位置 |
| 纹理 | 一种特殊的 uniform,如今可以看作任意大型数据数组 | 贴图、查找表 |
uniform 只存一份、所有顶点或像素共用,所以常量寄存器的数量远多于 varying 的输入输出寄存器。
流程控制
| 类型 | 依据 | 特点 |
|---|---|---|
| 静态流程控制 | uniform 的值 | 整个 draw call 走同一路径,没有线程分歧;适合让同一个着色器适应不同情况,比如不同数量的光源 |
| 动态流程控制 | varying 的值 | 每个片元可以走不同路径,更强大,但路径变化剧烈时代价很高(见 GPU 并行架构 的线程分歧) |
可编程着色和 API 的演进
| 时间 | 事件 |
|---|---|
| 1984 | Cook 提出 shade tree,可编程着色的雏形,后来发展出 RenderMan 着色语言 |
| 1996 | 3dfx Voodoo,固定功能管线 |
| 1999 | NVIDIA GeForce 256,第一个叫"GPU"的芯片,可配置但不可编程 |
| 2001 | GeForce 3,第一个支持可编程顶点着色器的 GPU(DirectX 8.0),类汇编语言 |
| 2002 | DirectX 9.0 / Shader Model 2.0,真正可编程的顶点和像素着色器,推出 HLSL;OpenGL 推出 GLSL |
| 2004 | Shader Model 3.0,动态流程控制 |
| 2006 | DirectX 10 / Shader Model 4.0,几何着色器、流输出、统一编程模型、整数类型;Wii 是最后一台主要的固定功能机器 |
| 2009 | DirectX 11 / Shader Model 5.0,曲面细分着色器、计算着色器 |
| 2013 | AMD Mantle,低开销 API 的开端 |
| 2014 | Apple Metal |
| 2015 | DirectX 12 |
| 2016 | Vulkan,使用中间语言 SPIR-V |
低开销 API
Mantle、DirectX 12、Vulkan、Metal 这一代 API 的重点不是新的 GPU 功能,而是大幅减少 CPU 花在驱动上的时间,并更好地支持 CPU 多线程。适合 CPU 驱动开销成为瓶颈的场景,在移动设备上还能省电。但从旧 API 移植很难,直接照搬的实现反而可能更慢。
移动端和 Web
- OpenGL ES:为嵌入式和移动设备设计。ES 2.0(2007)开始可编程,ES 3.0(2012)加入多渲染目标、纹理压缩、实例化等,ES 3.1 加计算着色器,ES 3.2 加几何和曲面细分着色器。
- WebGL:通过 JavaScript 调用,WebGL 1 相当于 OpenGL ES 2.0,WebGL 2 相当于 ES 3.0。跨平台、只需文本编辑器、浏览器自带调试器,非常适合做实验和教学;three.js 这类库提供了阴影、后处理、PBR、延迟渲染等现成实现。
在游戏开发中的意义
- 用 uniform 驱动的静态分支做"着色器变体",比在逐像素数据上分支安全得多。
- 选 API 时的取舍:DX12 和 Vulkan 能降低 CPU 开销、更好地多线程,但开发和移植成本高;如果瓶颈不在 CPU 驱动,收益有限。
GEA 的视角:着色器模型与 HLSL 入门(卷 II 11.4.5、11.4.10)
可编程性的演进
- GEA 的时间线:3Dfx Voodoo 只做光栅化、着色和合并,前段交给 CPU;NVIDIA GeForce 256 是第一块"真正的" GPU,把变换和投影也做进硬件,但整条管线可配置不可编程;后来部分阶段变为可编程,又加入曲面细分和几何放大阶段;最近厂商转向以网格着色器和放大着色器为中心的新管线(见 网格着色器管线)(卷 II 11.4.5)。
- 统一着色器:早期顶点着色器指令集比像素着色器复杂,两者是不同的核心,像素核心更简单、更省面积。ATI 为 Xbox 360 引入统一着色器架构,NVIDIA 和 AMD 随后跟进。
与 RTR4 的分歧:GeForce 3 支持哪种可编程着色器
| 来源 | 说法 |
|---|---|
| RTR4(本页「可编程着色和 API 的演进」表) | 2001 年 GeForce 3 是第一个支持可编程顶点着色器的 GPU(DirectX 8.0) |
| GEA 卷 II 11.4.5 | 最早的可编程 GPU GeForce 3 (NV20) 只提供可编程像素着色器,用表达力很有限的汇编编写,可编程顶点着色器"不久之后"才加入 |
两书说法相反,这里并列记录。
原书问题(11.4.5):GEA 的说法不对。GeForce 3(NV20,2001)同时支持 DirectX 8.0 的顶点着色器 1.1 和像素着色器 1.1;它的顶点着色器("nfiniteFX" 引擎中的可编程顶点处理器)反而比像素部分更接近真正的可编程处理器——像素着色器 1.x 只有很少的算术指令,更像可配置的寄存器组合器(Wikipedia "GeForce 3 series";Wikipedia "High-Level Shader Language" 的着色器模型对照表)。RTR4 的表述正确。 另:"ATI 为 Xbox 360 引入统一着色器,NVIDIA 和 AMD 随后跟进"一句中,AMD 在 2006 年收购了 ATI,跟进的 PC 产品是 NVIDIA G80(GeForce 8800,2006 年 11 月)和 ATI/AMD 的 R600(Radeon HD 2900,2007 年),并非 ATI 之外另有一家 AMD(Wikipedia "Unified shader model")。
着色器模型、着色器配置与 caps 位
- 着色器模型 (Shader Model) 是描述着色器能力的版本号;每种着色器还有着色器配置 (shader profile),前缀表示类型,后跟主次版本,如
ps_2_0(像素)、vs_(顶点)、cs_(计算)、gs_(几何)。SM1 到 SM3 能力逐步增强,但指令数、常量和寄存器都很受限,顶点和像素着色器用完全不同的 ISA;从 SM4 开始统一架构,几乎所有着色器共用一套 ISA。 - 早期 PC 游戏靠查询 GPU 的 caps 位(每位对应一个硬件特性)来适配不同硬件,组合繁多,容易出兼容性 bug。DirectX 11 的特性级别把它简化成线性序列,见 图形 API 设备、管线状态与资源。
HLSL 的数据类型
GEA 用 HLSL 讲着色器编程,说明 GLSL、PSSL 等语言大同小异(卷 II 11.4.10):
- 标量:
bool、int、uint、half(16 位浮点)、float、double,以及min10float这类只保证最低位宽的类型。 - 向量
float4(或vector<float,4>)、矩阵float3x4(或matrix<float,3,4>)、纹理与缓冲、纹理采样器、结构体(按值传递,没有指针和引用之分)、typedef。 - 分量可按
xyzw或rgba访问,可重排 (swizzle),如bar.xyz = foo.zxy;。 - 重要区别:着色器里的四元向量不是 SIMD 的四个通道。GPU 着色器跑在 16、32 或 64 通道的 SIMD 核心上,每个通道各运行一次内核;向量运算会被编译成逐分量的串行指令。这与在 CPU 上写 SIMD 代码完全不同(对照 SIMD 向量化)。
动态数据、uniform 数据与语义
- 动态数据:每次调用处理的顶点、图元或片元,类型一致但值各不相同;uniform 数据:一次 draw call 内所有调用看到的相同值,如常量缓冲中的模型-观察矩阵和投影矩阵。这与本页「输入类型」表中的 varying 与 uniform 对应。
- 语义 (semantics) 是给变量和结构体成员加的元信息。早期语义按阶段定义,如
POSITION[n]只能用于顶点着色器、COLOR[n]只能用于像素着色器;DirectX 10 起有以SV_开头的系统值语义,所有阶段都可用。像素着色器只能输出SV_Depth和SV_Target(GEA 原话;不完整:像素着色器还可输出SV_Coverage、SV_StencilRef,以及SV_DepthGreaterEqual/SV_DepthLessEqual等保守深度变体,见 MS Learn "Semantics" 系统值表);SV_VertexID、SV_InstanceID、SV_IsFrontFace只能作为第一个能解释它的阶段的输入,之后各阶段只能原样传递。
一对顶点/像素着色器的结构
书中示例的要点(卷 II 11.4.10.5):
- 两个常量缓冲:每相机常量(
c_viewProj、相机位置)和每物体常量(c_modelToWorld),外加一个采样器和一张漫反射纹理。 - 顶点着色器把模型矩阵与观察投影矩阵相乘,输出裁剪空间位置(
SV_POSITION)和世界空间位置,法线用 w=0 乘模型矩阵后归一化,颜色和 UV 原样传递。HLSL 中写作mul(float4(p,1), M),即 GEA 一贯的行向量约定(见 齐次坐标与矩阵约定)。 - 像素着色器拿到的是三次顶点着色器输出插值后的结果,所以要重新归一化法线(线性插值不保持长度),然后算 Blinn-Phong 光照,乘以纹理颜色和顶点颜色输出到
SV_TARGET。 - 原书问题(11.4.10.5 示例代码):像素着色器调用
BlinnPhong(fragment.position, fragment.normal),但fragment.position带SV_POSITION语义,到像素着色器时已是屏幕空间像素坐标(xy 为像素中心、z 为深度、w 为 ),不是世界空间位置;示例中顶点着色器专门输出的worldPos才是光照需要的位置,应写成BlinnPhong(fragment.worldPos, fragment.normal)(MS Learn "Semantics":SV_Position在像素着色器中描述像素位置)。另外法线用 乘模型矩阵只在没有非均匀缩放时正确,否则需要逆转置矩阵(见 基本仿射变换)。
LearnOpenGL 的视角:编译、链接与 Shader 封装(Getting Started / Shaders)
来源:Shaders;摘录 Getting Started — Shaders。
GLSL 源码在运行时(或离线)经 glCreateShader → glShaderSource → glCompileShader,再 glCreateProgram → glAttachShader → glLinkProgram。编译/链接失败时用 glGetShaderInfoLog / glGetProgramInfoLog 取日志——调试着色器几乎全靠这些日志与「把中间量画成颜色」。
LearnOpenGL 的 Shader 类把路径读入、编译链接、错误打印、use 与一组 setBool/setInt/setFloat(内部 glGetUniformLocation)收进单一对象。这与 GEA/RTR 讨论的「着色器配置 / 材质绑定」同一层抽象,只是示例停在最小可用封装,没有变体排列与反射系统(变体见 着色频率与材质系统)。
出处
- realtime_rendering_4ed.pdf 第 3 章 The Graphics Processing Unit
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 11 章 11.4.5、11.4.10 节
- Getting Started — Shaders
- learnopengl.com/Getting-started/Shaders