概述
GLSL(OpenGL Shading Language)是 WebGPU 之前最流行的 GPU 着色语言,也是 Vulkan SPIR-V 的生身之本。本文从语法基础出发,依次覆盖五大着色器阶段(顶点、几何、细分、片段、计算),并深入 UBO、SSBO、SPIR-V 互转以及主流版本差异,帮助读者建立对 GLSL 生态的完整认知。
现代图形管线将 CPU 与 GPU 的协作拆分为若干独立的可编程阶段,每个阶段通过 GLSL 编写的着色器控制执行逻辑。OpenGL 4.5 及以上版本还引入了直接状态访问(DSA)和原子操作,使得多线程渲染与大场景管理不再受 API 绑定点数的限制。
一、GLSL 语法基础
1.1 基本类型与向量矩阵
GLSL 的类型系统围绕 GPU SIMD 架构设计,向量与矩阵是核心原生类型。
// 标量 / 向量
double d = 1.0LF;
vec2 uv = vec2(0.5, 0.5);
vec4 color = vec4(1.0, 0.0, 0.0, 1.0);
ivec3 grid = ivec3(gl_GlobalInvocationID.xyz);
// 矩阵(列主序)
mat4 model = mat4(1.0); // 单位矩阵
mat3 rot = mat3(cosA, sinA, 0.0,
-sinA, cosA, 0.0,
0.0, 0.0, 1.0);
// 纹理与采样器
uniform sampler2D diffuseTex;
uniform sampler3D volumeTex;
uniform samplerCube envMap;
注意:mat4 采用列主序存储,与 OpenGL 的 glUniformMatrix4fv 第二个参数 GL_TRUE/GL_FALSE 对应,在 C++ 侧需特别注意内存布局。
1.2 存储修饰符与布局限定符
| 修饰符 | 含义 | 适用场景 |
|---|---|---|
in / out | 阶段间输入输出 | 顶点→几何→片段的数据传递 |
uniform | 全局只读常量 | 变换矩阵、材质参数、纹理采样器 |
buffer | 可读写 Shader Storage Buffer | 大规模粒子系统、间接绘制命令 |
shared | 工作组内共享内存 | 计算着色器中的 tile 缓存 |
const | 编译期常量 | 数组长度、循环边界 |
layout(location = 0) | 显式绑定位置 | 避免驱动自动分配导致的不一致 |
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
layout(location = 0) out vec4 FragColor;
layout(std140, binding = 0) uniform Transform {
mat4 model;
mat4 view;
mat4 proj;
} trans;
std140 是统一缓冲的标准内存布局,它对齐规则严格但可跨平台;若追求更高硬件利用率,OpenGL 4.2+ 支持 std430(类似 SSBO 的紧凑布局)。
1.3 内置变量概览
| 变量名 | 阶段 | 说明 |
|---|---|---|
gl_Position | 顶点/细分/几何输出 | 裁剪空间的顶点坐标 |
gl_VertexID | 顶点输入 | 当前顶点索引(含 base vertex) |
gl_FragCoord | 片段输入 | 屏幕空间像素坐标,z 为深度值 |
gl_FragDepth | 片段输出 | 可覆盖的深度值(需 layout(depth_*)) |
gl_PrimitiveID | 几何/片段输入 | 当前图元编号 |
gl_InvocationID | 细分控制/几何 | 当前 invocation 索引 |
gl_GlobalInvocationID | 计算 | 全局工作项三维 ID |
gl_NumWorkGroups | 计算 | 调度时传入的 work group 数量 |
二、顶点着色器(Vertex Shader)
顶点着色器是每个图元处理的入口,其职责是读取顶点属性、执行坐标变换,并输出下一阶段所需数据。
#version 450 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;
layout(location = 2) in vec2 aTexCoord;
layout(binding = 0) uniform Camera {
mat4 view;
mat4 proj;
vec3 cameraPos;
float nearPlane;
} camera;
layout(binding = 1) uniform Model {
mat4 model;
mat3 normalMatrix; // 避免非均匀缩放时用 inverse(transpose(model))
} model;
out VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
vec3 ViewDir;
} vs_out;
void main() {
vec4 worldPos = model.model * vec4(aPos, 1.0);
vs_out.FragPos = worldPos.xyz;
vs_out.Normal = model.normalMatrix * aNormal;
vs_out.TexCoord = aTexCoord;
vs_out.ViewDir = normalize(camera.cameraPos - worldPos.xyz);
gl_Position = camera.proj * camera.view * worldPos;
}
C++ 侧绑定 Uniform Block 的示例:
// 创建 UBO
glCreateBuffers(1, &uboCamera);
glNamedBufferStorage(uboCamera, sizeof(CameraData),
nullptr, GL_DYNAMIC_STORAGE_BIT);
// 绑定到 binding point
glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboCamera);
// 更新数据(DSA 风格)
glNamedBufferSubData(uboCamera, 0, sizeof(CameraData), &cameraData);
核心要点:顶点着色器无邻接信息,只能通过顶点属性传递数据。如需平滑法线,通常在建模阶段将每个顶点的法线计算为共享面法线的加权平均。
三、片段着色器(Fragment Shader)
片段着色器决定像素最终输出颜色,是现代光照计算的核心战场。
#version 450 core
in VS_OUT {
vec3 FragPos;
vec3 Normal;
vec2 TexCoord;
vec3 ViewDir;
} fs_in;
layout(binding = 2) uniform sampler2D diffuseMap;
layout(binding = 3) uniform sampler2D normalMap;
layout(binding = 4) uniform sampler2D metallicRoughnessMap;
layout(binding = 5) uniform Material {
vec4 baseColorFactor;
float metallicFactor;
float roughnessFactor;
float emissiveStrength;
float alphaCutoff;
} material;
layout(location = 0) out vec4 FragColor;
layout(location = 1) out vec4 GBufferNormal;
vec3 SampleNormal(vec2 uv) {
vec3 norm = texture(normalMap, uv).rgb * 2.0 - 1.0;
// 重建 TBN 矩阵略...
return normalize(/* TBN * norm */);
}
void main() {
vec4 baseSample = texture(diffuseMap, fs_in.TexCoord) * material.baseColorFactor;
// Alpha 测试(无需 blending 时高效剔除透明像素)
if (baseSample.a < material.alphaCutoff)
discard;
vec3 N = normalize(fs_in.Normal);
vec3 V = normalize(fs_in.ViewDir);
// 简化的 PBR 漫反射项
float NdotV = max(dot(N, V), 0.0);
vec3 diff = baseSample.rgb * (1.0 - material.metallicFactor);
FragColor = vec4(diff * NdotV, baseSample.a);
GBufferNormal = vec4(N * 0.5 + 0.5, 0.0);
}
此处演示了多输出(MRT)、Alpha 裁剪、纹理采样与基础 PBR 漫反射。discard 关键字会阻止该片元写入任何颜色附件,常用于 foliage 与 cutout 渲染;注意频繁使用 discard 会干扰 Early-Z 优化。
四、几何着色器(Geometry Shader)
几何着色器位于顶点和片段阶段之间,可动态创建或销毁图元,典型用途包括:单 Pass Billboarding、动态轮廓线、点精灵扩展。
#version 450 core
layout(points) in;
layout(triangle_strip, max_vertices = 4) out;
layout(binding = 0) uniform Camera {
mat4 viewProj;
} cam;
layout(binding = 1) uniform BillBoardParams {
float size;
int textureLayer;
} params;
out GS_OUT {
vec2 TexCoord;
flat int Layer;
} gs_out;
const vec2 corners[4] = vec2[](
vec2(-1, -1), vec2( 1, -1),
vec2(-1, 1), vec2( 1, 1)
);
const vec2 uvs[4] = vec2[](
vec2(0, 0), vec2(1, 0),
vec2(0, 1), vec2(1, 1)
);
void main() {
vec3 center = gl_in[0].gl_Position.xyz;
// 从 viewProj 逆推 view 的右/上方向(用于面向相机)
vec3 right = vec3(cam.viewProj[0][0], cam.viewProj[1][0], cam.viewProj[2][0]);
vec3 up = vec3(cam.viewProj[0][1], cam.viewProj[1][1], cam.viewProj[2][1]);
for (int i = 0; i < 4; ++i) {
vec3 offset = (corners[i].x * right + corners[i].y * up) * params.size;
gl_Position = cam.viewProj * vec4(center + offset, 1.0);
gs_out.TexCoord = uvs[i];
gs_out.Layer = params.textureLayer;
EmitVertex();
}
EndPrimitive();
}
几何着色器的输出图元类型必须与输入兼容:输入 points 可输出 points 、line_strip 或 triangle_strip;输入 triangles 则不能输出 points。max_vertices 指定了单 invocation 最大扩增顶点数,其值直接影响 GPU 并行调度粒度。
五、计算着色器(Compute Shader)
计算着色器脱离图形管线,直接运行在 GPU 通用计算单元上,是后处理、粒子模拟、剔除、物理求解的首选。
#version 450 core
layout(local_size_x = 256, local_size_y = 1, local_size_z = 1) in;
layout(binding = 0, std430) readonly buffer InputParticles {
vec4 positions[]; // xyz = pos, w = lifetime
} inParticles;
layout(binding = 1, std430) writeonly buffer OutputParticles {
vec4 positions[];
} outParticles;
layout(binding = 2, std430) coherent buffer CounterBuffer {
uint aliveCount;
} counter;
layout(binding = 3) uniform SimulationParams {
float dt;
uint maxParticles;
vec3 gravity;
} sim;
shared vec3 sVelocities[256];
void main() {
uint gid = gl_GlobalInvocationID.x;
if (gid >= sim.maxParticles) return;
vec4 p = inParticles.positions[gid];
if (p.w <= 0.0) {
// 死亡粒子,通过 atomicAdd 回收索引到 append buffer
return;
}
// 共享内存协作:预取相邻速度
sVelocities[gl_LocalInvocationID.x] = vec3(0.0, 1.0, 0.0);
barrier(); // 工作组内同步
memoryBarrierShared();
p.xyz += sim.gravity * sim.dt;
p.w -= sim.dt;
outParticles.positions[gid] = p;
if (p.w > 0.0) {
// 原子计数存活粒子以保证后续 dispatch indirect 使用
atomicAdd(counter.aliveCount, 1u);
}
}
计算着色器的核心概念:
- Work Group:三维线程组,可通过
glDispatchCompute(numGroupsX, Y, Z)调度。 - Local Size:每个 work group 内的线程数,三者乘积通常不超过 1024(硬件差异大)。
- Shared Memory:
shared变量驻留在芯片上的 LDS(Local Data Share),延迟远低于全局显存,适合 tile-based 算法。 - Barrier / Memory Barrier:
barrier()同步同组内所有 invocation;memoryBarrier*系列保证内存顺序。 - 原子操作:
atomicAdd、atomicMin、atomicCAS等,用于无锁并行计数与规约。
六、细分着色器(Tessellation Shader)
Tessellation 通过控制阶段(Tessellation Control Shader, TCS)和求值阶段(Tessellation Evaluation Shader, TES)动态将低模细分成高模,广泛用于地形、Displacement Mapping、曲面细分。
6.1 控制着色器(TCS)
#version 450 core
layout(vertices = 3) out;
layout(binding = 0) uniform TessParams {
float innerLevel;
float outerLevel;
} tess;
in vec3 vPos[];
out vec3 tcPos[];
void main() {
tcPos[gl_InvocationID] = vPos[gl_InvocationID];
if (gl_InvocationID == 0) {
gl_TessLevelInner[0] = tess.innerLevel;
gl_TessLevelOuter[0] = tess.outerLevel;
gl_TessLevelOuter[1] = tess.outerLevel;
gl_TessLevelOuter[2] = tess.outerLevel;
}
}
6.2 求值着色器(TES)
#version 450 core
layout(triangles, equal_spacing, ccw) in;
layout(binding = 1) uniform sampler2D heightMap;
layout(binding = 2) uniform mat4 mvp;
in vec3 tcPos[];
out vec3 tePos;
out vec2 teUV;
void main() {
vec3 p0 = gl_TessCoord.x * tcPos[0];
vec3 p1 = gl_TessCoord.y * tcPos[1];
vec3 p2 = gl_TessCoord.z * tcPos[2];
vec3 pos = p0 + p1 + p2;
teUV = pos.xz * 0.1;
float height = texture(heightMap, teUV).r * 64.0;
pos.y += height;
tePos = pos;
gl_Position = mvp * vec4(pos, 1.0);
}
细分模式:
equal_spacing / fractional_even_spacing / fractional_odd_spacing—— 决定细分因子的插值方式。triangles / quads / isolines—— 输入 patch 类型与输出图元模式。- TCS 的
vertices = N表示 patch 含 N 个控制点,TCS 按 invocation 执行 N 次,但内层通过gl_InvocationID区分。
七、Uniform Block 与 SSBO
7.1 Uniform Block (UBO)
UBO 将多个 uniform 打包成一个缓冲对象,减少驱动状态切换开销,并支持同一 buffer 多 draw call 复用。
layout(std140, binding = 0) uniform SceneData {
mat4 viewProj;
mat4 invViewProj;
vec3 cameraPos;
float time;
vec3 sunDirection;
float sunIntensity;
vec3 ambientColor;
float exposure;
} scene;
7.2 Shader Storage Buffer Object (SSBO)
SSBO 相比 UBO 的三大优势:
- 容量远大于 UBO(通常可达 GPU 显存上限,而 UBO 多为 64KB)。
- 支持 shader 读写(
buffer修饰符),可用于 append/consume 结构。 - 布局更灵活,支持
std430紧密排布以及运行时大小数组。
layout(std430, binding = 0) buffer DrawCommandBuffer {
uint count;
uint instanceCount;
uint firstIndex;
uint baseVertex;
uint baseInstance;
} drawCommands[];
layout(std430, binding = 1) buffer VisibilityBuffer {
uint visible[];
} vis;
C++ 侧使用 DSA 创建 SSBO:
GLuint ssbo;
glCreateBuffers(1, &ssbo);
glNamedBufferStorage(ssbo, visDataSize, visData,
GL_DYNAMIC_STORAGE_BIT);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 1, ssbo);
八、GLSL 编译为 SPIR-V
SPIR-V 是 Khronos 定义的标准中间语言,Vulkan 的着色器入口、OpenGL 4.6 的 glShaderBinary,以及 WebGPU 的 WGSL 都基于它。
8.1 使用 glslangValidator
# 顶点着色器 → SPIR-V
glslangValidator -V -S vert -o shader.vert.spv shader.vert
# 片段着色器 → SPIR-V
glslangValidator -V -S frag -o shader.frag.spv shader.frag
# 计算着色器 → SPIR-V
glslangValidator -V -S comp -o shader.comp.spv shader.comp
8.2 OpenGL 加载 SPIR-V
// OpenGL 4.6 起原生支持
glShaderBinary(1, &shader, GL_SHADER_BINARY_FORMAT_SPIR_V,
spvData, spvSize);
glSpecializeShader(shader, "main", 0, nullptr, nullptr);
glAttachShader(program, shader);
glLinkProgram(program);
Vulkan 与 OpenGL 4.6+ 支持 SPIR-V 二进制直接注入,省去运行时驱动编译 GLSL 的时间,且便于实现着色器管线缓存。glSpecializeShader 的第二个参数是 SPIR-V 入口点名称,默认 GLSL 编译后为 main。
九、版本差异对比
| 特性 | OpenGL 3.3 (GLSL 330) | OpenGL 4.5 (GLSL 450) | OpenGL ES 3.0 (GLSL ES 300) |
|---|---|---|---|
| 核心 profile | 有 | 有 / DSA | 只有 core(无兼容性) |
| 计算着色器 | 无 | 有 | 有(ES 3.1) |
| 细分着色器 | 无 | 有 | 无 |
| 几何着色器 | 无(通过扩展) | 有 | 无 |
| UBO | 有 | 有 | 有 |
| SSBO | 无(4.3+) | 有(4.3+) | 有(ES 3.1) |
| SPIR-V | 无(4.6+) | 有(4.6+) | 无 |
| 显式 uniform location | 无 | layout(location = N) | layout(location = N) |
| 内存模型 / barrier | 基础 | 完整(含 memory model) | 有限 |
| 精度修饰符 | 可选 | 可选 | 必须(highp/mediump/lowp) |
GLSL ES 的重要约束:必须显式指定精度(如 precision highp float;),不支持几何/细分着色器,输入输出仍需 layout(location = N),且纹理采样函数参数有所差异。
版本声明头:
#version 330 core
#version 450 core
#version 300 es // OpenGL ES 3.0
十、小结
GLSL 作为 GPU 编程的事实标准之一,贯穿 OpenGL 从 2.0 到 4.6 的完整演进。本文系统梳理了五大着色器阶段的写法与职责,深入讲解了 UBO/SSBO 的内存布局与上限差异,演示了计算着色器中的屏障与原子操作,并覆盖了 SPIR-V 互转流程。最后以版本对照表总结了 330/450/ES 300 的核心差异,方便开发者在不同平台与硬件代际间做出技术选型。
当应用场景从单机桌面延伸至移动端与 Web 时,建议优先选用 GLSL ES 3.0/300 或 SPIR-V 交叉编译到 WGSL,以获得更广泛的部署能力。对于需要曲面细分与大场景剔除的 3A 级渲染管线,OpenGL 4.5 + 计算着色器 + SSBO indirect draw 仍是成熟且高性能的方案。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。