强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

OpenGL / OpenCL 编程指南 / 第 4 章:GLSL 着色语言

第 4 章:GLSL 着色语言

GLSL(OpenGL Shading Language)是编写着色器的语言,语法类似 C 但拥有大量面向图形计算的内建特性。本章全面覆盖 GLSL 的核心语法和常用函数。


4.1 GLSL 概述

GLSL 是一种类 C 的强类型语言,专为 GPU 并行计算设计。它的关键特性:

  • 向量和矩阵是原生类型(不像 C 需要手动实现)
  • 输入/输出通过变量声明(而非函数参数)
  • 每个着色器阶段有特定的输入/输出限定符
  • 内建函数覆盖数学、纹理采样、几何运算等
特性C/C++GLSL
向量类型无原生支持vec2, vec3, vec4
矩阵类型无原生支持mat2, mat3, mat4
纹理采样需库支持内建 texture()
函数重载支持支持
指针支持❌ 不支持
递归支持❌ 不支持
文件 I/O支持❌ 不支持
动态内存支持❌ 不支持

4.2 版本声明

#version 460 core    // OpenGL 4.6, Core Profile
#version 430         // OpenGL 4.3
#version 330 core    // OpenGL 3.3 (兼容性最好)
#version 300 es      // OpenGL ES 3.0 (移动端/WebGL 2.0)
#version 100         // OpenGL ES 2.0 (WebGL 1.0)

⚠️ #version 必须是着色器文件的第一行(前面不能有空行或注释)。


4.3 数据类型

4.3.1 标量类型

类型大小说明
bool布尔值
int32-bit有符号整数
uint32-bit无符号整数
float32-bit单精度浮点
double64-bit双精度浮点(需要 #version 400+)

4.3.2 向量类型

类型分量常用场景
vec22 × floatUV 坐标、2D 位置
vec33 × float3D 位置、法线、RGB 颜色
vec44 × float齐次坐标、RGBA 颜色
ivec2/3/4int 版整数坐标、索引
uvec2/3/4uint 版无符号整数
bvec2/3/4bool 版条件组合
dvec2/3/4double 版高精度计算

4.3.3 向量访问(Swizzling)

vec4 color = vec4(1.0, 0.5, 0.2, 1.0);

// 分量访问
float r = color.x;      // 或 color.r 或 color.s 或 color[0]
float g = color.y;      // 或 color.g 或 color.t 或 color[1]
float b = color.z;      // 或 color.b 或 color.p 或 color[2]
float a = color.w;      // 或 color.a 或 color.q 或 color[3]

// Swizzle 组合
vec3 rgb = color.rgb;       // (1.0, 0.5, 0.2)
vec2 rg  = color.rg;        // (1.0, 0.5)
vec3 bgr = color.bgr;       // 反转顺序 (0.2, 0.5, 1.0)
vec4 rr  = color.rrrr;      // (1.0, 1.0, 1.0, 1.0)

分量命名约定

用途第 1 分量第 2 分量第 3 分量第 4 分量
位置xyzw
颜色rgba
纹理stpq

4.3.4 矩阵类型

类型大小说明
mat22×22D 变换
mat33×33D 旋转/法线变换
mat44×4完整 3D 变换(最常用)
mat2x32列×3行非方阵
// 创建单位矩阵
mat4 identity = mat4(1.0);

// 逐元素构造
mat2 m = mat2(
    1.0, 0.0,   // 第一列
    0.0, 1.0    // 第二列
);

// 访问列(GLSL 是列主序!)
vec4 col0 = identity[0];  // 第一列
float m01 = identity[0][1]; // 第一列第二行

⚠️ GLSL 矩阵是列主序(Column-Major),这与 C/C++ 中常见的行主序不同。mat4 mm[0] 是第一列,不是第一行。


4.4 变量限定符

4.4.1 存储限定符

限定符含义示例
const编译时常量const float PI = 3.14159;
in输入变量(从上一阶段或顶点属性)in vec3 aPos;
out输出变量(传递给下一阶段或帧缓冲)out vec4 FragColor;
uniform全局统一变量(CPU 端设置)uniform mat4 model;
buffer着色器存储缓冲(可读写)buffer Data { ... };
shared计算着色器工作组共享内存shared float data[128];

4.4.2 uniform 变量详解

uniform 是从 CPU 传递数据到 GPU 的主要方式之一:

// 顶点着色器中声明
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
uniform float time;
uniform vec3 lightColor;
// C++ 端设置
glUseProgram(shaderProgram);

// 获取 uniform 位置
int modelLoc = glGetUniformLocation(shaderProgram, "model");

// 设置值
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(modelMatrix));
glUniform1f(glGetUniformLocation(shaderProgram, "time"), glfwGetTime());
glUniform3f(glGetUniformLocation(shaderProgram, "lightColor"), 1.0f, 1.0f, 1.0f);

Uniform 设置函数对照表

GLSL 类型C++ 函数
floatglUniform1f(loc, v)
vec2glUniform2f(loc, v0, v1)
vec3glUniform3f(loc, v0, v1, v2)
vec4glUniform4f(loc, v0, v1, v2, v3)
int / sampler2DglUniform1i(loc, v)
mat4glUniformMatrix4fv(loc, 1, GL_FALSE, ptr)
mat3glUniformMatrix3fv(loc, 1, GL_FALSE, ptr)

⚠️ Uniform 的默认值:如果 CPU 端没有设置 uniform,其值为 0(int/float)或零向量/零矩阵。不会报错,但渲染结果可能不对。

4.4.3 in / out 传递

着色器阶段间的数据通过 in / out 传递:

// ===== 顶点着色器 =====
#version 460 core

layout (location = 0) in vec3 aPos;   // 从 VBO 输入
layout (location = 1) in vec3 aColor;

out vec3 vColor;     // 输出到片段着色器
out vec2 vTexCoord;

uniform mat4 mvp;

void main() {
    gl_Position = mvp * vec4(aPos, 1.0);
    vColor = aColor;
    vTexCoord = aPos.xy * 0.5 + 0.5;
}
// ===== 片段着色器 =====
#version 460 core

in vec3 vColor;      // 从顶点着色器插值输入
in vec2 vTexCoord;

out vec4 FragColor;  // 输出到帧缓冲

uniform sampler2D ourTexture;

void main() {
    vec4 texColor = texture(ourTexture, vTexCoord);
    FragColor = mix(texColor, vec4(vColor, 1.0), 0.5);
}

数据流

VBO → [aPos, aColor] → 顶点着色器 → [vColor, vTexCoord]
                           │
                      光栅化插值
                           │
                           ▼
                     片段着色器 → FragColor → 帧缓冲

4.5 常用内建变量

4.5.1 顶点着色器输出

变量类型说明
gl_Positionvec4必须设置:裁剪空间坐标
gl_PointSizefloat点的像素大小(GL_POINTS 模式)
gl_VertexIDint当前顶点的索引(只读)

4.5.2 片段着色器输入

变量类型说明
gl_FragCoordvec4片段的窗口坐标 (x, y, z, 1/w)
gl_FrontFacingbool是否正面
gl_PointCoordvec2点精灵内的坐标 (0~1)
gl_FragDepthfloat可写:自定义深度值
// 片段着色器:基于窗口坐标的渐变效果
void main() {
    vec2 uv = gl_FragCoord.xy / vec2(800.0, 600.0);
    FragColor = vec4(uv, 0.5, 1.0);
}

4.6 内建函数

4.6.1 数学函数

函数说明
abs(x)绝对值
sign(x)符号 (-1, 0, 1)
floor(x)向下取整
ceil(x)向上取整
fract(x)小数部分
mod(x, y)取模
min(x, y) / max(x, y)最小/最大值
clamp(x, lo, hi)限制在 [lo, hi] 范围
mix(x, y, a)线性插值: x*(1-a) + y*a
step(edge, x)阶跃函数: x < edge ? 0 : 1
smoothstep(e0, e1, x)平滑阶跃(Hermite 插值)

4.6.2 三角函数

函数说明
sin(x), cos(x), tan(x)标准三角函数
asin(x), acos(x), atan(x)反三角函数
radians(deg)角度转弧度
degrees(rad)弧度转角度

4.6.3 向量/矩阵函数

函数说明
length(v)向量长度
distance(p0, p1)两点距离
dot(a, b)点积
cross(a, b)叉积(仅 vec3)
normalize(v)归一化
reflect(I, N)反射向量
refract(I, N, eta)折射向量
matrixCompMult(A, B)矩阵逐分量乘法
transpose(M)矩阵转置
inverse(M)矩阵求逆
determinant(M)行列式

4.6.4 纹理采样函数

函数说明
texture(sampler, coord)2D 纹理采样
texture(sampler, coord, bias)带 LOD 偏移的采样
textureLod(sampler, coord, lod)指定 LOD 级别采样
textureGrad(sampler, coord, dPdx, dPdy)指定梯度采样
texelFetch(sampler, icoord, lod)整数坐标采样(无过滤)
textureSize(sampler, lod)获取纹理尺寸

4.6.5 常用技巧示例

// 渐变混合
vec3 skyBlue = vec3(0.5, 0.7, 1.0);
vec3 sunset = vec3(1.0, 0.5, 0.2);
vec3 result = mix(skyBlue, sunset, timeOfDay);  // timeOfDay: 0~1

// 边缘发光(Fresnel)
float fresnel = pow(1.0 - dot(normalize(viewDir), normal), 3.0);

// 伪随机(哈希)
float random(vec2 st) {
    return fract(sin(dot(st, vec2(12.9898, 78.233))) * 43758.5453123);
}

// 平滑过渡
float edge = smoothstep(0.45, 0.55, uv.x);  // 在 0.45~0.55 之间平滑过渡

// 色调映射(Reinhard)
vec3 mapped = hdrColor / (hdrColor + vec3(1.0));

4.7 控制流

// 条件分支(与 C 相同)
if (condition) {
    // ...
} else {
    // ...
}

// for 循环(循环次数必须是编译时可确定的常量表达式)
for (int i = 0; i < 4; i++) {
    // ...
}

// while 循环(有限次)
int i = 0;
while (i < 10) {
    i++;
}

// discard(片段着色器专用:丢弃当前片段)
if (alpha < 0.1) {
    discard;  // 不写入帧缓冲
}

⚠️ GPU 不擅长分支:同一 warp/wavefront(32/64 线程)中的线程必须执行相同的代码路径。分支会导致"两路都走"(divergence),降低并行效率。


4.8 结构体与数组

// 结构体
struct Material {
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
    float shininess;
};

uniform Material material;

// 在着色器中使用
vec3 diffuse = material.diffuse * lightColor * max(dot(normal, lightDir), 0.0);

// 数组
uniform vec3 lightPositions[4];
uniform vec3 lightColors[4];

for (int i = 0; i < 4; i++) {
    // 处理每个光源...
}

4.9 Uniform Buffer Object (UBO)

当多个着色器程序共享相同的 uniform 数据时,UBO 避免了重复设置:

// 共享的 uniform 块
layout (std140, binding = 0) uniform SharedData {
    mat4 projection;
    mat4 view;
    vec3 cameraPos;
    float time;
};
// C++ 端:创建 UBO
unsigned int ubo;
glGenBuffers(1, &ubo);
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferData(GL_UNIFORM_BUFFER, 128, NULL, GL_STATIC_DRAW);  // 预分配
glBindBuffer(GL_UNIFORM_BUFFER, 0);

// 绑定到绑定点 0
glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo);

// 更新数据
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);

std140 布局规则

类型对齐大小
float44
vec288
vec31612
vec41616
mat41664
标量数组16 per element16 × N
结构体最大成员对齐向上取整到 vec4 边界

⚠️ 使用 std140 布局时必须严格按照规则计算偏移,否则数据会错位。std430 更紧凑但不能用于 UBO(只用于 SSBO)。


4.10 Shader Storage Buffer Object (SSBO)

SSBO 比 UBO 更灵活:可读写、容量更大(GB 级),适合大量数据:

// 片段着色器中写入 SSBO(需要 GL 4.3+)
layout (std430, binding = 0) buffer Histogram {
    uint bins[256];
};

void main() {
    uint luminance = uint(dot(FragColor.rgb, vec3(0.299, 0.587, 0.114)) * 255.0);
    atomicAdd(bins[luminance], 1u);  // 原子操作
}

4.11 预处理器

// 宏定义
#define MAX_LIGHTS 4
#define USE_NORMAL_MAP

#ifdef USE_NORMAL_MAP
    vec3 normal = texture(normalMap, texCoord).rgb * 2.0 - 1.0;
#else
    vec3 normal = vNormal;
#endif

// 条件编译
#if VERSION >= 430
    // 使用高级特性
#else
    // 回退方案
#endif

// 行号指令(调试用)
#line 100

4.12 完整示例:动态波浪效果

顶点着色器

// shaders/wave_vertex.glsl
#version 460 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 vTexCoord;
out float vHeight;

uniform float time;
uniform mat4 mvp;

void main() {
    vec3 pos = aPos;

    // 波浪效果
    float wave1 = sin(pos.x * 2.0 + time) * 0.1;
    float wave2 = cos(pos.z * 3.0 + time * 0.7) * 0.05;
    pos.y += wave1 + wave2;

    gl_Position = mvp * vec4(pos, 1.0);
    vTexCoord = aTexCoord;
    vHeight = pos.y;
}

片段着色器

// shaders/wave_fragment.glsl
#version 460 core

in vec2 vTexCoord;
in float vHeight;

out vec4 FragColor;

void main() {
    // 基于高度的颜色映射
    vec3 deepWater = vec3(0.0, 0.1, 0.4);
    vec3 shallowWater = vec3(0.0, 0.5, 0.8);
    vec3 foam = vec3(0.9, 0.95, 1.0);

    float t = clamp(vHeight * 5.0 + 0.5, 0.0, 1.0);
    vec3 color = mix(deepWater, shallowWater, t);

    // 泡沫(在波峰处)
    if (vHeight > 0.08) {
        color = mix(color, foam, smoothstep(0.08, 0.12, vHeight));
    }

    FragColor = vec4(color, 0.9);
}

4.13 注意事项

⚠️ GLSL 不会自动初始化变量。未赋值的局部变量包含未定义值(垃圾数据),必须手动初始化。

⚠️ 精度问题float 在 GPU 上通常只有 23-bit 尾数(约 7 位有效数字)。对于需要高精度的计算(如世界坐标原点远离原点),使用相对坐标或 double

⚠️ 版本不匹配:着色器的 #version 必须与 OpenGL 上下文版本匹配。请求 GL 3.3 上下文就不能用 #version 460

⚠️ Uniform 查找性能glGetUniformLocation 涉及字符串查找,建议在初始化时缓存位置值,不要每帧调用。


4.14 业务场景

场景 1:自定义材质系统

通过 Uniform 传递 Material 结构体,实现 PBR(物理基础渲染)材质切换。

场景 2:屏幕后处理

全屏四边形 + 片段着色器实现模糊、色彩校正、边缘检测等效果。

场景 3:程序化纹理

用 GLSL 数学函数生成木纹、大理石、噪声纹理,避免加载外部图片。


4.15 扩展阅读

资源说明
GLSL 规范 (4.6)官方语言规范
The Book of ShadersGLSL 片段着色器创意教程
Shadertoy在线 GLSL 编辑与分享平台
GLSL SandBox另一个在线 GLSL 编辑器
Inigo Quilez 的文章2D/3D SDF 与程序化纹理

本章小结

  • GLSL 是类 C 的强类型语言,原生支持向量和矩阵
  • in/out/uniform 是三种核心数据传递方式
  • 着色器阶段间通过同名 outin 变量传递,光栅化时自动插值
  • Uniform 从 CPU 设置,全局有效;UBO 可在多个程序间共享
  • SSBO 提供更大的可读写缓冲,适合数据密集计算
  • 内建函数覆盖数学、向量、纹理采样等常用操作
  • Swizzling 是 GLSL 的独特语法糖,方便分量重排

上一章第 3 章:OpenGL 基础 下一章第 5 章:纹理映射