在Hello,OpenGL World文章中,我们并未过多的提及着色器,只是叫读者将顶点着色器、片段着色器的相关代码进行复制使用。虽然我们通过它们绘制出了三角形,但我们并不知道其中代码的任何含义。那么这篇文章我们便来讲讲这着色器。
着色器是运行在GPU上的把输入转化为输出的独立的小程序。书写着色器的语言我们叫做GLSL,GLSL是一种类C语言写成的。我们通过Hello,OpenGL World中使用的顶点着色器和片段着色器对相关知识进行说明。
// 顶点着色器
#version 300 es
layout (location = 0) in vec4 vPosition;
out vec4 vertexColor;
void main() {
gl_Position = vPosition;
vertexColor = vec4(1, 0.8, 0.0, 1.0);
}
// 片段着色器
#version 300 es
precision mediump float;
in vec4 vertexColor;
out vec4 fragColor;
void main() {
fragColor = vertexColor;
}
从以上代码我们可看出顶点着色器与片段着色器有许多的相似之处也有不同之处。
版本声明
声明了es的着色语言版本,用于检查着色器语言语法,一般来说每个着色器起始于一个版本说明,但是如果着色器不声明版本号,那么该着色器就会被认定为使用OpenGL ES着色语言1.00的版本,着色器语言的1.00版本适用于OpenGL ES 2.0。我们使用OpenGL ES 3.0版本,所以我们一般声明其版本为300即可。(比如GLSL 420版本对应于OpenGL 4.2)
变量和变量类型
在计算机图形中,两个基本的数据类型组成了变换的基础:向量和矩阵。着色器语言包含标量、向量和矩阵的数据类型。
a.标量
标量包含了C语言中集中基本的数据类型。float、int、bool、uint、double
b.向量
向量包含了由标量组成四种向量类型。vecn、ivecn、bvecn、uvecn,dvecn其中n表示的分量个数,例如在上面的顶点着色器中声明的vec4 vertexColor即表示vertexColor是有4个分量的基于浮点的向量类型。
变量构造器
OpenGL ES着色器在类型转换方面有非常严格的规则,变量的赋值、计算都只能是相同类型的变量,在着色器语言中不允许隐含类型转换。(类似于Swift)
a.使用构造器初始化和转换标量值
float myFloat = 1.0;
float myFloat = 1; // 错误, 类型不正确
bool myBool = true;
int myInt = 1;
int myInt1 = 1.0; // 错误, 类型不正确
myFloat = float(myBool); // 将bool类型转为float类型
myFloat = float(myInt); // 将int类型转为float类型
b.构造器初始化、转换向量数据类型
向量构造器参数的传递有两种基本方法:
1.只为向量构造器提供一个标量参数,则该值用于设置向量所有分量的值。
2.提供多个标量或者向量参数,向量的值从左至右使用这些参数设置,因此向量中必须有至少和参数中一样多的分量。
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0};
vec3 myVec3 = vec3(1.0, 0.5, 0.0);
vec3 tempVec3 = vec3(myVec3); // tempVec3 = {myVec3.x, myVec3.y, myVec3.z};
vec2 myVec2 = vec2(tempVec3); // myVec2 = {tempVec3.x, tempVec3.y};
myVec4 = vec4(myVec2, tempVec3) // myVec4 = {myVec2.xy, tempVec3.xy};
c.输入与输出
前面我们已经提到过,着色器是将输入转化为输出的一段独立的小程序来实现数据流的传输,恰好GLSL便定义了in和out关键字来实现改目的。每个着色器使用in和out来设定输入和输出的变量,只要输出变量和下一个输入变量的名称类型相同,数据就能正常的往下传递。例如我们前文贴出的两个着色器便可以很容易的找到对应的in和out。
out vec4 vertexColor; // 顶点着色器的输出变量
in vec4 vertexColor; // 片段着色器的输入变量
顶点着色器可以直接在顶点数据中直接接收输入。在前一章渲染三角形时,我们需要告诉OpenGL如何去解析顶点数据,为了定义顶点数据如何管理,我们使用location这一元数据指定输入变量,当然你也可以使用glGetAttribLocation函数查询属性位置,但为了更好地理解和减少OpenGL的工作量,我们直接在着色器中设置它们。
在片段着色器中,我们有一个颜色输出变量fragColor,因为片段着色器需要生成一个最终输出的颜色,如果在片段着色其中没有定义输出颜色,OpenGL会把你定义的物体渲染成黑色。因此要实现从一个着色器传递数据至另一个着色器,在发送方必须要声明输出,在接收方声明输入。
d.Uniform
Uniform是CPU中的应用向GPU中的着色器发送数据的方式,它声明的变量是一个全局的变量,也就是说该变量会保存其最新数据,并可被着色器程序的任意着色器在任意阶段访问。我们适当修改一下绘制三角形所用到的片段着色器,使用Uniform来设置三角形的颜色。
#version 300 es
precision mediump float;
out vec4 fragColor;
uniform vec4 ourColor;
void main() {
fragColor = ourColor;
}
需要注意的是,更新一个Uniform需要在调用glUseProgram之后,因此,我们可以如下设置Uniform声明的变量。
int vertexColorLoaction = glGetUniformLocation(self.program, "ourColor");
glUniform4f(vertexColorLoaction, 0.4, 0.3, 0.6, 1);
glUniform函数有特定的后缀,用于标识设定的Uniform类型。在这里我们是为了设置一个RGBA的颜色,所以我们使用glUniform4f函数。glGetUniformLocation函数是用于查询Uniform变量ourColor的位置值,当返回的数值为-1时,便代表没有找到这个位置值,所以为了安全起见,我们可以在此多做一个判断并打印相关错误,为我们调试提供更多的信息。该案例在Learn_OpenGLES_Demo中的TwoTriangles中。
e.main函数
main函数是每一个着色器的入口点,在这个函数中处理所有的输入变量,并将结果输出到输出变量中。(可联想C语言的main函数)
多属性的顶点数据
在绘制三角形时,我们只在vertices数组中配置了位置坐标,那么在这里,我们打算把各个顶点的颜色也配置在vertices数组中。
float vertices[] = {
// 位置 // 颜色
0.0, 0.5, 0.0, 1.0, 0.0, 0.0,
-0.5, -0.5, 0.0, 0.0, 1.0, 0.0,
0.5, -0.5, 0.0, 0.0, 0.0, 1.0
};
由于我们现在还有颜色的相关信息在顶点数据中,所以我们可以思考该如何书写我们的顶点着色器。
按照前文的讲述,一个着色器是由版本信息开始的,所以我们声明对应的版本信息。
#version 300 es
并且片段着色器需要一个输入来知道最终的颜色,所以在顶点着色器中理应包含一个输入用于告知片段着色器绘制的最终颜色。为了更好地熟悉变量构造器的相关知识点。我们这里使用vec3类型来声明输出的颜色。
out vec3 ourColor;
并且在前面我们也介绍过,顶点着色器与片段着色器的一些不同点,顶点着色器可以使用location来指定输入变量,因为我们的顶点数据的坐标包含三个分量,颜色也含有三个分量,所以我们使用vec3来声明输入的值。但要注意的是我们需要设定一下location的值。
layout (location = 0) in vec3 aPos; // 声明位置输入,位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 声明颜色输入,颜色变量的属性位置为1
最后便是main函数,将处理完的输入变量的结果输出到输出变量中。
gl_Position = vec4(aPos, 1);
ourColor = aColor;
到这里,我们自己书写的一个顶点着色器就完成了,具体如下:
#version 300 es
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
out vec3 ourColor;
void main() {
gl_Position = vec4(aPos, 1);
ourColor = aColor;
}
在片段着色器方面,因为最终物体绘制的颜色是通过顶点着色器传入的,所以片段着色器必然有一个输入用于接收顶点着色器的输出。
in vec3 ourColor;
在此之前,我们还是一样声明版本并设置精度。
#version 300 es
precision mediump float;
最后声明一个输出的变量,将接收到的ourColor的值赋值给输出变量。
out vec4 fragColor;
最后实现main函数就完成了一个简单地片段着色器。
void main() {
fragColor = vec4(ourColor, 1.0);
}
在着色器书写完成后,我们实现上下文的创建,着色器的创建,编译,链接后,还需要告诉OpenGL如何解析顶点数据。所以调用顶点数组指针函数应该如下:
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);
小总结:将多个属性写入顶点数据中需要注意的是在告诉OpenGL如何解析顶点数据时的步进值以及偏差时,并在书写顶点着色器时注意location的位置(也可通过函数获取)。
运行程序,得到以下效果:
该案例的具体实现LearnShaders,SMLearnShader.fsh,
SMLearnShader.vsh
学习OpenGL的所有Demo均可在Github下载。