GLSL 着色器编程:从入门到高级技巧

系统讲解 GLSL 着色器编程全栈:从基础语法、顶点/片段着色器,到几何/计算/细分着色器,深入 Uniform Block、SSBO、SPIR-V 互转,覆盖 OpenGL 3.3/4.5/ES 3.0 核心差异。

概述

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 可输出 pointsline_striptriangle_strip;输入 triangles 则不能输出 pointsmax_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 Memoryshared 变量驻留在芯片上的 LDS(Local Data Share),延迟远低于全局显存,适合 tile-based 算法。
  • Barrier / Memory Barrierbarrier() 同步同组内所有 invocation;memoryBarrier* 系列保证内存顺序。
  • 原子操作atomicAddatomicMinatomicCAS 等,用于无锁并行计数与规约。

六、细分着色器(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 的三大优势:

  1. 容量远大于 UBO(通常可达 GPU 显存上限,而 UBO 多为 64KB)。
  2. 支持 shader 读写(buffer 修饰符),可用于 append/consume 结构。
  3. 布局更灵活,支持 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 locationlayout(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 仍是成熟且高性能的方案。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「graphics」更多文章

  1. OpenGL VAO/VBO/EBO 与顶点管理:高效顶点数据传输
  2. OpenGL Core Profile 与管线:现代OpenGL编程指南
  3. Vulkan 高级渲染技术:延迟渲染、光追与计算着色器