MCJE着色器教程:从开发入门到游戏崩溃(二)——GLSL
着色器由一种名为GLSL的语言编写,这是一个类C语言,也就是说,它的语法和C语言是相当相似的。因此,如果你有类C语言的编写基础,本节的学习将会更加的轻松。
关于C语言的教程可以参考C 语言教程 | 菜鸟教程 (runoob.com)
GLSL
一个着色器通常是长这样的
#version version_number in type in_variable_name; in type in_variable_name; out type out_variable_name; uniform type uniform_name; int main() { // 处理输入并进行一些图形操作 ... // 输出处理过的结果到输出变量 out_variable_name = weird_stuff_we_processed; }
我们来慢慢解析这段代码。首先,GLSL有着不同的版本,因此我们需要在第一行声明这个GLSL程序的什么版本。一般来说,针对Minecraft,我们会选择140版本,因此第一行会是#version 140
。接下来的四行(不包括空行)声明了一些全局变量,这些变量将能够在这个着色器的任意地方调用。之后就是一个main函数,同C语言一样,着色器程序的入口也是main函数。在main函数中你可以进行数据的处理,以及调用其他的函数。
数据类型
GLSL支持C语言中的大部分默认基本数据类型,包括int,float,double,uint(无符号整数,即非负整数),bool和数组。此外,GLSL还支持两种容器数据类型,分别为向量(Vector)和矩阵(Matrix)。
值得注意的是,GLSL中不存在数据类型的自动提升,比如int类型不能被自动转换为float类型,即形如double a = 1
的写法是错误的。GLSL中的类型必须保持严格一致。
向量
向量可以容纳二到四个变量,具体能容纳的数量在声明时确定,之后不可更改。向量能储存的数据个数这些向量的名字形式可以是这样的:
类型 | 含义 |
---|---|
vecn | 包含n 个float分量的默认向量 |
bvecn | 包含n 个bool分量的向量 |
ivecn | 包含n 个int分量的向量 |
uvecn | 包含n 个unsigned int分量的向量 |
dvecn | 包含n 个double分量的向量 |
例如,vec2 qwq = vec2(114.0,514.0)
声明了一个包含了两个float的向量。一般来说我们都会使用vecn,这足以满足大多数情况下的需求。
形如vec.x
这样的方式可以获取到向量中的各个分量,xyzw分别能获取到从第一到第四个分量。比如对上文中声明的变量,qwq.x
的值即为114.0。特别的,对于代表颜色的向量,你也可以用rgba来获取四个分量,四个分量分别代指了红色、绿色、蓝色和透明度。
同样的,你也可以用类似数组的方法访问向量中的元素:
vec3 color = vec3(1.0,0.5,0.5); float red = color[0]; float green = color[1]; float blue = color[2];
用length方法可以返回向量的维度,例如上文中,使用color.length()
会返回3。
有趣的是,在GLSL中还允许向量的重组,如下面所示
vec2 someVec; //声明了一个二维的向量 vec4 differentVec = someVec.xyxx; //声明的一个四维的向量 vec3 anotherVec = differentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVec.yxzy; //将someVec的第一位和anotherVec的第二位加起来赋值给otherVec的第一位,someVec的第一位和anotherVec的第一位加起来赋值给otherVec的第二位,以此类推
还可以像下面一样:
vec2 vect = vec2(0.5, 0.7); //声明一个二维向量 vec4 result = vec4(vect, 0.0, 0.0); //将vect的两位分别作为result的第一位和第二位 vec4 otherResult = vec4(result.xyz, 1.0); //将result的第一二三位作为otherResult的第一二三位
这种做法能大大缩短代码的长度,使得代码更加简洁。
矩阵
关于矩阵的数学基础是线性代数中讲到的,如果你没有学习线性代数相关的知识,我强烈推荐你阅读一下的材料进行学习。别担心,这里只有基础的矩阵知识,不会涉及到过于细节的线性代数知识。事实上,即使是刚刚初中毕业的学生也能很好的理解基本的线性代数的知识。
不推荐看这篇材料下面的glsl,因为这里涉及到了太多的glsl与C之间的联系,我们并不需要掌握这么多
基本类型 | 矩阵类型 |
---|---|
float | mat2 mat3 mat4 mat2x2 mat2x3 mat2x4 mat3x2 mat3x3 mat3x4 mat4x2 mat4x3 mat4x4 |
double | dmat2 dmat3 dmat4 dmat2x2 dmat2x3 dmat2x4 dmat3x2 dmat3x3 dmat3x4 dmat4x2 dmat4x3 dmat4x4 |
矩阵只能储存float和double类型的数据。matnxm
代表一个n列m行的矩阵,matn
则代表一个n列n行的矩阵。注意这里比较反常识,因为一般是行在前列在后,而在glsl中是列在前行在后。事实上如果将矩阵看作一个数组,这其实是很好理解的。可以看作是一个m长度的数组作为行,然后一共有n行。矩阵能够像二维数组一样访问,例如somemax[0][1]
就访问了第1列的第2个元素。在二维数组中也是先列后行,因此glsl的这种表示方法也变得很自然了。
矩阵的初始化主要有两种方式。第一种是对矩阵中的每个元素一次赋值。注意,赋值的时候是一列一列赋值的。例如:
//依次赋值 mat3 M1 = mat3(1.0,2.0,3.0, 1.0,3.0,2.0, 0.0,2.0,1.0); //用列向量进行赋值 vec3 column1 = vec3(1.0,2.0,3.0); vec3 column2 = vec3(1.0,3.0,2.0); vec3 column3 = vec3(0.0,2.0,1.0); mat3 M2 = mat3(column1,column2,column3); //用列向量赋值,但是列向量只有二维,因此需要额外添加一个数值 vec2 column1 = vec2(1.0,2.0); vec2 column2 = vec2(1.0,3.0); vec3 column3 = vec(0.0,2.0); mat3 M3 = mat3(column1,3.0, column2,2.0, column3,1.0);
第二种是初始化为对角矩阵:
mat3 tempMat = mat3(2.0);
能得到一个对角元素为2.0的矩阵。
除了直接访问到矩阵的值外,还能访问到矩阵的某一列。例如vec3 vec = M1[0]
即可获取M1矩阵的第一列。
和向量一样,对矩阵使用length()方法能得到矩阵中能包含的元素个数,这里就不举例子了。
输入与输出
着色器需要接受一个画面,处理之后输出这个画面,因此我们需要着色器拥有一个和其他着色器进行“交流”的输入和输出口。GLSL中定义了in
和out
两个关键字用于实现着色器的输入和输出。前者代表输入,用于从上一个着色器接受数据,后者代表输出,用于向下一个着色器输出数据。这样的变量叫做传递变量。当然,这里有两个例外。一个是顶点着色器,因为顶点着色器是直接接收顶点数据,因此顶点着色器需要专门配置location
这一元数据方便CPU配置顶点属性。幸运的是,因为我们现在是面向Minecraft编写着色器,我们并不需要考虑这么多。我们只需要写一行in vec4 Position;
就能获取到顶点信息了。这个四维向量储存了一个坐标值,表示显示的画面的一个角落的坐标(前面说到过,后处理着色器的顶点着色器只能处理已有游戏画面的四个顶点的位置)。另外一个例外是片段着色器,因为这个着色器产生最后输出的颜色,因此我们需要指定一个四维向量输出这个颜色,也就是写上这样一行out vec4 fragColor
。这样我们的着色器就能够输出它处理到的颜色了。而其他情况,就是需要输出数据的着色器声明一个out变量,需要从那个着色器输入数据的着色器则需要声明一个同名的in变量来。这样一个out一个in,就完成了着色器之间的数据传递。
下面是一个完整的例子
//顶点着色器 #version 150 in vec4 Position; //接受坐标 out vec4 vertexColor; // 为片段着色器指定一个颜色输出 void main() { vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色 }
//片段着色器 #version 150 out vec4 fragColor; // 要输出的这个像素的颜色 in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) void main() { fragColor = vertexColor; //赋值 }
你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4
输出,并在片段着色器中声明了一个类似的vertexColor。由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。
在Minecraft中,顶点着色器定义并赋值了一些非常有用的传递变量。texCoord
表示了一个从(0, 0)到(1, 1)的坐标,而oneTexel
则表示了texCoord
表示的坐标中,一个像素所占的大小。注意,我们说过,着色器是每一个像素每一个像素依次处理的,因此texCoord
和oneTexel
都不是一个固定的值,它在着色器具体被Minecraft调用的时候是不断变化的,依次遍历完每一个坐标。但是在写着色器内部的时候,它就只是代表了一个像素的位置,我们的着色器就这样针对一个像素进行处理,处理完再处理下一个。texCoord
所起到的作用,就像你在用for循环遍历数组时那个i变量的作用一样。
Uniform
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
同in和out变量一样,我们可以使用uniform
关键字来声明一个uniform变量。uniform变量的使用方法将会在后面的章节中讲解Minecraft资源包调用着色器时详细讲到。
那么到这里,我们关于GLSL的基础知识就差不多讲完了。GLSL还有很多细节是这里没有讲到的,而必要的细节将会在后面一一讲解到。