需要预先了解:


1. 程序结构

在着色器中你已经见过一段完整的 GLSL 代码。这篇笔记把它的语法规则系统梳理一遍。一个 GLSL 程序由版本声明、全局声明(变量、结构体、函数)和入口函数 main 组成:

#version 330 core

layout (location = 0) in vec3 aPos;    // 全局变量声明

out vec4 FragColor;                    // 输出变量

void main()                            // 入口函数
{
    gl_Position = vec4(aPos, 1.0);
}

几个语法层面的硬性规则:

  1. #version 声明必须出现在文件第一行(前面只允许空白和注释),且要先于任何其他声明
  2. 入口函数是 main,返回 void,不接受参数
  3. 每条语句以分号结尾,注释风格与 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

注意三条规则:

  1. 一次选择最多 4 个分量
  2. 一次选择中不能重复同一个分量——color.xx 非法,写入时尤其如此
  3. 不能混用两套名字——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,但有几点显著差异,初学时容易踩坑:

  1. 没有指针、没有 new/delete、没有标准库——内存管理全部交给 GPU 驱动
  2. 向量和矩阵是一等公民,自带运算符与大量数学函数
  3. 有一套独特的限定符体系(in/out/uniform/layout),对应 GPU 数据流
  4. 不允许递归,数组没有越界检查,整数除法等行为与 C 相同但精度受 GPU 限制