初识 GLSL
从一个程序开始
在进入正式的创作之前,我们先来简单了解一下 GLSL 的相关语法。GLSL 使用类似 C 的语法,但是包含了一些额外特性。现在,让我们从一个包含了顶点和片元部分的简单着色器开始来认识一下它们。
先从顶点部分开始:
文件开头一行的
指定了着色器所使用的 GLSL 版本和配置。类似 #version 这样以 # 开头的就是宏命令 ,我们将在之后的章节具体介绍它们。其中 330 表示使用 3.30 的 GLSL,对应 OpenGL 3.3.0;而 core 则表示我们使用核心配置。在本教程中,若无必要,我们都将使用这个版本,它包含大多数现代特性,不会过于老旧,还能在一些有点年头的设备上运行。
接着的两行以 uniform 开头的语句
声明了从 OpenGL 上下文传入的统一变量, 不可以在着色器内被修改 。在这里,它们的类型是 mat4 ,表示这是一个
紧跟着的 in 和 out 开头的语句
定义了传入变量和传出变量。对于顶点着色器来说, in 修饰的变量是顶点属性, out 修饰的变量可以被几何着色器或片元着色器接受。 in 变量不可更改,在传出时对应的变量会在每个顶点之间进行插值使其平滑过渡。其中, vaPosition 是顶点的局部坐标 1, vaColor 是顶点的颜色。
和 C 一样,GLSL 执行时从 main 函数开始,但是它必须是 void 类型,也不接受任何输入。
以 gl_ 开头的变量都是 GLSL 的内建变量, gl_Position 表示顶点的最终位置。主函数的第一行
将传入的 vaPosition 转换为四维向量,并将多出来的第四分量赋上 1.0 (你会在第二章了解到为什么需要这样做),然后和传入的两个矩阵相乘,并赋值给了 gl_Position ,OpenGL 期望这个值落在坐标区间为
最后,我们将顶点传入的颜色属性 vaColor 赋值给传出变量 vColor
以便将其传出到之后的程序中,传入变量无法同时设置为传出。
[1] 有关这些矩阵和坐标的相关内容,我们将在几何缓冲章节详细介绍。
[2] 如果不进行透视矫正,几何根据顶点的数据贴上纹理之后看起来就像从三角形交界处向不同方向进行了扭曲,最终画面观感会大打折扣。早期的游戏主机比如 PlayStation 1 上的顶点数据就没有进行透视矫正,这被称为仿射变换 (Affine Transformation)。 
接着是片元着色器:
和顶点着色器一样,片元着色器也需要声明 GLSL 版本。然后我们将顶点着色器传出的变量 vColor 传入,注意保证类型和变量名的一致,如果顶点着色器中对应的传出变量是不插值的 flat out ,则片元着色器中应该使用 flat in。
接着我们使用 out 修饰了另一个变量 fragColor ,在片元着色器中,被 out 修饰的变量会输出当前片元的数据到目标缓冲区对应的位置上。
最后,在主函数中,我们给输出变量赋上了顶点颜色。GL 在读取普通位图到缓冲区时会将颜色值从
[3] 仅默认情况或使用归一化的缓冲区时 GL 才会主动约束值域,我们将在下一章认识缓冲区。
[4] 目前广泛使用的 8bit 色深屏幕下 RGB 通道最多可以容纳总共
至此,着色器的流程大致结束。
GLSL 和 OpenGL 的版本对应
GLSL 版本与 OpenGL 密切相关,如果平台支持的 OpenGL 版本过低,则无法使用高版本的 GLSL 和它们的新特性。
GLSL | GL | 变动 |
|---|---|---|
110 | 2.0 | 基本功能,支持顶点和片元着色器。 |
120 | 2.1 | 引入 |
130 | 3.0 | 移除 |
140 | 3.1 | 支持整数和位运算。 |
150 | 3.2 | 引入几何着色器(Geometry Shader)。 |
330 | 3.3 | 引入核心模式,移除固定功能管线。 |
400 | 4.0 | 引入细分着色器(Tessellation Shader)。 |
410 | 4.1 | 支持显式顶点属性位置( |
420 | 4.2 | 支持图像加载/存储(Image Load/Store),可以对标量进行 Swizzle 操作。 |
430 | 4.3 | 引入计算着色器(Compute Shader)。 |
440 | 4.4 | 支持显式绑定点(Explicit Binding Points)。 |
450 | 4.5 | 支持直接状态访问(Direct State Access)。 |
版本与配置
在定义 #version 时,我们可以在版本号后添加 core 或 compatibility 来启用特定的配置。
使用 core 会完全禁用固定管线功能,大多数变量都需要用户在 OpenGL 上下文中显式提供,像我们之前的顶点着色器程序中的
就在干这个事情。同时,大多数 gl_ 开头的内建变量都将被禁用,因为它们都来自固定管线。当然,也有部分内建变量得到了保留,参阅 核心配置中可用的内建变量
若你选择了 compatibility 配置,可以直接使用 gl_ModelViewMatrix 和 gl_ProjectionMartix, vec4(vaPosition, 1.0) 也可以直接用 gl_Vertex 代替:
甚至可以直接使用 ftransform() 函数直接它们:
但这些内容都在固定管线中进行,我们对其的掌控能力较弱,还会造成一些不必要的开销。OptiFine 给我们提供了较为全面的变量,所以将我们尽量使用 core 配置。部分对应的变量需要 JE 1.17 及之后的 OptiFine 版本才能提供。
语法糖
GLSL 相较 C 额外支持一些便利语法,可以帮助我们更快捷地编程。
简化构造向量和矩阵
GLSL 允许在构造向量和矩阵时使用标量直接构造,或者在构造时混合使用标量、矢量和矩阵。
GLSL 允许使用多个向量快速构造矩阵。
GLSL 默认使用列主序构造和存储矩阵,参与构造的每个向量被称为列向量。其内存布局是线性代数中矩阵表达的转置,按照先列后行进行索引。如无说明,之后的教程中数学公式都使用常规的行主序矩阵, GLSL 代码都使用列主序矩阵。
例如,对于一个
在 GLSL 中的构造矩阵和存储方式为:
同时,GLSL 还允许简化构造对角矩阵。
以及将高维向量的额外维度丢弃并转化为低维向量。
操作分量
GLSL 允许 Swizzle 式和数组式提取分量。
可以使用 xyzw、 rgba 和 stpq 中的任意一组进行 Swizzle 操作,但是不能在同一个操作中混用。它们的语义通常分别表示空间坐标、 颜色和纹理坐标 ,正确地选择后缀组可以降低代码的阅读和维护门槛。
此外,GL 允许在构造向量时混用 Swizzle 和其他方法
矩阵可以混用数组索引和 Swizzle 操作,按其在内存中的布局进行索引:
向量和矩阵乘法
向量与矩阵、矩阵与矩阵之间的乘法可以直接使用 * 进行运算,但是向量与向量、向量与标量之间使用 * 时默认进行四则运算。
若想进行向量间的点乘或叉乘,可以使用 dot() 和 cross()
类型转换函数
GLSL 不支持 C 式类型转换,转而使用类型函数进行转换。
也可以将标量直接转换到向量:
数组和结构体初始化函数
GLSL 支持直接使用结构体名和数组类型函数进行初始化。
编译时计算常量
GLSL 支持在编译时计算常量表达式和函数以节省运行时性能。
若向常量函数传入变量,该函数会自动降级为运行时计算;若函数的参数显式声明了常量,传入变量将会报错。
将下列函数和变量两两组合时:
对应函数调用的返回结果为:
内建函数
除了上述特性,GLSL 还内置了许多方便的函数,你可以在 这里 查阅。
其中也包含了大多数 C 中 math 库的函数。
函数重载
GLSL 支持函数重载,即定义多个同名但不同参的函数,在调用时将会根据传参类型选择对应函数。
类型
GLSL 内置了布尔类型(bool ),同时支持整型向量和布尔向量(ivec、 bvec)。
GLSL 不支持 static 关键字和 char 类型。
GLSL 中没有无符号修饰符 unsigned ,取而代之的是无符号整数类型 uint 和 uvec。