OpenGL学习 (二)

OpenGL学习 (二)

这一节回顾OpenGL的着色器语言GLSL。

GLSL数据类型

和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix)。

全数据类型

变量类别 变量类型 备注
void 用于标识无参函数或者无返回值函数:void main(void);
标量 bool/int/float 布尔类型、整型、浮点型
布尔型向量 bvec2/bvec3/bvec4 其中b表示向量类型,数字表示向量的分量数
整型向量 ivec2/ivec3/ivec4 其中i表示向量类型,数字表示向量的分量数
浮点型向量 vec2/vec3/vec4 默认情况下是float类型,数字表示向量的分量数
浮点型矩阵 mat2/mat3/mat4 数字表示矩阵的列数,行数和列数相同
2D texture sampler2D 2D纹理,仅能作为uniform变量
Cubemap(立方体) texture samplerCube 立方体纹理,仅能作为uniform变量
结构体 struct 类似于C语言结构体,把多个变量聚合在一起
数组 array GLSL只支持1维数组,数据类型可以是标量类型、向量类型、矩阵类型、结构体类型

向量

GLSL中的向量是一个可以包含有1、2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表分量的数量):

类型 含义
vecn 包含n个float分量的默认向量
bvecn 包含n个bool分量的向量
ivecn 包含n个int分量的向量
uvecn 包含n个unsigned int分量的向量
dvecn 包含n个double分量的向量

大多数时候我们使用vecn,因为float足够满足大多数要求了。

一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量。你可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4个分量。GLSL也允许你对颜色使用rgba,或是对纹理坐标使用stpq访问相同的分量。

向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。重组允许这样的语法:

1
2
3
4
5
vec2 someVec;
//重组
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

你可以使用上面4个字母任意组合来创建一个和原来向量一样长的(同类型)新向量,只要原来向量有那些分量即可;然而,你不允许在一个vec2向量中去获取.z元素。

我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:

1
2
3
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

矩阵

这里主要谈论的是矩阵的构造,这个比较容易弄混淆!
矩阵构造器主要有两种构造方式:
1.单标量参数:用于主对角线上分量的初始化,其他分量皆为0.0。
2.多标量参数、向量参数、或者标量和向量混合参数:按照参数顺序初始化矩阵的所有分量(列优先),需要保证参数个数(向量参数的分量拆分开)不少于矩阵分量个数。

GLSL
1
2
3
4
5
6
7
8
9
10
11
12
// 通过多个标量为矩阵的各个分量赋值
mat3 aMat3 = mat3(1.0, 0.0, 0.0, // 第一列
0.0, 1.0, 0.0, // 第二列
0.0, 0.0, 1.0); // 第三列
// 单个标量用于主对角线上分量的初始化,其他分量皆为0.0
mat3 bMat3 = mat3(1.0);

vec3 aVec3 = vec3(1.0,0.0,0.0);
vec3 bVec3 = vec3(0.0,1.0,0.0);
mat3 cMat3 = mat3(aVec3, // 通过向量初始化第一列
bVec3, // 通过向量初始化第二列
0.0, 0.0, 1.0); // 通过多个标量初始化第三列

非常需要注意的是你用向量初始化矩阵的时候每个向量初始化的是矩阵的一列!而在CG语言(包括unity的shaderLab)中我们初始化的时候是先初始化的一行!两者的区别在于前者OpenGL是列优先的,而后者是行优先的。因此这两种方式构造矩阵刚好是一个转置的关系!这在之后理解切线空间TBN矩阵的时候会有影响,如果不理解会感到疑惑为什么两者用类似的方法构造矩阵在运算的时候表示的意思总是相反的 (假如你要用别的着色器语言实现的话你会碰到这个问题)!


输入与输出

GLSL定义了inout关键字,每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。顶点着色器和片元着色器输入输出的模板略有不同:

顶点着色器

顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。为了指定location我们需要给顶点着色器的输入提供一个额外的layout标识,这样我们才能关联到相应的顶点数据,如:layout (location = 0)

顶点着色器可以给片段着色器传递变量。我们必须在发送方着色器(顶点着色器)中声明一个输出,在接收方着色器中(片段着色器)声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。

1
2
3
4
5
6
7
8
9
10
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0

out vec4 vertexColor; // 为片段着色器指定一个颜色输出,下面的片段着色器中有一个同名的输入变量

void main()
{
gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}

片段着色器

片段着色器的输入数据,来自于顶点着色器,我们通过声明in变量来接收来自顶点着色器的输出。

片段着色器的输出,需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。

1
2
3
4
5
6
7
8
9
10
#version 330 core
//片段着色器最终输出颜色,送给后面的流水线处理
out vec4 FragColor;

in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

void main()
{
FragColor = vertexColor;
}

Uniform

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问(我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介)。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。

如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!

给Uniform添加数据

我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。

1
2
3
4
//为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

glGetUniformLocation查询uniform ourColor的位置值,如果glGetUniformLocation返回-1就代表没有找到这个位置值。最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。

glUniform这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有:

后缀 含义
f 函数需要一个float作为它的值
i 函数需要一个int作为它的值
ui 函数需要一个unsigned int作为它的值
3f 函数需要3个float作为它的值
fv 函数需要一个float向量/数组作为它的值

uniform变量使用流程

1)在需要使用uniform变量的shader(vertex shader或者fragment shader都可以)中声明变量。

2)给uniform变量添加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*这里是让三角形颜色慢慢变换例子程序的循环部分*/
while(!glfwWindowShouldClose(window))
{
// 输入
processInput(window);

// 渲染
// 清除颜色缓冲
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

// 记得激活着色器
glUseProgram(shaderProgram);

// 更新uniform颜色
//获取运行的秒数
float timeValue = glfwGetTime();
float greenValue = sin(timeValue) / 2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
//我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

// 绘制三角形
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

// 交换缓冲并查询IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}

参考资料

LearnOpenGL CN

OpenGL之GLSL

评论