OpenGL ES 的渲染管线包含有一个可编程的顶点阶段和一个可编程的片段阶段。其余的阶段则有固定的功能,应用程序对其行为的控制非常有限。每个可编程阶段中编译单元的集合组成了一个着色器。在OpenGL ES 2.0 中,每个着色器只支持一个编译单元。着色程序则是一整套编译好并链接在一起的着色器的集合。着色器 shader 的编写需要使用着色语言 GL Shader Language(GLSL),GLSL 的语法与 C 语言很类似。
// 顶点着色器 .vsh
attribute vec4 position;
attribute vec4 color;
varying vec4 colorVarying;
void main(void) {
colorVarying = color;
gl_Position = position;
}
// 片段着色器 .fsh
varying lowp vec4 colorVarying;
void main(void) {
gl_FragColor = colorVarying;
}
习惯上,我们一般把顶点着色器命名为 xx.vsh,片段着色器命名为 xx.fsh。当然,你喜欢怎么样就怎么样~
和 C 语言程序对应,用 GLSL 写出的着色器,它同样包括:
- 变量 position
- 变量类型 vec4
- 限定符 attribute
- main 函数
- 基本赋值语句 colorVarying = color
- 内置变量 gl_Position
这一切,都是那么像…所以,在掌握 C 语言的基础上,GLSL 的学习成本是很低的。
变量
变量及变量类型
变量类别 | 变量类型 | 描述 |
---|---|---|
空 | void | 用于无返回值的函数或空的参数列表 |
标量 | float, int, bool | 浮点型,整型,布尔型的标量数据类型 |
浮点型向量 | float, vec2, vec3, vec4 | 包含1,2,3,4个元素的浮点型向量 |
整数型向量 | int, ivec2, ivec3, ivec4 | 包含1,2,3,4个元素的整型向量 |
布尔型向量 | bool, bvec2, bvec3, bvec4 | 包含1,2,3,4个元素的布尔型向量 |
矩阵matrix | mat2, mat3, mat4 | 尺寸为2x2,3x3,4x4的浮点型矩阵 |
纹理句柄 | sampler2D, samplerCube | 表示2D,立方体纹理的句柄 |
除上述之外,着色器中还可以将它们构成数组或结构体,以实现更复杂的数据类型。
GLSL中没有指针类型。
变量构造器和类型转换
对于变量运算,GLSL 中有非常严格的规则,即只有类型一致时,变量才能完成赋值或其它对应的操作。可以通过对应的构造器来实现类型转换。
标量
标量对应 C 语言的基础数据类型,它的构造和 C 语言一致,如下:
float myFloat = 1.0;
bool myBool = true;
myFloat = float(myBool); // bool -> float
myBool = bool(myFloat); // float -> bool
向量
当构造向量时,向量构造器中的各参数将会被转换成相同的类型(浮点型、整型或布尔型)。往向量构造器中传递参数有两种形式:
- 如果向量构造器中只提供了一个标量参数,则向量中所有值都会设定为该标量值。
- 如果提供了多个标量值或提供了向量参数,则会从左至右使用提供的参数来给向量赋值,如果使用多个标量来赋值,则需要确保标量的个数要多于向量构造器中的个数。
向量构造器用法如下:
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0}
vec3 myVec3 = vec3(1.0, 0.0, 0.5); // myVec3 = {1.0, 0.0, 0.5}
vec3 temp = vec3(myVec3); // temp = myVec3
vec2 myVec2 = vec2(myVec3); // myVec2 = {myVec3.x, myVec3.y}
myVec4 = vec4(myVec2, temp, 0.0); // myVec4 = {myVec2.x, myVec2.y , temp, 0.0 }
矩阵
矩阵的构造方法则更加灵活,有以下规则:
- 如果对矩阵构造器只提供了一个标量参数,该值会作为矩阵的对角线上的值。例如 mat4(1.0) 可以构造一个 4 × 4 的单位矩阵
- 矩阵可以通过多个向量作为参数来构造,例如一个 mat2 可以通过两个 vec2 来构造
- 矩阵可以通过多个标量作为参数来构造,矩阵中每个值对应一个标量,按照从左到右的顺序
除此之外,矩阵的构造方法还可以更灵活,只要有足够的组件来初始化矩阵,其构造器参数可以是标量和向量的组合。在 OpenGL ES 中,矩阵的值会以列的顺序来存储。在构造矩阵时,构造器参数会按照列的顺序来填充矩阵,如下:
mat3 myMat3 = mat3(1.0, 0.0, 0.0, // 第一列
0.0, 1.0, 0.0, // 第二列
0.0, 1.0, 1.0); // 第三列
向量和矩阵的分量
单独获得向量中的组件有两种方法:即使用 “.” 符号或使用数组下标方法。依据构成向量的组件个数,向量的组件可以通过 {x, y, z, w} , {r, g, b, a} 或 {s, t, r, q} 等 swizzle 操作来获取。之所以采用这三种不同的命名方法,是因为向量常常会用来表示数学向量、颜色、纹理坐标等。其中的x、r、s 组件总是表示向量中的第一个元素,如下表:
分量访问符 | 符号描述 |
---|---|
(x,y,z,w) | 与位置相关的分量 |
(r,g,b,a) | 与颜色相关的分量 |
(s,t,p,q) | 与纹理坐标相关的分量 |
不同的命名约定是为了方便使用,所以哪怕是描述位置的向量,也是可以通过 {r, g, b, a} 来获取。但是在使用向量时不能混用不同的命名约定,即不能使用 .xgr 这样的方式,每次只能使用同一种命名约定。当使用 “.” 操作符时,还可以对向量中的元素重新排序,如下:
vec3 myVec3 = vec3(0.0, 1.0, 2.0); // myVec3 = {0.0, 1.0, 2.0}
vec3 temp;
temp = myVec3.xyz; // temp = {0.0, 1.0, 2.0}
temp = myVec3.xxx; // temp = {0.0, 0.0, 0.0}
temp = myVec3.zyx; // temp = {2.0, 1.0, 0.0}
除了使用 “.” 操作符之外,还可以使用数组下标操作。在使用数组下标操作时,元素 [0] 对应的是 x,元素 [1] 对应 y,以此类推。值得注意的是,在 OpenGL ES 2.0 中的某些情况下,数组下标不支持使用非常数的整型表达式(如使用整型变量索引),这是因为对于向量的动态索引操作,某些硬件设备处理起来很困难。在 OpenGL ES 2.0 中仅对 uniform 类型的变量支持这种动态索引。
矩阵可以认为是向量的组合。例如一个 mat2 可以认为是两个 vec2,一个 mat3 可以认为是三个 vec3 等等。对于矩阵来说,可以通过数组下标 “[]” 来获取某一列的值,然后获取到的向量又可以继续使用向量的操作方法,如下:
mat4 myMat4 = mat4(1.0); // Initialize diagonal to 1.0 (identity)
vec4 col0 = myMat4[0]; // Get col0 vector out of the matrix
float m1_1 = myMat4[1][1]; // Get element at [1][1] in matrix
float m2_2 = myMat4[2].z; // Get element at [2][2] in matrix
向量和矩阵的操作
绝大多数情况下,向量和矩阵的计算是逐分量进行的(component-wise)。当运算符作用于向量或矩阵时,该运算独立地作用于向量或矩阵的每个分量。
以下是一些示例:
vec3 v, u;
float f;
v = u + f;
等价于:
v.x = u.x + f;
v.y = u.y + f;
v.z = u.z + f;
再如:
vec3 v, u, w;
w = v + u;
等价于:
w.x = v.x + u.x;
w.y = v.y + u.y;
w.z = v.z + u.z;
对于整型和浮点型的向量和矩阵,绝大多数的计算都同上,但是对于向量乘以矩阵、矩阵乘以向量、矩阵乘以矩阵则是不同的计算规则。这三种计算使用线性代数的乘法规则,并且要求参与计算的运算数值有相匹配的尺寸或阶数。
例如:
vec3 v, u;
mat3 m;
向量乘以矩阵
u = v * m;
等价于:
u.x = dot(v, m[0]); // m[0] is the left column of m
u.y = dot(v, m[1]); // dot(a,b) is the inner (dot) product of a and b
u.z = dot(v, m[2]);
再如:
矩阵乘以向量
u = m * v;
等价于:
u.x = m[0].x * v.x + m[1].x * v.y + m[2].x * v.z;
u.y = m[0].y * v.x + m[1].y * v.y + m[2].y * v.z;
u.z = m[0].z * v.x + m[1].z * v.y + m[2].z * v.z;
再如:
矩阵乘以矩阵
mat m, n, r;
r = m * n;
等价于:
r[0].x = m[0].x * n[0].x + m[1].x * n[0].y + m[2].x * n[0].z;
r[1].x = m[0].x * n[1].x + m[1].x * n[1].y + m[2].x * n[1].z;
r[2].x = m[0].x * n[2].x + m[1].x * n[2].y + m[2].x * n[2].z;
r[0].y = m[0].y * n[0].x + m[1].y * n[0].y + m[2].y * n[0].z;
r[1].y = m[0].y * n[1].x + m[1].y * n[1].y + m[2].y * n[1].z;
r[2].y = m[0].y * n[2].x + m[1].y * n[2].y + m[2].y * n[2].z;
r[0].z = m[0].z * n[0].x + m[1].z * n[0].y + m[2].z * n[0].z;
r[1].z = m[0].z * n[1].x + m[1].z * n[1].y + m[2].z * n[1].z;
r[2].z = m[0].z * n[2].x + m[1].z * n[2].y + m[2].z * n[2].z;
对于2阶和4阶的向量或矩阵也是相似的规则。
结构体
与 C 语言相似,除了基本的数据类型之外,还可以将多个变量聚合到一个结构体中
struct customStruct
{
vec4 color;
vec2 position;
} customVertex;
首先,定义会产生一个新的类型叫做 customStruct ,及一个名为 customVertex 的变量。结构体可以用构造器来初始化,在定义了新的结构体之后,还会定义一个与结构体类型名称相同的构造器。构造器与结构体中的数据类型必须一一对应,如下:
customVertex = customStruct(vec4(0.0, 1.0, 0.0, 0.0), // color
vec2(0.5, 0.5)); // position
结构体的构造器是基于类型的名称,以参数的形式来赋值。获取结构体内元素的方法和C语言中一致:
vec4 color = customVertex.color;
vec4 position = customVertex.position;
数组
除了结构体外,GLSL 中还支持数组。 语法与 C 语言相似,创建数组的方式如下代码所示:
float floatArray[4];
vec4 vecArray[2];
与C语言不同,在GLSL中,关于数组有两点需要注意:
- 除了uniform 变量之外,数组的索引只允许使用常数整型表达式。
- 在GLSL 中不能在创建的同时给数组初始化,即数组中的元素需要在定义数组之后逐个初始化,且数组不能使用 const 限定符。