GLSL 语法
需要预先了解:
1. 程序结构
在着色器中你已经见过一段完整的 GLSL 代码。这篇笔记把它的语法规则系统梳理一遍。一个 GLSL 程序由版本声明、全局声明(变量、结构体、函数)和入口函数 main 组成:
#version 330 core
layout (location = 0) in vec3 aPos; // 全局变量声明
out vec4 FragColor; // 输出变量
void main() // 入口函数
{
gl_Position = vec4(aPos, 1.0);
}
几个语法层面的硬性规则:
#version声明必须出现在文件第一行(前面只允许空白和注释),且要先于任何其他声明- 入口函数是
main,返回void,不接受参数 - 每条语句以分号结尾,注释风格与 C 相同(
//与/* */)
注:
#version 330 core中的core表示核心模式(core profile)。OpenGL 3.2 起把旧版兼容特性移到了 compatibility profile,核心模式只保留现代 API,所以不能使用attribute、varying等旧关键字。
2. 数据类型
2.1 标量类型
GLSL 的标量类型与 C 语言基本一致:
| 类型 | 说明 |
|---|---|
float |
32 位浮点数,GPU 的主要运算类型 |
double |
64 位浮点数,GLSL 4.0 起支持 |
int |
32 位有符号整数 |
uint |
32 位无符号整数 |
bool |
布尔值 |
浮点字面量必须有小数点或指数部分:1.0、.5、1e3 都是合法的 float,而 1 是整数。float x = 1; 会直接编译报错。
2.2 向量类型
向量是 GLSL 最常用的类型,由 2~4 个同类型分量组成:
vec2/vec3/vec4— float 向量ivec2~ivec4— int 向量uvec2~uvec4— uint 向量bvec2~bvec4— bool 向量dvec2~dvec4— double 向量
向量可以直接参与逐分量运算(component-wise),这是它与 C++ 中普通结构体的本质区别:
vec3 a = vec3(1.0, 2.0, 3.0);
vec3 b = a * 2.0; // vec3(2.0, 4.0, 6.0),标量与向量混合运算
vec3 c = a + b; // vec3(3.0, 6.0, 9.0)
bool eq = (a == b); // bvec3(false, false, false),比较也是逐分量的
2.3 矩阵类型
矩阵类型有 mat2、mat3、mat4,以及行列任意组合的 mat2x3(2 列 3 行)等。矩阵按列优先(column-major)存储,下标 m[0] 取的是第 0 列(一个 vec4):
mat4 m = mat4(1.0); // 对角线为 1.0 的单位矩阵
mat3 m2 = mat3(vec3(1.0), vec3(2.0), vec3(3.0)); // 按列构造
vec4 v = m * vec4(1.0, 0.0, 0.0, 1.0); // 矩阵乘向量
mat4 r = m * m2; // 矩阵乘矩阵
* 是线性代数乘法(行乘列)。如果要做逐分量乘法,需要用内置函数 matrixCompMult。
2.4 数组
数组语法与 C 类似,但长度必须在编译期确定(GLSL 3.30;GLSL 4.0 起部分场景允许用 uniform 指定长度):
float weights[4];
vec3 positions[2] = vec3[2](vec3(0.0), vec3(1.0)); // 数组构造语法
float w = weights[0]; // 下标访问
weights[1] = 2.0;
数组可以作为函数参数,也可以作为结构体成员。注意 GLSL 数组没有越界检查,越界访问是未定义行为。
2.5 结构体
结构体(struct)的语法与 C 相同,把多个变量打包成一个整体,常配合 in/out 变量跨着色器阶段传递复杂数据:
struct Light
{
vec3 position;
vec3 color;
float intensity;
};
Light sun;
sun.position = vec3(0.0, 10.0, 0.0);
2.6 采样器
采样器(sampler)类型用于在着色器中访问纹理,它属于不透明类型(opaque type)——不能做算术运算,只能声明为 uniform 或作为函数参数:
uniform sampler2D tex; // 2D 纹理
uniform samplerCube envMap; // 立方体贴图
uniform sampler2DArray atlas; // 纹理数组
uniform sampler2DShadow depth; // 阴影贴图(配合 PCF 使用)
采样器绑定的纹理单元(texture unit)由 CPU 端 glActiveTexture 与 glUniform1i 指定。
3. 分量访问与运算
3.1 分量选择(swizzle)
向量的分量可以用三套名字访问:位置 x/y/z/w、颜色 r/g/b/a、纹理坐标 s/t/p/q。同一套名字内可以任意组合和重排:
vec4 color = vec4(1.0, 0.5, 0.2, 1.0);
float r = color.r; // 1.0
vec2 rg = color.rg; // vec2(1.0, 0.5)
vec3 bgr = color.bgr; // vec3(0.2, 0.5, 1.0),允许重排
color.bgr = color.rgb; // 交换 r 和 b
注意三条规则:
- 一次选择最多 4 个分量
- 一次选择中不能重复同一个分量——
color.xx非法,写入时尤其如此 - 不能混用两套名字——
color.xr非法
3.2 常用向量函数
| 函数 | 作用 |
|---|---|
dot(a, b) |
点积,结果是标量 |
cross(a, b) |
叉积,结果是向量(仅 vec3) |
length(v) |
向量长度 |
distance(a, b) |
两点间距离 |
normalize(v) |
单位化向量 |
reflect(i, n) |
反射向量(高光计算用) |
refract(i, n, eta) |
折射向量 |
4. 变量限定符
4.1 存储限定符
| 限定符 | 作用 |
|---|---|
const |
编译期常量,声明时必须初始化 |
in |
从上一个阶段接收数据(顶点阶段则从 VBO 读取) |
out |
向下一阶段输出数据(片段阶段则写入帧缓冲) |
uniform |
从 CPU 传入的全局常量,一次绘制调用内不变 |
buffer |
着色器存储缓冲(SSBO),GLSL 4.3 起 |
shared |
计算着色器工作组内共享内存 |
in/out 变量按名字和类型匹配:顶点着色器的 out 与片段着色器的 in 同名同类型时,数据在光栅化阶段自动传递。
4.2 插值限定符
从顶点着色器传到片段着色器的变量,在片元之间默认平滑插值(smooth,透视校正)。也可以显式指定其他插值方式:
flat— 不插值,整个图元取同一个值(provoking vertex 的值),适合法线、实例 ID 这类不能线性插值的数据noperspective— 线性插值,不做透视校正
// 顶点着色器
flat out int instanceID;
// 片段着色器
flat in int instanceID;
注意:两端限定符必须一致。一个阶段声明
flat,另一个阶段漏写,链接会失败。
4.3 精度限定符
精度限定符声明浮点运算的精度:highp、mediump、lowp。桌面 GPU 默认全部为 highp,通常不需要写;移动端(GLSL ES)必须声明,因为低精度运算更快更省电:
precision mediump float; // 声明默认精度
lowp vec4 color; // 单个变量指定低精度
4.4 layout 限定符
layout 给变量附加布局信息:
layout (location = 0) in vec3 aPos; // 顶点属性位置,对应 glVertexAttribPointer
layout (location = 0) out vec4 FragColor; // 帧缓冲颜色附着
layout (binding = 0) uniform sampler2D tex; // 直接绑定纹理单元 0
location 也可以不写,由链接器自动分配,再用 glGetAttribLocation / glGetUniformLocation 查询。binding 对采样器直接指定绑定点,省去 CPU 端的 glUniform1i 调用。
4.5 uniform 块
多个 uniform 可以打包成uniform 块(uniform block),对应 CPU 端的 UBO(Uniform Buffer Object)。块内布局用 std140 规则对齐:
layout (std140) uniform Matrices
{
mat4 projection;
mat4 view;
};
// 使用时直接按成员名访问
gl_Position = projection * view * vec4(aPos, 1.0);
同一份矩阵数据可以被多个着色器程序共享,减少 glUniformMatrix4fv 的调用次数。
5. 控制流与函数
5.1 控制流语句
GLSL 的控制流与 C 几乎相同:if/else、for、while、do-while、switch、break/continue/return。
for (int i = 0; i < 4; ++i)
{
sum += weights[i];
}
if (sum > 1.0)
color = vec4(1.0);
else
color = vec4(0.0);
片段着色器还有一个独有的语句 discard:直接丢弃当前片元,不写入颜色和深度:
if (texture(tex, uv).a < 0.5)
discard; // 半透明区域直接丢弃
注:GPU 是 SIMT(单指令多线程)架构。同一个
if分支里,如果并行执行的片元走向不同路径,两条路径都会被执行(结果被屏蔽)。所以不要指望分支能节省执行时间,着色器里应尽量避免大规模动态分支。
5.2 函数
函数定义与 C 类似,但参数有方向限定符:
in— 传值(默认)out— 只写,结果传回调用者inout— 既能读又能写
void addOffset(inout vec3 pos, vec3 offset)
{
pos += offset;
}
vec3 mirrorZ(vec3 v)
{
return vec3(v.x, v.y, -v.z);
}
函数支持重载(按参数类型区分)。但不允许递归——GLSL 规范不保证递归可用,实际硬件也普遍不支持。
6. 内置变量
每个着色器阶段都有一组内置变量(built-in variables),由 GPU 自动填充或消费。
6.1 顶点着色器
| 变量 | 类型 | 方向 | 含义 |
|---|---|---|---|
gl_VertexID |
int |
in | 当前顶点的索引(从 0 开始) |
gl_InstanceID |
int |
in | 当前实例的索引(实例化渲染用) |
gl_Position |
vec4 |
out | 必须赋值,裁剪空间顶点位置 |
gl_PointSize |
float |
out | 点精灵大小(绘制 GL_POINTS 时用) |
6.2 片段着色器
| 变量 | 类型 | 方向 | 含义 |
|---|---|---|---|
gl_FragCoord |
vec4 |
in | 片元在窗口中的坐标(xy 为像素位置,z 为深度) |
gl_FrontFacing |
bool |
in | 片元是否属于图元正面 |
gl_PointCoord |
vec2 |
in | 点精灵内的归一化坐标(0~1) |
gl_FragDepth |
float |
out | 自定义深度值(默认由光栅化决定) |
注:早期 GLSL(1.10)用内置的
gl_FragColor输出颜色,GLSL 1.30 起改为声明自己的out变量(如out vec4 FragColor),更灵活——可以声明多个输出,配合 MRT(多渲染目标)一次绘制写入多张纹理。
6.3 几何着色器(了解)
几何着色器的输入是图元,所以内置输入是一个数组 gl_in[],每个元素包含对应顶点的 gl_Position 等属性:
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
void main()
{
for (int i = 0; i < gl_in.length(); ++i)
{
gl_Position = gl_in[i].gl_Position;
EmitVertex();
}
EndPrimitive();
}
EmitVertex() 和 EndPrimitive() 是几何着色器特有的内置函数,用于逐个发射顶点并结束一个图元。
7. 常用内置函数
7.1 数学函数
float x = abs(-3.0); // 3.0
float f = fract(2.75); // 0.75,取小数部分
float c = clamp(x, 0.0, 1.0); // 限制到 [0, 1]
float m = mix(a, b, t); // 线性插值 a*(1-t) + b*t
float s = smoothstep(0.0, 1.0, t); // Hermite 平滑插值
float p = pow(2.0, 3.0); // 8.0
float r = radians(180.0); // π,角度转弧度
float st = step(0.5, x); // x < 0.5 ? 0 : 1
这些函数都支持向量参数(逐分量运算)。smoothstep 和 mix 在边缘过渡、渐变等效果里非常常用。
7.2 纹理采样函数
现代 GLSL 用统一的 texture 系列函数取代了旧版的 texture2D:
vec4 color = texture(tex, uv); // 基础采样
vec4 color = textureLod(tex, uv, 2.0); // 指定 mipmap 层级
vec4 color = textureProj(tex, uv); // 投影纹理(uv 为 vec3/vec4,自动除以 w)
vec4 texel = texelFetch(tex, ivec2(10, 20), 0); // 直接取纹素,不做插值
int w = textureSize(tex, 0).x; // 纹理宽度
texture 根据采样器类型自动选择重载:sampler2D 用 vec2 坐标、samplerCube 用 vec3 方向、sampler2DShadow 返回 float 阴影因子(配合ShadowMap与PCF、PCSS中的 PCF 使用)。
8. 与 C 的差异总结
GLSL 语法脱胎于 C,但有几点显著差异,初学时容易踩坑:
- 没有指针、没有
new/delete、没有标准库——内存管理全部交给 GPU 驱动 - 向量和矩阵是一等公民,自带运算符与大量数学函数
- 有一套独特的限定符体系(
in/out/uniform/layout),对应 GPU 数据流 - 不允许递归,数组没有越界检查,整数除法等行为与 C 相同但精度受 GPU 限制



