本章的内容是完全与设备搭载的操作系统平台无关的,无论是在 Android、 iOS、BlackBerry 还是嵌入式 Linux 上,基于着色语言开发的代码基本都是完全通用,不需要移植的,可以说是做到了“一次开发、到处运行”。
与传统通用编程语言有很大不同的是,其提供了更加丰富的原生类型,如向量、矩阵等。这些特性的加入使得 OpenGL ES 着色语言在处理 3D 图形方面更加高效、易用。简单来说, OpenGLES 着色语言主要包括以下特性。
❏ OpenGL ES 着色语言是一种高级的过程语言(注意,不是面向对象)。
❏ 对顶点着色器、片元着色器使用的是同样的语言,不做区分。
❏ 基于 C/C++的基本语法及流程控制。
❏ 完美支持向量与矩阵的各种操作。
❏ 通过类型限定符来管理输入与输出。
❏ 拥有大量的内置函数来提供丰富的功能
OpenGL ES 着色语言虽然是基于 C/C++基本语法的语言,但是其与 C/C++相比较还是有很大不同的。例如,该语言不支持双精度浮点型(double)、字节型(byte)、短整型(short)、长整型(long),并且取消了 C 中的联合体(union)及枚举类型(enum)等特性。
➊数据类型概述
1.标量
❏ 布尔型——bool
bool b;// 声明一个布尔型变量
❏ 有/无符号整型——int/uint
int a = 5;// 十进制
uint b = 3u;// 无符号十进制
int c = 036;// 八进制
int d = 0x3D;// 十六进制
❏ 浮点型——float
float f;// 声明一个float型变量
float s = 3e2;// 声明变量并赋予指数形式的值,表示3×10^2
2.向量
vec2 v2;// 声明一个vec2类型的向量
ivec3 v3;// 声明一个ivec3类型的向量
uvec3 vu3;// 声明一个uvec3类型的向量
bvec4 v4;// 声明一个bvec4类型的向量
向量在着色器代码的开发中有着十分重要的作用,可以很方便地存储以及操作颜色、位置、纹理坐标等不仅包含一个组成部分的量。开发中,有时也可能需要单独访问向量中的某个分量,基本的语法为“<向量名>.<分量名>” ,根据目的的不同,主要有如下几种用法。
❏ 将一个向量看作颜色时,可以使用 r、 g、 b、 a 四个分量名,其分别代表红、绿、蓝、透明度 4 个色彩通道,具体用法如下。
aColor.r = 0.6;// 给向量aColor的红色通道分量赋值
aColor.g = 0.8;// 给向量aColor的绿色通道分量赋值
若向量是 4 维的, 则可以使用的分量名为: r、 g、 b、 a;若向量是 3 维的,则可以使用的分量名为 r、 g、 b;若为 2 维的,则仅可以使用 r、 g 两个分量名。
❏ 将一个向量看作位置时,可以使用 x、 y、 z、 w 四个分量名,其分别代表 x 轴、 y 轴、 z轴分量及 W 值,具体用法如下。
aPosition.x = 67.2;// 给向量aPosition的X分量赋值
aPosition.z = 48.3;// 给向量aPosition的Z分量赋值
若向量是 4 维的,则可以使用的分量名为: x、 y、 z、 w;若向量为 3 维的,则可以使用的分量名为 x、 y、 z;若向量为 2 维的,则仅可以使用 x、 y 两个分量名。另外,一般只有在使用四维齐次坐标的情况下才会同时使用到 x、 y、 z、 w 这 4 个分量。
❏ 将一个向量看作纹理坐标时,可以使用 s、 t、 p、 q 四个分量名,其分别代表纹理坐标的不同分量,具体用法如下。
aTextCoor.s = 0.65;// 给向量aTextColor的s分量赋值
aTextCoor.t = 0.34;// 给向量aTextColor的t分量赋值
若向量是 4 维的,则可以使用的分量名为: s、 t、 p、 q;若向量为 3 维的,则可以使用的分量名为 s、 t、 p;若向量为 2 维的,则仅可以使用 s、 t 两个分量名。
访问向量中的各个分量不但可以采用“.”加上不同的分量名,还可以将向量看作一个数组,用下标来进行访问,具体用法如下。
aColor[0] = 0.6;// 给向量aColor的红色通道分量赋值
aPosition[2] = 48.3;// 给向量aPosition的z轴分量赋值
aTextCoor[1] = 0.34;// 给向量aTextCoor的t分量赋值
其实,在 C 语言中也可以通过自己构建结构体的方式来支持向量,但进行向量的运算时必须由 CPU 将每个分量依照顺序计算(3 个分量就需要计算 3 次),效率不高。而着色器中的向量则不同,其由硬件原生支持,进行向量的运算时是各分量并行一次完成的(n 个分量只需要一次计算),效率大大提高。
3.矩阵
3D 场景中的移位、旋转、缩放等变换都是由矩阵的运算来实现的。因此 3D 场景的开发中会非常多地使用到矩阵,因此, OpenGL ES 着色语言中也提供了对矩阵类型的支持。
矩阵按尺寸分为 2× 2 矩阵、 2× 3 矩阵和 2× 4 矩阵、 3× 2 矩阵、 3× 3 矩阵和 3× 4 矩阵以及4× 2 矩阵、 4× 3 矩阵和 4× 4 矩阵,其中矩阵类型的第一个数字表示矩阵的列数,第二个数字表示矩阵的行数,具体情况如下表:
mat2 m2;// 声明一个mat2类型的矩阵
mat3 m3;// 声明一个mat3类型的矩阵
mat4 m4;// 声明一个mat4类型的矩阵
mat3x2 m5;// 声明一个mat3x2类型的矩阵
OpenGL ES 着色语言中,矩阵是按列顺序组织的,也就是一个矩阵可以看作由几个列向量组成。例如, mat3 就可以看作由 3 个 vec3 组成。另外, mat2 与 mat2× 2、 mat3 与 mat3× 3 以及 mat4与 mat4× 4 是 3 组两两完全相同的类型,只是其类型的名称不同而已。
对于矩阵的访问,可以将矩阵作为列向量的数组来访问。如 matrix 为一个 mat4,可以使用matrix[2]取到该矩阵的第 3 列,其为一个vec4;也可以使用 matrix[2][2]取得第 3 列的向量的第 3个分量,其为一个 float;其他的依此类推。
从数学上讲,矩阵看作由向量组成时有两种选择:可以将矩阵看作由多个行向量组成或看作由多个列向量组成。虽然不同的选择功能一样,但在具体进行计算时是有所不同的,因此,了解 OpenGL ES 的选择非常重要。
4.采样器
采样器是着色语言中不同于 C 语言的一种特殊的基本数据类型,其专门用来进行纹理采样的相关操作。一般情况下,一个采样器变量代表一幅或一套纹理贴图,其具体情况如下表所列:
需要注意的是,与前面介绍的几类变量不同,采样器变量不能在着色器中进行初始化。一般情况下采样器变量都用 uniform 限定符来修饰,从宿主语言(如 C++、 Java)接收传递进着色器的值。此外,采样器变量也可以用作函数的参数,但是作为函数参数时不可以使用 out 或 inout修饰符来修饰。
5.结构体
struct into {// 声明一个结构体info
vec3 color;// 颜色成员
vec3 position;// 位置成员
vec2 textureCoor;// 纹理坐标成员
};
info CubeInfo;// 声明一个info类型的变量CubeInfo
6.数组
vec3 position[20]; 声明一个包含20个vec3的数组
float x[] = float[2] {1.0, 2.0};
float y[] = float[] {1.0, 2.0, 3.0};
OpenGL ES 3.0 的着色语言只支持一维数组的使用,不支持二维以及更多维数组的使用。
7.空类型
void main() {// 声明一个空返回值类型的main方法
...
}
➋数据类型的基本使用
1.声明、作用域及初始化
变量的声明及作用域与 C++语法类似,可以在任何需要的位置声明变量,同时其作用域也与C++类似,分为局部变量与全局变量。
int a, b;// 声明全局变量a、b
vec3 aPosition = vec3(1.0, 2.0, 3.0);// 声明全局变量aPosition并赋值
void myFunction() {
int c = 14;// 声明局部变量c并赋初值
a = 4;// 给全局变量a赋值
b = a*c;// 给全局变量b赋值
}
❏ 由于系统中有很多内建变量都是以“gl_”作为开头,因此用户自定义的变量不允许使用“gl_”作为开头。
❏ 为自己的函数或变量取名时尽量采用有意义的拼写,除了一些局部变量外不要采用 a、 b、c 这样的名称。若一个单词不足以描述变量的用途,可以用多个单词组合,除第一个单词全小写外,其他每个单词的第一个字母大写。
float a = 12.3; //声明了浮点变量 a 并赋初值
float b = 11.4; //声明了浮点变量 b 并赋初值
vec2 va = vec2(2.3, 2.5);//声明了 2 维向量 va 并赋初值
vec2 vb = vec2(a, b); //声明了 2 维向量 vb 并赋初值
vec3 vc = vec3(vb, 13.5);//声明了 3 维向量 vc 并赋初值
vec4 vd = vec4(va, vb); //声明了 4 维向量 vd 并赋初值
vec4 ve = vec4(0.2); //声明了 4 维向量 ve 并赋初值,相当于 vec4(0.2,0.2,0.2,0.2)
vec3 vf = vec3(ve); //声明了 3 维向量并初始化,相当于(0.2,0.2,0.2),舍弃了 ve 的第 4 个分量
矩阵的初始化也有一些灵活变化的技巧,具体分为如下几种情况。
❏ 初始化时矩阵的各个元素既可以使用字面常量,也可以使用变量,还可以从其他向量直接获取。
❏ 初始化时若矩阵只有对角线上有值且相同,可以通过给出 1 个字面常量初始化矩阵。
❏ 初始化时矩阵 M1 的行列数( N× N)小于构造器中矩阵 M2 的行列数( M× M)时,即N
❏ 初始化时矩阵 M1 的行列数( N× N)大于构造器中矩阵 M2 的行列数( M× M)时,即N>M,矩阵 M1 左上角 M× M 个元素的值为矩阵 M2 的对应元素值,矩阵 M1 右下角剩余对角线元素的值为 1,矩阵 M1 剩余其他的元素值为 0。
1 float a = 6.3; //声明了浮点变量 a 并赋初值
2 float b = 11.4; //声明了浮点变量 b 并赋初值
3 float c = 12.5; //声明了浮点变量 c 并赋初值
4 vec3 va = vec3(2.3,2.5,3.8);
5 vec3 vb = vec3(a,b,c);
6 vec3 vc = vec3(vb.x,va.y,14.4);
7 mat3 ma = mat3(1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0,c);//通过给出 9 个字面常量初始化 3×3 的矩阵
8 mat3 mb = mat3(va,vb,vc); //通过给出 3 个向量初始化 3× 3 的矩阵
9 mat3 mc = mat3(va,vb,1.0,2.0,3.0); //通过给出 2 个向量和 3 个字面常量初始化 3×3 的矩阵
10 mat3 md = mat3(2.0) ; //通过给出 1 个字面常量初始化 3× 3 的矩阵
11 mat4x4 me = mat4x4(3.0);//等价于 mat4x4(3.0,0.0,0.0,0.0, 0.0,3.0,0.0,0.0, 0.0,0.0,3.0,0.0, 0.0,0.0,0.0,3.0)
12 mat3x3 mf = mat3x3(me);//等价于 mat3x3(3.0,0.0,0.0 ,0.0,3.0,0.0, 0.0,0.0,3.0)
13 vec2 vd = vec2(a,b);
14 mat4x2 mg = mat4x2(vd,vd,vd,vd);
15 mat2x3 mh = mat2x3(mg); //等价于 mat2x3(6.3,11.4,0.0, 6.3,11.4,0.0)
16 mat4x4 mj = mat4x4(mf);// 等价于 mat4×4(3.0,0.0,0.0,0.0, 0.0,3.0,0.0,0.0, 0.0,0.0,3.0,0.0, 0.0,0.0,0.0,1.0)
第 7~ 9 行的代码遵循了第一条规则,第 10 行、第 11行的代码遵循了第二条规则,第 12 行的代码遵循了第三条规则,第 15 行的代码遵循了第四条规则,第 16 行代码遵循了第五条规则(即最后一个值是1而非0)。
2.变量初始化的规则
int a = 2, b = 3, c;// 声明了int型变量a、b与c,同时为a与b变量赋初值
/* 用const限定符修饰的变量【必须】在声明时进行初始化 */
const float k = 1.0;
/* 全局的:输入变量、一致变量、输出变量,在声明时【一定】不能进行初始化 */
in float angleSpan;
uniform int k;
out vec3 position;
为了防止重复计算,着色器中应少使用字面常量,而用常量代替,如有多个 1.0、0.0,可以声明常量,重复使用。
➌运算符
1.索引
float array[10];
array[2] = 1.0;
vec3 position = vec3(2.3, 5.0, 0.2);
float tmp = position[1];// 通过索引获取到第二个值5.0并赋给tmp变量
mat4 matrix = mat4(1.0);
vec4 tmpV = matrix[1];// 通过索引对matrix矩阵进行操作,获取到对应向量并赋给tmpV
2.混合选择
vec4 color = vec4(0.7, 0.1, 0.5, 1.0);
vec3 temp = color.agb;// 获取到向量(1.0, 0.1, 0.5)赋给temp
vec4 tempL = color.aabb;// 获取到向量(1.0, 1.0, 0.5, 0.5)赋给tempL
vec3 tmepLL;
tempLL.grb = color.aab;// 对向量tempLL的3个分量赋值
❏ 一次混合最多只能列出 4 个分量名称,且一次出现的各部分的分量名称必须是来自同一名称组。 3 个名称组分别为: xyzw、 rgba、 stpq。
❏ 各分量的名称在进行混合时可以改变顺序以进行重新排列。
❏?
3.算法运算符
若在向量以及矩阵中使用++、--,则向量或矩阵的每个元素加1或减1。但若是在矩阵上使用乘法,则执行的是线性代数中的矩阵乘法。
vec3 va = vec3(0.5, 0.5, 0.5);
vec3 vb = vec3(2.0, 1.0, 4.0);
vec3 vc = va * vb;// 两个向量执行按分量的乘法,加减与之类似
mat3 ma = mat3(1,2,3, 4,5,6, 7,8,9);
mat3 mb = mat3(9,8,7, 6,5,4, 3,2,1);
vec3 vd = va * ma;// 执行向量与矩阵的乘法,满足线性代数的定义
mat3 mc = ma * mb;// 执行矩阵乘法,满足线性代数的定义
❏ 向量用算术运算符运算时,执行的是各分量的算术运算。如将两个向量用“+”相加,实际执行的是向量的各分量相加得到一个新的向量。
❏ 关于向量与矩阵以及矩阵与矩阵的乘法都是执行的满足线性代数定义的运算。
与向量类似,矩阵也有基向量的概念,如矩阵:
该矩阵也是三维的,基向量i=(2,0,0),j=(0,2,0),k=(0,0,2)
上述向量(2,2,2)其实是矩阵三个基向量i, j, k通过加法得到的。
因此矩阵与向量的关系:n维向量是由n维矩阵中n列对应的n个基向量通过加法法则构成的。故矩阵就是基向量的集合。
m×n矩阵,当m小于n时,相当于降维;当m大于n时,相当于升维。
该矩阵的基向量i=(2,0),j=(0,2),k=(2,2)从原来的三维降到二维了。
矩阵[m×n] × [n×p] = [m×p]。所以,矩阵×列—>列
所以上面:vec3 vd = va * ma按理说无法计算,因为矩阵乘法有左乘和右乘,这里应该是向量与矩阵相乘默认都是向量右乘生成矩阵。
float vertices[] = new float[] {
1, 2, 3,
4, 5, 6,
7, 8, 9
};
注意,以上初始化中:
1,2,3是第一个顶点X、Y、Z坐标,也是矩阵第一【列】的值;
4,5,6是第二个顶点X、Y、Z坐标,也是矩阵第二【列】的值;
7,8,9是第三个顶点X、Y、Z坐标,也是矩阵第三【列】的值。
4.其他运算符
❏ 关系运算符(<、 >、 <= 、 >=)只能用在浮点数或整数标量的操作中,通过关系运算符的运算将产生一个布尔型的值。如果想要得到两个向量中的每一个元素比较大小的结果,则可以调用内置函数 lessThan、 lessThanEqual、 greaterThan 和 greaterThanEqual。
❏ 赋值运算符中最常用的“=”在操作时,要求符号两边的操作数必须类型完全相同。着色语言的赋值没有自动类型转换或提升功
能。例如“float a=1;”就是错的,因为左侧的 a 是浮点型,右侧的 1 是整型。
➍构造函数
1.向量的构造函数
向量的构造函数可以用来创建指定类型向量的实例,其入口参数一般可以为基本类型的字面常量、变量或其他向量,主要有如下两种基本形式。
❏ 如果向量的构造函数内只有一个标量值,那么该向量的所有分量都等于该值。
❏ 如果向量的构造函数内有多个标量或者向量参数,那么向量的分量则由左向右依次被赋值。在这种情况下,参数的分量和向量的分量至少要一样多。
vec4 myVec4 = vec4(1.0); // 每个分量都是1.0
vec3 myVec3 = vec3(1.0, 0.0, 0.5);// 分量分别为1.0、0.0、0.5
vec3 temp = vec3(myVec3);// 各分量的值等于myVec3各分量的值
vec2 myVec2 = vec2(myVec3);// 分量值分别为1.0、0.0
myVec4 = vec4(myVec2, temp);// 分量值分别为1.0、0.0、1.0、0.0
我们可以看到,如果向量的构造函数的参数与声明向量的类型不相符,则会选择数据类型转换的方式转换参数类型,与向量类型匹配。
2.矩阵的构造函数
矩阵的构造函数共有 3 种基本形式。
❏ 如果矩阵的构造函数内只有一个标量值,那么矩阵的对角线上的分量都等于该值,其余值为 0。
❏ 矩阵可以由许多向量构造而成。比如说,一个 mat2 矩阵可以由两个 vec2 构成。
❏ 矩阵还可以由大量的标量值构成,矩阵的分量由左向右依次被赋值。
只要提供了足够的参数,矩阵甚至可以由任意的标量值和向量值合并构成。
vec2 d = vec2(1.0, 2.0);
mat2 e = mat2(d, d); // e的现列都为1.0、2.0
mat3 f = mat3(e);// 将矩阵e放到矩阵f的左上角,右下角对角线的值为1,其余为0
mat4x2 g = mat4x2(d, d, d, d);// 声明一个4x2矩阵
mat2x3 h = mat2x3(g);// 将矩阵g左上角2*2个元素值赋给h中对应的元素,h矩阵的最后一行为0,0
mat3 myMat3 = mat3(1.0, 0.0, 0.0 // 矩阵第一【列】的值
0.0, 1.0, 0.0, // 矩阵第二【列】的值
0.0, 1.0, 1.0); // 矩阵第三【列】的值
注意myMat3中每三个元素成为一【列】而非行。这由g=mat4x2(d,d,d,d)也可以印证(其中向量参数是以列排放的)。
OpenGL ES中矩阵元素的存储顺序以列为主,即矩阵由列向量组成。因此,当使用矩阵的构造函数时,矩阵的元素将会按照矩阵的列的顺序依次被参数赋值。这一点从上述代码片段的第 5 行有所体现。
3.结构体的构造函数
struct light {
float intensity;
vec3 position;
};
light lightVar = light(2.0, vec3(1.0, 2.0, 3.0));// 创建light结构体的实例
4.数组的构造函数
const float i[3] = float[3](1.0, 2.0, 3.0); //声明一个长度为 3 的 float 数组
const float j[3] = float[](1.0, 2.0, 3.0); //声明一个长度为 3 的 float 数组
float k = 1.0; //声明一个变量
float m[3]; //定义一个一维数组
m = float[3](k, k+1.0, k+2.0); //给数组赋值
在使用数组的构造函数时,需要【保证】参数的个数与定义的数组长度相同。数组的索引值从 0 开始,并且每个参数的类型与定义数组的类型【必须】一致。
➎类型转换
OpenGL ES 着色语言没有提供类型的自动提升功能,并且对类型匹配的要求十分严格。例如前面介绍过的,赋值表达式中的两个操作数类型必须完全相同,另外调用函数时的形参以及实参的类型也必须完全相同。
同时 OpenGL ES 着色语言也没有提供数据类型的强制转换功能,只能使用构造函数来完成类型转换,下面的代码片段说明了这个问题。
float f = 1.0; //声明一个浮点数 f 并赋值
bool b = bool(f); //将浮点数转换成布尔类型,该构造函数将非 0 的数字转为 true, 0 转为 false
float f1 = float(b); //将布尔值转变为浮点数, true 转换为 1.0, false 转换为 0.0
int c = int(f1); //将浮点数转换成有符号或者无符号整型,直接去掉小数部分
➏存储限定符
1 uniform mat4 uMVPMatrix; //声明一个用 uniform 修饰的 mat4 类型的矩阵
2 in vec3 aPosition; //声明一个用 in 修饰的 vec3 类型的向量
3 out vec4 aaColor; //声明一个用 out 修饰的 vec4 类型的向量
4 const int lightsCount = 4; //声明一个用 const 修饰的 int 类型的常量
使用 in、 uniform 以及 out 限定符修饰的变量【必须】为全局变量。同时要注意的是,着色语言中没有默认限定符的概念,因此如果有需要,【必须】为全局变量明确指定需要的限定符。
1.in/centroid in限定符
in/centroid in 限定符修饰的全局变量又称为输入变量,其形成当前着色器与渲染管线前驱阶段的动态输入接口。输入变量的值是在着色器开始执行时,由渲染管线的前一阶段送入。【在着色器程序执行过程中,变量【不可以被重新赋值】】。 in/centroid in 限定符的使用分为如下两种情况。
(1)顶点着色器的输入变量
顶点着色器中只能使用 in 限定符来修饰全局变量,【不能】 使用 centroid in 限定符和后面将要介绍的 interpolation 限定符。在顶点着色器中使用 in 限定符修饰的变量用来接收渲染管线传递进顶点着色器的当前待处理顶点的各种属性值。这些属性值每个顶点各自拥有独立的副本,用于描述顶点的各项特征,如顶点坐标、法向量、颜色、纹理坐标等。
顶点着色器中用 in 限定符修饰的变量其值实质是由宿主程序(本书中为 Java、 C++)批量传入渲染管线的,管线进行基本处理后再传递给顶点着色器(参考上图)。数据中有多少个顶点,管线就调用多少次顶点着色器,每次将一个顶点的各种属性数据传递给顶点着色器中对应的 in 变量。因此,顶点着色器每次执行将完成对一个顶点各项属性数据的处理。
在顶点着色器中, in 限定符只能用来修饰浮点数标量、浮点数向量、矩阵变量以及有符号或无符号的整型标量或整型向量,不能用来修饰其他类型的变量。下面的代码片段给出了在顶点着色器中正确使用 in 限定符的情况。
in vec3 aPosition;// 顶点位置
in vec3 aNormal;// 顶点法向量
从上述介绍中可以看出,若需要渲染的 3D 物体中有很多顶点,顶点着色器就需要执行很多次,这很耗费时间。另外,由于顶点着色器每次执行仅处理一个独立顶点的相关数据,可见顶点着色器的多次执行之间并没有什么逻辑依赖。因此,当今主流的 GPU 中都配置了不止一套顶点着色器的硬件,数量从几套到几百套不等。通过这些顶点着色器的并发执行,可以提高渲染速度。
顶点着色器中对于用 in 限定符修饰的变量其值是由宿主程序批量传入渲染管线的 。 一般来说 , 将顶点数据传送进渲染管线需要调用 glVertexAttribPointer 方法或 glVertexAttribIPointer 方法。(vertext [ˈvɜːrteks] (几何形状的)顶点)
1 int maPositionHandle; //用来存储从着色器中获取的对应输入变量的引用值
2 maPositionHandle = GLES30.glGetAttribLocation( //获取着色器中指定名称输入变量的引用值
3 mProgram, //采用的着色器程序 id
4 "aPosition" //着色器中对应的输入变量名称
5 );
6 GLES30.glVertexAttribPointer( //将已经放入缓冲中的数据批量传入渲染管线
7 maPositionHandle, //顶点位置属性引用
8 3, //每顶点一组的数据个数(这里是 X、 Y、 Z 坐标,因此为 3)
9 GLES30.GL_FLOAT, //数据类型
10 false, //是否规格化
11 3*4, //每组数据的尺寸,这里每组 3 个浮点数值(X、 Y、 Z 坐标)
12 //每个浮点数 4 个字节,共 3*4=12 个字节
13 mVertexBuffer //存放了数据的缓冲
14 );
15 GLES30.glEnableVertexAttribArray(maPositionHandle); //启用顶点位置数据
❏ 第 6-14 行将已经放入缓冲中的数据批量传入渲染管线,以备管线经过基本处理后将对应的值传递给顶点着色器中接收顶点属性数据的输入变量。
❏ 上述代码中对应于输入变量 aPosition,每个顶点的数据个数为 3,占用的空间为 12 字节,类型为 GL_FLOAT。因此,对于输入变量 aPosition,当管线将数据传递给其时,会每次从对应的缓冲中取出 12 个字节,拆分成 3 个 4 字节浮点数。而着色器中名称为 aPosition 的输入变量类型为 vec3,正好与之匹配。
❏ 将缓冲中的数据传递进渲染管线时需要根据对应输入变量的类型给 glVertexAttribPointer方法设置合理的参数值。
❏ 第 15 行通过顶点位置属性引用 maPositionHandle 启用了批量传入管线的数据。
从上述代码中可以看出,主要的工作分为 3 步:➀获取对应接收属性值的输入变量的引用、➁通过引用将缓冲中的数据传入管线、➂启用传入的数据。上述代码片段中的工作往往不是在一个方法中完成的,具体位臵情况可以参考上一章 Sample3_1 中的 Triangle 类。
从上述代码片段中可以看出,在传送顶点属性数据之前,首先需要按照顶点的次序依次将属性值送入缓冲,这项工作对应的代码片段如下:
float vertices[] = new float[] { //首先将顶点此项属性数据依次放入数组,这里是顶点坐标
-4*UNIT_SIZE, 0, 0, //第 1 个顶点的 X、 Y、 Z 坐标值
0, 0, -4*UNIT_SIZE, //第 2 个顶点的 X、 Y、 Z 坐标值
0, 4*UNIT_SIZE, 0 //第 3 个顶点的 X、 Y、 Z 坐标值
};
ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);//开辟对应容量的缓冲
vbb.order(ByteOrder.nativeOrder()); //设置字节顺序为本地操作系统顺序
mVertexBuffer = vbb.asFloatBuffer() //浮点(Float)型缓冲
mVertexBuffer.put(vertices); //将数组中的顶点数据送入缓冲
mVertexBuffer.position(0); //设置缓冲起始位置
上述代码给出了准备顶点坐标数据缓冲的基本套路,首先将需要的数据依次放入数组,然后开辟对应容量的缓冲,最后将数组中的数据存入缓冲即可。
(2)片元着色器的输入变量
片元着色器中可以使用 in 或 centroid in 限定符来修饰全局变量,其变量用于接收来自顶点着色器的相关数据,最典型的是接收根据顶点着色器的顶点数据插值产生的片元数据。
在片元着色器中, in/centroid in 限定符可以修饰的类型包括有符号或无符号的整型标量或整型向量、浮点数标量、浮点数向量、矩阵变量以及数组变量、结构体变量。然而,当片元着色器中 in/centroid in 变量的类型为有符号或无符号整型标量或整型向量时,变量也【必须】使用后面介绍到的 flat 限定符来修饰。
in vec3 vPosition; //接收从顶点着色器传递过来的顶点位置数据
centroid in vec2 vTexCoord; //接收从顶点着色器传递过来的纹理坐标数据
flat in vec3 vColor; //接收从顶点着色器传递过来的颜色数据
顶点着色器的 in 变量【不可以】用来声明数组,也【不可以】用来声明结构体对象。此外,顶点着色器的 in 变量不像一致变量能通过打包传送数据,因此最好使用 vec4 的整数倍送入,以提高效率。
2.uniform限定符
uniform 为一致变量限定符,一致变量指的是对于同一组顶点组成的单个 3D 物体中所有顶点都相同的量。 uniform 变量可以用在顶点着色器或片元着色器中,其支持用来修饰所有的基本数据类型。与 in 变量类似,一致变量的值也是从宿主程序传入的。
uniform mat4 uMVPMatrix;// 总变换矩阵
uniform mat4 uMMatrix;// 基本变换矩阵
uniform vec3 uLightLocation;// 光源矩阵
uniform vec3 uCamera;// 摄像机位置
——————————————————————————————————————
将一致变量的值从宿主程序(Java或C++)传入渲染管线:
int muMVPMatrixHandle;// 总变换矩阵一致变量引用
// 获取着色器程序中总变换矩阵一致变量的引用
muMVPMatrixHandle = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix");
// 通过一致变量引用将一致变量值传入渲染管线
GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, Triangle.getFinalMatrix(mMMatrix, 0), 0);
从上述代码中可以看出,将一致变量的值送入渲染管线比较简单,主要包括两个步骤:➀获取着色器程序中一致变量的引用;➁调用类似 glUniformMatrix4fv 这样的方法将对应一致变量的值送入渲染管线。
随一致变量类型的不同将值传入渲染管线的方法也有所不同,这些方法的名称都以“glUniform”开头,常用的如下所列。
❏ glUniformNf / glUniformNfv 方法,将 N 个浮点数传入管线,以备管线传递给由 N 个浮点数组成的一致变量, N 的取值为1、2、3、4。
❏ glUniformNi / glUniformNiv 方法,将 N 个整数传入管线,以备管线传递给由 N 个整数组成的一致变量, N 的取值为 1、2、3、4。
❏ glUniformMatrixNfv 方法,将 N×N 的矩阵传入管线,以备管线传递给 N×N 矩阵类型的一致变量, N 的取值为 2、3、4。
3.out/centroid out限定符(centroid ['sɛntrɔɪd]:质心)
out/centroid out 限定符修饰的全局变量又称为输出变量,其形成当前着色器与渲染管线后继阶段的动态输出接口。通常在当前着色器程序执行完毕时,输出变量的值才被送入后继阶段进行处理。因此,【不能】在着色器中声明同时起到输入和输出作用的 inout 全局变量, out/centroid out 限定符的使用分为如下两种情况。
(1)顶点着色器的输出变量
顶点着色器中可以使用 out 或 centroid out 限定符修饰全局变量,其变量用于向渲染管线后继阶段传递当前顶点的数据。
在顶点着色器中, out/centroid out 限定符只能用来修饰浮点型标量、浮点型向量、矩阵变量、有符号或无符号的整型标量或整型向量、数组变量及结构体变量。然而,当顶点着色器中out/centroid out 变量的类型为有符号或无符号的整型标量或整型向量时,变量也【必须】使用后面介绍到的 flat 限定符来修饰。
下图给出了默认情况下 out 变量的工作原理。
从图中可以看出,在默认情况下,首先顶点着色器在每个顶点中都对 out 变量 vPosition进行了赋值。接着在片元着色器中接收 in 变量 vPosition 的值时得到的并不是某个顶点赋的特定值,而是根据片元所在的位置及图元中各个顶点的位置进行插值计算产生的值。
如图中顶点 1、 2、 3 的 vPosition 值分别为 vec3(0.0,7.0,0.0)、 vec3(-5.0,0.0,0.0)、 vec3(5.0,0.0,0.0),则插值后片元 a 的 vPosition 值为 vec3(1.27,5.27,0.0),这个值是根据 3 个顶点对应的着色器给 vPosition 赋的值、 3 个顶点的位置及此片元的位置由管线插值计算得到的。
从上述介绍中可以看出,光栅化后产生了多少个片元,就会插值计算出多少套 in 变量。同时,渲染管线就会调用多少次片元着色器。可以看出,一般情况下对一个 3D 物体的渲染中,片元着色器执行的次数会大大超过顶点着色器。因此, GPU 硬件中配置的片元着色器硬件数量往往多于顶点着色器硬件数量,通过这些硬件单元的并行执行,提高渲染速度。
从上述介绍中可以看出,这就是为什么 GPU 在图形处理性能上远远超过同时代、同档次 CPU 的原因, CPU 中没有这么多的并行硬件处理单元。
下面的代码片段给出了在顶点着色器中正确使用 out/centroid out 限定符的情况。
out vec4 ambient; //环境光 out 变量
out vec4 diffuse; //散射光 out 变量
centroid out vec2 texCoor; //纹理坐标 out 变量
invariant centroid out vec4 color; //颜色值 out 变量
(2)片元着色器的输出变量
在片元着色器中【只能】使用 out 限定符来修饰全局变量,而不能使用 centroid out 限定符。片元着色器中的 out 变量一般指的是由片元着色器写入计算完成片元颜色值的变量,一般在片元着色器的最后都需要对其进行赋值,最后将其送入渲染管线的后继阶段进行处理。
在片元着色器中, out 限定符只能用来修饰浮点型标量、浮点型向量、有符号或无符号的整型标量或整型向量及数组变量,不能用来修饰其他类型的变量。
out vec4 fragColor; //输出的片元颜色
out uint luminosity;
对于顶点着色器而言,一般是既声明 out 变量,又对 out 变量进行赋值用以传递给片元着色器。而片元着色器中声明 in 变量用于接收顶点着色器传过来的值即可,是不可以对 in 变量赋值的。 OpenGL ES 3.0 中片元着色器内的内建输出变量gl_FragColor(此内建变量在OpenGL ES 2.0 中几乎总要用到)不存在了,需要自己声明 out( vec4)变量,用声明的 out 变量替代 gl_FragColor 内建变量。
4.const限定符
用 const 限定符修饰的变量是只读的,其值是不可以变的,也就是常量,又称为编译时常量。编译时常量在声明时必须进行初始化,同时,这些常量在着色器外部是完全不可见的。
const int tempx = 1;
结构体内的成员变量不可以用 const 限定符修饰,而结构体类型的变量可以使用其进行修饰。用 const 限定符修饰的结构体变量需要在声明时通过构造器进行初始化,后期不可以再进行赋值。
用 const 限定符修饰的变量在编译时,编译器是不需要向其分配任何运行时资源的,恰当采用可以一定程度上提高设备的运行效率。
➐插值限定符
插值(interpolation)限定符,其主要用于控制顶点着色器传递到片元着色器数据的插值方式。插值限定符包含 smooth、 flat 两种。
若使用插值限定符,则该限定符应该在 in、 centroid in、 out 或 centroid out 之前使用,且只能用来修饰顶点着色器的 out 变量与片元着色器中对应的 in 变量。当未使用任何插值限定符时,默认的插值方式为 smooth。
1.smooth限定符
如果顶点着色器中 out 变量之前含有 smooth 限定符或者不含有任何限定符,则传递到后继片元着色器对应的 in 变量的值,是在光栅化阶段由管线根据片元所属图元各个顶点对应的顶点着色器对此 out 变量的赋值情况,及片元与各顶点的位置关系插值产生,下图以颜色值为例,说明了这个问题。
从图中可以看出,当顶点着色器中的 out 变量被 smooth 限定符修饰时,首先顶点着色器在每个顶点中都对 out 变量 vColor 进行了赋值,接着在片元着色器中接收 in 变量 vColor 的值时得到的并不是某个顶点赋的特定值,而是根据片元的位置及图元中各个顶点的位置与各个顶点赋值的情况进行插值计算产生的值。
如图中顶点 1、 2、 3 的 vColor 值分别为 vec3(0.0,0.0,0.0)、 vec3(1.0,1.0,1.0)、 vec3(0.5,0.5,0.5),则插值后片元 a 的 vColor值为 vec3(0.27,0.27,0.27),这个值是根据 3 个顶点对应的着色器给 vColor 赋的值、 3 个顶点的位置及此片元的位置由管线插值计算得到的。
smooth out vec3 normal;// 顶点着色器out变量
smooth in vec3 normal;// 片元着色器in变量
2.flat限定符
如果顶点着色器中 out 变量之前含有 flat 限定符,则传递到后继片元着色器中对应的 in 变量的值不是在光栅化阶段插值产生的,一般是由图元的最后一个顶点对应的顶点着色器对此 out 变量所赋的值决定的。此时,图元中每个片元的此项值均相同。
若顶点着色器中的输出变量的类型为整型标量或整型向量,则变量【必须】使用 flat 限定符修饰。与之对应,若片元着色器中的输入变量的类型为整型标量或整型向量,变量【必须】使用 flat 限定符修饰。
flat out vec4 vColor;// 用于传递给片元着色器的变量
flat in vec4 vColor;// 用于接收来自顶点着色器的变量
无论顶点着色器中的 out 全局变量被哪种插值限定符修饰,后继片元着色器中必须含有与之对应的修饰符修饰的 in 全局变量。
➑一致块
多个一致变量的声明可以通过类似结构体形式的接口块实现,该形式的接口块又称为一致块( uniform block)。一致块的数据是通过缓冲对象送入渲染管线的,以一致块的形式批量传送数据比单个传送效率高,其基本语法为:
[
] uniform 一致块名称 {<成员变量列表>} [<实例名>]
❏ layout 限定符的具体内容会在下一小节进行介绍。
❏ uniform 为一致块的修饰关键字,声明一致块时必须使用该关键字。
❏ 应用程序是通过一致块名称识别一致块的。
❏ 成员变量列表中可以包含多个变量的声明,与普通结构体内成员变量的声明类似。
❏ 实例名是一致块的实例名称。
uniform Transform { // 声明一个uniform接口块
float radius;// 半径成员
mat4 modelViewMatrix;// 矩阵成员
uniform mat3 normalMatrix;// 矩阵成员
} block_Transform;
需要注意的是,一致块内不允许声明 in 或 out 变量、采样器类型的变量,也不能定义结构体类型。另外,内建变量、数组变量及已定义结构体类型的变量可以作为一致块的成员变量,其用法与在块外的用法相同。
创建一致块时,可以声明实例名,也可以不声明实例名。下面分两种情况进行介绍
❏ 未声明实例名
如果在创建一致块时未声明实例名,则一致块内的成员变量与在块外一样,其作用域是全局的,宿主语言既可以直接通过一致块的成员变量名称访问对应变量,也可以通过“<一致块名称>.<成员变量名>”的形式访问一致块的成员变量。
❏ 声明实例名
如果在创建一致块时声明了实例名,则一致块内成员变量的作用域为从声明开始到一致块结束,宿主语言通过“<一致块名称>.<成员变量名>”访问成员变量,而着色器中需要通过“<实例名>.<成员变量名>”访问一致块的成员变量。
uniform MatrixBlock { // 一致块
mat4 uMVPMatrix;// 块成员变量
} mb; // mb为实例名
gl_Position = mb.uMVPMatrix * vec4(aPosition, 1);// 根据总变换矩阵计算此次绘制此项点位置
➒layout限定符
其可以作为接口块定义的一部分或者接口块的成员。
其也可以仅仅修饰 uniform,用于建立其他一致变量声明的参照。语法:
其还可以用于修饰被接口限定符修饰的单独变量。语法:
着色器中的 layout 限定符【必须】在存储( storage)限定符之前使用,且 layout 限定符修饰的变量或接口块的作用域【必须】是全局的。
接口限定符有 in、 out、 uniform 三种选择, layout 限定符修饰接口限定符的内容将在下面进行介绍,具体内容如下。
1.layout输入限定符
顶点着色器允许 layout 输入限定符修饰输入变量的声明。
layout (location = 0) in vec3 aPosition;// aPosition输入变量的引用值为0
layout (loaction = 1) in vec4 aColor;// aColor输入变量的引用值为1
注意,片元着色器内不允许有 layout 输入限定符。
2.layout输出限定符
片元着色器中, layout 限定符通过 location 值将输出变量和指定编号的绘制缓冲绑定起来。每一个输出变量的索引(引用)值都会对应到一个相应编号的绘制缓冲,而这个输出变量的值将写入相应缓冲。
layout 限定符的 location 值是有范围的,其范围为[0,MAX_DRAW_BUFFERS-1]。不同手持设备的范围有可能不同,最基本的范围是[0,3]。
layout (location = 0) out vec4 fragColor;// 此输出变量写入到0号绘制缓冲
layout (location = 1) out vec4 colors[2];// 此输出变量写入到1号绘制缓冲
顶点着色器不允许有 layout 输出限定符。如果在片元着色器中只有一个输出变量,则不需要用 layout 修饰符说明其对应绘制缓冲,在这种情况下,默认值为 0。如果片元着色器中有多个输出变量,则不允许重复使用相同的 location 值。
3.一致块layout限定符
一致块可以使用 layout 限定符进行修饰,但是,单独一致变量的声明不能使用。可供一致块使用的 layout 限定符主要有 5 种情况。
❏ shared:shared 限定符重写了 std140 和 packed,其他限定符是被继承的。编译器/链接器必须要保证在许多着色器或者许多程序中一致块的内存布局是共享的。如果使用了 shared 限定符,则定义的 row_major/column_major的值必须是完全相同的。这样的话,将允许在不同的程序中的相同块的定义使用相同的缓冲。
❏ packed:packed 限定符重写了 std140 和 shared,其他限定符是被继承的。编译器可以优化一致块的内存布局。使用这个限定符则必须查询偏移量的位置,并且一致块在顶点着色器/片元着色器或者程序中不能共享。
❏ std140:std140 限定符重写了 packed 和 shared,其他限定符被继承。它主要强调了一致块的布局是基于一系列的标准准则的。
❏ row_major:row_major 限定符只重写了 column_major,其他限定符被继承。其只影响矩阵的布局,矩阵的各个元素在内存中将按照行优先的顺序存放。
❏ column_major:column_major 限定符只重写了 row_major,其他限定符被继承。它同样只影响矩阵的布局,矩阵的各个元素在内存中按照列优先的顺序存放,这是默认情况。
layout(std140, row_major) uniform MatrixBlock {// 块的布局是std140,行优先
mat4 M1;// 该矩阵变量的布局是行优先
layout(column_major) mat4 uMVPMatrix;// 该矩阵变量的布局是列优先
mat4 M2;// 该矩阵变量的布局是列优先
};
➓流程控制
OpenGL ES 着色语言共提供了 4 种流程控制方式,分别由 if-else 条件语句、 switch-case-default条件语句、 while(do-while)循环语句以及 for 循环语句实现。
⓫函数的声明与使用
与 C 语言中相同,着色语言中也可以开发自定义的函数,基本语法为:
[<精度限定符>]<返回类型> 函数名称 ( [<参数序列>]) { /*函数体*/}
❏ 精度限定符可以选择 highp、 mediump、 lowp 这 3 种之一,具体含义会在后面进行详细介绍。
❏ 返回类型根据需要可以是前面介绍的除采样器之外的任何类型。需要注意的是,返回类型为数组时,必须指定数组的长度。
❏ 函数名称要满足着色语言的命名规定。
❏ 参数序列放在一对圆括号中,若没有则为空。
❏ 函数体包含在一对花括号中,包含完成函数功能所需要的语句。
参数序列中的参数除了可以指定类型外,还可以指定用途。具体方法为用参数用途修饰符进行修饰,常用的参数用途修饰符如下所列。
❏ “in”修饰符,用其修饰的参数为输入参数,仅供函数接收外界传入的值。若某个参数没有明确给出用途修饰符,则等同于使用了“in”修饰符。
❏ “out”修饰符,用其修饰的参数为输出参数,在函数体中对输出参数赋值可以将值传递到调用其的外界变量中,且外界变量在被传递进来之前不能初始化。对于输出参数,要注意的是,在调用时不可以使用字面常量。
❏ “inout”修饰符,用其修饰的参数为输入输出参数,具有输入与输出两种参数的功能。输入输出参数在调用时也不可以使用字面常量。
从上述用途修饰符的介绍中可以看出,着色语言中函数返回信息的渠道除了返回值外还有输出参数,在需要时恰当使用可以增加开发的灵活性。需要注意的是,out 和 inout 限定符之前不可以使用 const 限定符。
另外,与 C 语言中相同,着色器也可以重载用户自定义的函数。对于名称相同的函数,只要参数序列中参数类型不同或参数个数不同即可。
void pointLight(in vec4 x,out vec4 y){}
void pointLight(in vec4 x,out ivec4 y){} //参数类型不同
void pointLight(in vec4 x,out vec4 y,out vec4 z){} //参数个数不同
注,着色器内只能重载用户自定义的函数,不可以重写或重载内建函数。
⓬片元着色器中浮点变量精度的指定
片元着色器中使用浮点相关类型的变量时与顶点着色器中有所不同,在顶点着色器中直接声明使用即可,而在片元着色器中必须指定精度,若不指定精度可能会引起编译错误。指定精度的方法如下面的代码片段所示。
lowp float color; //指定名称为 color 的 float 型变量精度为 lowp
in mediump vec2 Coord; //指定名称为 Coord 的 vec2 型变量精度为 mediump
highp mat4 m; //指定名称为 m 的 mat4 型变量精度为 highp
从上述代码片段中可以看出,精度有 3 种选择, lowp、 mediump 及 highp。这 3 种选择分别代表低、中、高 3 种精度等级,在不同的硬件中实现可能会有所不同。一般情况下,使用 mediump即可。另外,还可以看出所谓浮点相关类型不单包括标量类型 float,还包括与之对应的向量类型vec2、 vec3、 vec4,以及与之对应的矩阵类型 mat2、 mat3、 mat4 等。
如果在开发中同一个片元着色器中浮点相关类型的变量都选用同一种精度,则可以指定整个着色器中浮点相关类型的默认精度,语法为:precision <精度> <类型>
❏ 精度可以选择 lowp、 mediump 及 highp 3 种之一。
❏ 类型一般为 float,这不单表示为浮点标量类型 float 指定了精度,还表示对浮点类型相关的向量、矩阵也指定了默认精度。因此,一般开发中经常将片元着色器的第一句写为“precision mediump float”。
类型不但可以是 float,还可以是 int 或任何的采样器类型。由于整型相关类型和采样器类型并不一定要求指定精度,因此用的不多。另外,精度限定符不影响变量的类型。同时,可链接到一起的两个着色器中相同的 uniform 变量必须有相同的精度限定符。
⓭程序的基本结构
一个着色器程序一般由 4 大部分组成,主要包括:着色语言版本声明、全局变量声明、自定义函数、 main 函数。下面的代码片段给出了一个完整的顶点着色器程序。
1 #version 300 es
2 uniform mat4 uMVPMatrix;// 总变换矩阵
3 layout (location = 3) in vec3 aPosition;// 顶点位置
4 layout (location = 2) in vec4 aColor;// 顶点颜色
5 out vec4 vColor;// 用于传递给片元着色器的输出变量
6 void positionShift() {// 根据总变换矩阵计算此次绘制此顶点位置的方法
7 gl_Position = uMVPMatrix * vec4(aPosition, 1);
8 }
9 void main() {// 主函数
10 positionShift();// 根据总变换矩阵计算此次绘制此顶点位置
11 vColor = aColor;// 将接收的颜色传递给片元着色器
12 }
❏ 第 1 行为声明使用 3.0 着色语言版本的语句,每个着色器开始都必须使用该语句来声明着色语言版本。
❏ 第 2-5 行为全局变量的声明,根据具体情况的不同可能会有增加或减少。
❏ 第 6-8 行为自定义的函数,这一部分根据需要可能没有,也可能有很多不同的函数。
❏ 第 7 行的 gl_Position 是顶点着色器中的内建变量,这部分内容下一节将进行详细介绍。
❏ 第 9-12 行为主函数 main,这是每个着色器里面都必须有的部分。
每个着色器都必须在着色器程序的第一行通过“ #version 300 es”语句声明使用3.0 着色语言版本,如果没有该语句,则表示着色语言的版本是 2.0。另外,与很多高级语言不同的是,着色器程序中要求被调用的函数必须在调用之前声明,且自己开发的着色器中自己开发的函数不可以递归调用。就如上述代码中首先在第 6 行声明了 positionShift 函数,然后才能在第 10 行进行调用。这也是初学者易犯的一个错误,在开发中要多留心。