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 | — | 布尔值 |
int | 32-bit | 有符号整数 |
uint | 32-bit | 无符号整数 |
float | 32-bit | 单精度浮点 |
double | 64-bit | 双精度浮点(需要 #version 400+) |
4.3.2 向量类型
| 类型 | 分量 | 常用场景 |
|---|---|---|
vec2 | 2 × float | UV 坐标、2D 位置 |
vec3 | 3 × float | 3D 位置、法线、RGB 颜色 |
vec4 | 4 × float | 齐次坐标、RGBA 颜色 |
ivec2/3/4 | int 版 | 整数坐标、索引 |
uvec2/3/4 | uint 版 | 无符号整数 |
bvec2/3/4 | bool 版 | 条件组合 |
dvec2/3/4 | double 版 | 高精度计算 |
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 分量 |
|---|---|---|---|---|
| 位置 | x | y | z | w |
| 颜色 | r | g | b | a |
| 纹理 | s | t | p | q |
4.3.4 矩阵类型
| 类型 | 大小 | 说明 |
|---|---|---|
mat2 | 2×2 | 2D 变换 |
mat3 | 3×3 | 3D 旋转/法线变换 |
mat4 | 4×4 | 完整 3D 变换(最常用) |
mat2x3 | 2列×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 m中m[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++ 函数 |
|---|---|
float | glUniform1f(loc, v) |
vec2 | glUniform2f(loc, v0, v1) |
vec3 | glUniform3f(loc, v0, v1, v2) |
vec4 | glUniform4f(loc, v0, v1, v2, v3) |
int / sampler2D | glUniform1i(loc, v) |
mat4 | glUniformMatrix4fv(loc, 1, GL_FALSE, ptr) |
mat3 | glUniformMatrix3fv(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_Position | vec4 | 必须设置:裁剪空间坐标 |
gl_PointSize | float | 点的像素大小(GL_POINTS 模式) |
gl_VertexID | int | 当前顶点的索引(只读) |
4.5.2 片段着色器输入
| 变量 | 类型 | 说明 |
|---|---|---|
gl_FragCoord | vec4 | 片段的窗口坐标 (x, y, z, 1/w) |
gl_FrontFacing | bool | 是否正面 |
gl_PointCoord | vec2 | 点精灵内的坐标 (0~1) |
gl_FragDepth | float | 可写:自定义深度值 |
// 片段着色器:基于窗口坐标的渐变效果
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 布局规则
| 类型 | 对齐 | 大小 |
|---|---|---|
float | 4 | 4 |
vec2 | 8 | 8 |
vec3 | 16 | 12 |
vec4 | 16 | 16 |
mat4 | 16 | 64 |
| 标量数组 | 16 per element | 16 × 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 Shaders | GLSL 片段着色器创意教程 |
| Shadertoy | 在线 GLSL 编辑与分享平台 |
| GLSL SandBox | 另一个在线 GLSL 编辑器 |
| Inigo Quilez 的文章 | 2D/3D SDF 与程序化纹理 |
本章小结
- GLSL 是类 C 的强类型语言,原生支持向量和矩阵
in/out/uniform是三种核心数据传递方式- 着色器阶段间通过同名
out→in变量传递,光栅化时自动插值 - Uniform 从 CPU 设置,全局有效;UBO 可在多个程序间共享
- SSBO 提供更大的可读写缓冲,适合数据密集计算
- 内建函数覆盖数学、向量、纹理采样等常用操作
- Swizzling 是 GLSL 的独特语法糖,方便分量重排
上一章:第 3 章:OpenGL 基础 下一章:第 5 章:纹理映射