原博主博客地址:http://blog.csdn.net/qq21497936
本文章博客地址:http://blog.csdn.net/qq21497936/article/details/78943946
OpenGL学习笔记(十):深入学习和理解着色器
、
前话
前面的章节介绍了基本的VAO、VBO、EBO,使用的都是最基础的着色器,本章节对着色器进行深入的学习。
节日祝福
2017年12月31日23:51完成,祝大家2018年元代快乐!!!
着色器
着色器(Shader)是运行再GPU上的小程序,这些小程序为图形渲染管线的某个特定部分而运行,从基本意义上来说,着色器只是一种把输入转换为输出的程序,其是独立的程序,它们之间不能通信,唯一的沟通就是输入和输出。
GLSL
着色器使用一种叫GLSL的类C语言写成的。GLSL是为为图形计算量身定制的,它包含一些针对响亮和矩阵操作的有用特性。
着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,再这个函数中处理所有的输入变量,并将结果输出的输出变量中。
一个典型的着色器程序如下:
#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; }
顶点着色器的每个输入变量成为顶点属性(Vertex Attribute),一个着色器可声明的顶点属性是又上限的,它一般由硬件来决定。OpenGL确保至少16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,可以查询GL_MAX_VERTEX_ARRIBS来获取具体的上限:
int nrAttributes; glGetIntegerv(GL_MAX_VERTEx_ARRIBS, &nrAttributes); std::out << “Maximum nr of vertex attributes supported: “ << nrArribute << std::endl;
数据类型
GLSL有数据类型可以来指定变量的种类,GLSL中包含C等其他语言大部分的默认基础数据类型:int、float、double、uint和bool。GLSL还还有两种容器类型,分别是向量(Vector)和矩阵(Matrix)。
向量
GLSL中的向量是一个可以包含1、2、3或4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表分量的数量):
类型 |
含义 |
vecn |
包含n个float分量的默认向量 |
bvecn |
包含n个bool分量的向量 |
lvecn |
包含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)。重组允许这样的语法:
vec2 someVec; vec4 differentVec = someVec.xyxx; vec3 anotherVec = diffrentVec.zyw; vec4 otherVec = someVec.xxxx + anotherVer.yxzy;
可以使用上面4个字母任意组合来创建一个和原来向量一样长的(同类型)心想辆,只要原来向量又那些分量即可;然后,不允许再一个vec2向两种去获取.z元素。可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:
vec2 vect = vec2(0.5, 0.7); vec4 result = vec4(vect, 0.0, 0.0); vec4 otherResult = vect(result, 1.0);
输入与输出
着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,每个着色器必须都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去,但在顶点和片段着色器中会有点不同,这是两个意外。
顶点着色器应该接受的是一种特殊形式的输入,否则就会效率底下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元素指定输入变量,这样才能在CPU上配置顶点属性。“layout (location=0)”,顶点着色器需要为它的输入提供一个额外的layout标识,这样才能把它链接到顶点数据。
片段着色器,它需要一个vect4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色,如果在片段着色器没有定义输出颜色,OpenGL会把物理渲染为黑色(或白色)。
所以,一个着色器向另一个着色器发送数据,我们必须再发送方着色器一个输出再接收方着色器中声明一个类型的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。为下面展示一下,让顶点着色器为片段着色器决定颜色。
在基础程序上编码解说,本章节基础程序使用《OpenGL学习笔记(八):进一步理解VAO、VBO和SHADER,并使用VAO、VBO和SHADER绘制一个三角形》完成的demo。
文章地址: http://blog.csdn.net/qq21497936/article/details/78888286
demo2下载: http://download.csdn.net/download/qq21497936/10171739
按照下面的代码修改顶点和片段着色器
#if 0 // 1.顶点着色器 const char *vertexShaderSource = \ "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}"; #else // 1.顶点着色器 着色器章节修改后的 const char *vertexShaderSource = \ "#version 330 core\n" "layout (location = 0) in vec3 aPos;\n" "out vec4 vertexColor;\n" // 新增,为片段着色器指定一个颜色输出 "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" " vertexColor = vec4(0.5, 0.0, 0.0, 1.0); " // 新增,为片段着色器指定一个颜色输出 "}"; #endif #if 0 // 2.片段着色器 const char *fragmentShaderSource = \ "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}"; #else // 2.片段着色器 着色器章节修改后的 const char *fragmentShaderSource = \ "#version 330 core\n" "out vec4 FragColor;\n" "in vec4 vertexColor;\n" // 新增,从顶点着色器传来的输入变量(名称相同、类型相同) "void main()\n" "{\n" " FragColor = vertexColor;\n" // 修改,将输入颜色(顶点着色器的颜色输出)转换为输出颜色 "}"; #endif
这里特别注意换行符号和分号(推荐读者写在单独的文件中读取字符串,该笔记为了方便阅读就不多文件了)。
运行效果如图(成功传递颜色):
Uniform
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。
第一:unifrom是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器再任意阶段访问。
第二:无论把unifrom值设置成什么,unifrom会一直保存它们的数据,知道它们被重置或更新。
因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;glUniform是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有:
后缀 |
含义 |
F |
函数需要一个float作为它的值 |
I |
函数需要一个int作为它的值 |
ui |
函数需要一个unsigned int作为它的值 |
3f |
函数需要3个float作为它的值 |
fv |
函数需要一个float向量/数组作为它的值 |
每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在下面的例子里,我们希望分别设定uniform的4个float值,所以我们通过glUniform4f传递我们的数据(注意:我们也可以使用fv版本)。
下面,我们在一个着色器中添加uniform关键字声明一个GLSL的uniform,从此处开始,我们就可以再着色器中使用新声明的uniform了,通过unifrom设置三角形的颜色为绿色随着时间变化,分为两个步骤,如下
步骤一:着色器中声明uniform全局变量;(注意:如果声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误)
#if 0 // 2.片段着色器 const char *fragmentShaderSource = \ "#version 330 core\n" "out vec4 FragColor;\n" "void main()\n" "{\n" " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n" "}"; #else #if 0 // 2.片段着色器 着色器章节修改后的 const char *fragmentShaderSource = \ "#version 330 core\n" "out vec4 FragColor;\n" "in vec4 vertexColor;\n" // 新增,从顶点着色器传来的输入变量(名称相同、类型相同) "void main()\n" "{\n" " FragColor = vertexColor;\n" // 修改,将输入颜色(顶点着色器的颜色输出)转换为输出颜色 "}"; #else // 2.片段着色器 着色器章节修改后的 增加了unifrom全局变量 const char *fragmentShaderSource = \ "#version 330 core\n" "out vec4 FragColor;\n" "uniform ver4 outColor" "void main()\n" "{\n" " FragColor = outColor;\n" "}"; #endif #endif
步骤二:uniform现在为空,找到着色器中unifrom属性的索引/位置值,得到后即可更新;
while (!glfwWindowShouldClose(window)) { // 捕捉输入 processInput(window); // 清空颜色缓冲 glClearColor(1.0f, 1.0f, 1.0f, 0.0f); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(shaderProgram); glBindVertexArray(VAO); glViewport(0, 0, 600, 600); // 新增,着色器章节全局变量修改 float timeValue = glfwGetTime(); float greenValue = (sin(timeValue)); int vertexColorLocation = glGetUniformLocation(shaderProgram, "outColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // 绘制三角形:三角形 数组起始索引 绘制多少个顶点 glDrawArrays(GL_TRIANGLES, 0, 3); glfwSwapBuffers(window); glfwPollEvents(); }
首先我们通过glfwGetTime()获取运行的秒数。然后我们使用sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。
接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。如果glGetUniformLocation返回-1就代表没有找到这个位置值。最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。
运行效果:
融合颜色数据到顶点数据中
前面把顶点数据配置到VAO里,还可以把颜色数据加进顶点数据中,我们把三个三角形分别指定为红色、绿色和蓝色:
float vertices[] = { // 位置 // 颜色 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部 };
顶点着色器代码修改:
// 1.顶点着色器 着色器章节修改后的 const char *vertexShaderSource = \ "#version 330 core\n" "layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0 \n" "layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1 \n" "out vec4 vertexColor;\n" // 新增,为片段着色器指定一个颜色输出 "void main()\n" "{\n" " gl_Position = vec4(aPos, 1.0);\n" " vertexColor = vec4(aColor, 1.0); " // 新增,为片段着色器指定一个颜色输出 "}";
片段着色器代码修改:
// 2.片段着色器 const char *fragmentShaderSource = \ "#version 330 core\n" "out vec4 FragColor;\n" "in vec4 vertexColor;\n" "void main()\n" "{\n" " FragColor = vertexColor;\n" "}";
再使用glVertexAttribPointer函数更新顶点格式:
// 设置顶点属性指针,glVertexAttribPointer()函数告诉OpenGL该如何解析顶点数据 #if 0 // 顶点属性位置 顶点属性大小 数据的类型 是否被标准化 步长 偏移 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void *)0); // glEnableVertexAttribArray()以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的 glEnableVertexAttribArray(0); #else // 位置属性 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); #endif
我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:
运行效果
demo下载地址:http://download.csdn.net/download/qq21497936/10182590
原博主博客地址:http://blog.csdn.net/qq21497936
本文章博客地址:http://blog.csdn.net/qq21497936/article/details/78943946