原文档地址:着色器 - LearnOpenGL CN (learnopengl-cn.github.io)
https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/
一.前言
本文是对LearnOpenGL中入门板块的着色器篇进行一些重点的记录和总结,内容基本来自上述文档,建议详细去看原文档进行学习;
二.GLSL
2.1着色器是使用一种叫GLSL的类C语言写成的,GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。
2.2着色器开头要声明版本,输入和输出变量、uniform和main函数。
-->>一个典型的着色器应当有以下的结构:
#version version_number//版本名字;
in type in_variable_name;//输入变量类型和变量名;
in type in_variable_name;
out type out_variable_name;//输出变量类型和变量名;
uniform type uniform_name;//uniform类型以及uniform的变量名;
int main()//main函数;
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
2.3当谈到顶点着色器的时候,每一个输入的变量也称为:顶点属性(Vertex Attribute),其中可以声明的顶点属性类型是有上限的,一般由硬件决定;OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性;可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限,具体代码如下:
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);//获取顶点个数;
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;//打印;
通常情况下它至少会返回16个;
三.数据类型
GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。
GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix);
3.1向量
3.1.1向量分量
GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表分量的数量):
大多数时候我们使用vecn,因为float足够满足大多数要求了;
一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量。你可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4个分量;
GLSL也允许你对颜色使用rgba,或是对纹理坐标使用stpq访问相同的分量;
3.1.2向量分量重组(Swizzling)
向量分量重组允许如下的操作:
vec2 someVec=(0.5,0.7);//创建一个二维向量;
vec4 differentVec = someVec.xyxx;//创建一个四维向量,someVec.xyxx代表向量(0.5,0.7,0.5,0.5);
vec3 anotherVec = differentVec.zyw;//创建一个三维向量,differentVec.zyw代表(0.5,0.7,0.5);
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;//创建一个新的四维向量,结果为:
//(0.5,0.5,0.5,0.5)+(0.7,0.5,0.5,0.7)=(1.2,1.0,1.0,1.2);
注意:你不允许在一个vec2向量中去获取.z元素;
我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);//将二维向量vect作为参数传入;
vec4 otherResult = vec4(result.xyz, 1.0);//将三维向量result作为参数传入;
四.输入和输出
虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同;
4.1顶点着色器的输入输出
顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性 [layout (location = 0)](在前面的三角形章节中,location = 0代表的是顶点属性中的顶点位置,可以将location设置为1,2,3...来表示其他属性)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据;
{你也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量。}
4.2片段着色器的输入输出
片段着色器,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
!!!所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。
4.3从顶点着色器向片段着色器发送信息
先看两个着色器的代码;
4.3.1顶点着色器
#version 330 core//OpenGL版本为3.3则为330;
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.4,0.3,1.0);//设置输出变量的值;
}
[ 对于layout(location = 0) in vec3 aPos;如果输入变量占用了多个属性位置槽,那么将按照属性位置顺序依次分配,举个例子:
layout(location = 2) in vec3 values[4];
values将依次获取属性位置2,3,4,5处的值。]
4.3.2片段着色器
#version 330 core
out vec4 FragColor;//输出的变量为FragColor,是一个四维向量;
in vec4 vertexColor;//与顶点着色器的输出类型和变量名相同,OpenGL会自动进行链接!!;
void main()
{
FragColor = vertexColor;
}
你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了!!;
此时我们在顶点着色器中设置的颜色,在经过片段着色器操作输出后会产生同样的颜色效果,例如将顶点着色器中的vertexColor向量设置为暗红色,结果也为暗红色,如下:
到此,我们成功地从顶点着色器向片段着色器发送数据,接下来要做的是:
看看能否从应用程序中直接给片段着色器发送一个颜色!
五.Uniform
5.1Uniform的定义和性质
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新;
5.2设置Uniform变量
我们可以在一个着色器中添加uniform关键字类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色:
#version 330 core
out vec4 FragColor;
uniform vec4 OurColor;//OurColor变量要在OpenGL的程序代码中设定
void main()
{
FragColor = OurColor;
}
//注意:如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!
此处在片段着色器中声明了一个uniform vec4的ourColor,并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介;顶点着色器中不需要这个uniform,所以我们不用在那里定义它!!!
5.3Uniform数据添加
这个uniform现在还是空的;我们还没有给它添加任何数据。
我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色:
float timeValue = glfwGetTime();//获取运行的秒数;
float greenValue = (sin(timeValue)/2.0f)+0.5;//目的是要创建一个渐变的三角形,这里用sin函数进行模操作,使得最后获得的颜色值在0.0到1.0之间(颜色值的范围);
int vertexColorLocation = glGetUniformLocation(shaderProgram,"OurColor");//这里用来获取Uniform类型的变量OurColor的值,注意传入的数据包括一个着色器程序shaderProgram,以及要传入Uniform类型变量的名字OurColor(注意:如果glGetUniformLocation的返回值是-1,则说明不存在这个Uniform变量OurColor);
glUseProgram(shaderProgram);//要先对着色器程序进行激活,才可以进一步对Uniform变量的值进行修改;
glUniform4f(vertexColorLocation,0.0f,greenValue,0.0f,1.0f);//这里我们希望设定Uniform4个不同的值,glUniform的下缀表示如下:
1.首先我们通过glfwGetTime()获取运行的秒数。然后我们使用sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。
2.接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。如果glGetUniformLocation返回-1就代表没有找到这个位置值。
3.最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。
5.4使用Uniform来进行渲染
在进行渲染过程中,如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代(也就是在渲染循环中)中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。下面我们就计算greenValue然后每个渲染迭代都更新这个uniform:
#include <glad/glad.h>
#include <GLFW\glfw3.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
"}\0";
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"uniform vec4 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = ourColor;\n"
"}\n\0";
int main()
{
//先进行初始化glfw;
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本设置为3;
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本设置为3;
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "MY OPENGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Fail to create a window" << std::endl;
glfwTerminate();//释放资源;
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//创建完告诉将上下文设置为进程上下文;
//对glad进行初始化;
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Fail to initnite glad" << std::endl;
return -1;
}
//创建顶点着色器以及片段着色器;
//顶点着色器;
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//将着色器源码附到vertexShader上;
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);//判断是否出错;
//片段着色器;
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//把着色器源码附到上面;
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//着色器程序;
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
//对着色程序进行链接;
glLinkProgram(shaderProgram);
//链接完删除两个对象;
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
//float vetices[] =
//{
// -0.5f,0.5f,0.0f,
// 0.5f,-0.5f,0.0f,
// 0.0f,0.5f,0.0f
//};
float firstTriangle[] = {
-0.5f, 0.8f, 0.0f, // left
-0.5f, -0.3f, 0.0f, // right
0.0f, 0.0f, 0.0f, // top
};
float secondTriangle[] = {
0.5f, 0.3f, 0.0f, // left
0.0f, 0.0f, 0.0f, // right
0.5f, -0.8f, 0.0f // top
};
unsigned int VAO[2], VBO[2];
glGenVertexArrays(2, VAO);//创建两个VAO;
glGenBuffers(2, VBO);//创建两个VBO;
//先对两个VAO,VBO进行绑定;
//第一个三角形绑定;
glBindVertexArray(VAO[0]);
glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(firstTriangle), firstTriangle, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);//进行顶点属性链接;
//0为顶点属性的位置,3为指定顶点属性的大小(为一个vec3的数组),3*sizeof(float)指步长,下个数组位置在这个步长之后;
glEnableVertexAttribArray(0);//启用此顶点属性;
//对第二个三角形进行绑定;
glBindVertexArray(VAO[1]);
glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
glBufferData(GL_ARRAY_BUFFER, sizeof(secondTriangle), secondTriangle, GL_STATIC_DRAW);//将第二个三角形的顶点位置信息传入;
//GL_STATIC_DRAW定义显卡如何管理数据;此处代表数据不会或者几乎不会改变很多;
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);//启动顶点属性;
//准备引擎:
while (!glfwWindowShouldClose(window))
{
//准备输入:
processInput(window);
//每一次更新缓冲颜色,之后继续画下一帧;
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);//清除颜色缓冲单元;
//设置uniform的值;
double timeValue = glfwGetTime();
float greenValue = static_cast<float>(sin(timeValue) / 2.0 + 0.5);
float redValue = static_cast<float>(sin(timeValue) / 2.0 + 0.1);
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, redValue, greenValue, 0.4f, 1.0f);
//float timeValue = glfwGetTime();
//float greenValue = (sin(timeValue) / 2.0f) + 0.5f;//修改rgb中b位置的颜色值;
//int vertexColorLocation = glGetUniformLocation(shaderProgram, "OurColor");
//glUseProgram(shaderProgram);//首先启用着色程序;
//glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
//下面渲染两个三角形,用从uniform中设置的函数;
//第一个三角形;
glBindVertexArray(VAO[0]);//绑定VAO;
glDrawArrays(GL_TRIANGLES, 0, 3);
//第二个三角形;
glBindVertexArray(VAO[1]);
glDrawArrays(GL_TRIANGLES, 0, 3);
//开始绘制;
//glDrawArrays(GL_TRIANGLES, 0, 3);//第一个参数是要进行绘制的类型,第二个参数制定了顶点数组的开始索引,
//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//注意:6是指一个要绘制六个顶点;!!
//第三个参数是要进行绘制的顶点的长度;
//更新缓冲;
glfwSwapBuffers(window);
//进行检查;
glfwPollEvents();
}
//结束要删除VAO,VBO,以及删除着色器程序;
glDeleteVertexArrays(2, VAO);
glDeleteBuffers(2, VBO);
glDeleteProgram(shaderProgram);//删除着色器程序;
//结束清楚资源;
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//如果按下的键为回车键;
glfwSetWindowShouldClose(window, true);
}
结果如下:
三角形的颜色有一个渐变的过程;
六.更多属性
在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:
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 // 顶部
};
由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用layout标识符来把aColor属性的位置值设置为1:
//对顶点着色器的修改;
#version 330 core
layout(location = 0) in vec3 aPos;//将位置的属性值设置为0;
layout(location = 1) in vec3 aColor;//将顶点颜色的属性值设置为1;
out vec3 QueColor;//向片段着色器发送输出数据;
int main()
{
gl_Position = vec4(aPos,1.0);
OurColor = aColor;//将颜色设置为我们从顶点数据中得到的颜色向量;
}
此处我们不再用uniform来进行颜色读取,因此我们也要修改片段着色器;
#version 330 core
in vec3 QurColor;
out vec3 FragColor;
void main()
{
FragColor = QurColor;
}
此外,由于传入了更多的顶点属性,因此也要重新配对顶点属性指针,更新VBO;
因此我们要使用glVertexAttribPointer函数更新顶点格式:
// 位置属性
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);//启动颜色属性,由于颜色属性的location是1,因此这里要填1;
glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。
由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。
!!!同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3 * sizeof(float),用字节来计算就是12字节。
全部代码如下:
#include <glad/glad.h>
#include <GLFW\glfw3.h>
#include <iostream>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
const char* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"layout (location = 1) in vec3 aColor;\n"
"out vec3 ourColor;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
" ourColor = aColor;\n"
"}\0";
const char* fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"in vec3 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(ourColor, 1.0f);\n"
"}\n\0";
int main()
{
//先进行初始化glfw;
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本设置为3;
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本设置为3;
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "MY OPENGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Fail to create a window" << std::endl;
glfwTerminate();//释放资源;
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//创建完告诉将上下文设置为进程上下文;
//对glad进行初始化;
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Fail to initnite glad" << std::endl;
return -1;
}
//创建顶点着色器以及片段着色器;
//顶点着色器;
unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
//将着色器源码附到vertexShader上;
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);//判断是否出错;
//片段着色器;
unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
//把着色器源码附到上面;
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
//着色器程序;
unsigned int shaderProgram;
shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
//对着色程序进行链接;
glLinkProgram(shaderProgram);
//链接完删除两个对象;
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
//float vetices[] =
//{
// -0.5f,0.5f,0.0f,
// 0.5f,-0.5f,0.0f,
// 0.0f,0.5f,0.0f
//};
float vertices[] = {
// 位置 // 颜色
0.5f, -0.5f, 0.0f, 0.8f, 0.4f, 0.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.6f, // 左下
0.0f, 0.5f, 0.0f, 0.4f, 0.0f, 1.0f // 顶部
};
unsigned int VAO, VBO;
glGenVertexArrays(1, &VAO);//创建VAO;
glGenBuffers(1, &VBO);//创建VBO;
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER,VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//链接顶点属性;
//位置属性;
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);//启用颜色属性;
//准备引擎:
while (!glfwWindowShouldClose(window))
{
//准备输入:
processInput(window);
//每一次更新缓冲颜色,之后继续画下一帧;
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);//清除颜色缓冲单元;
glUseProgram(shaderProgram);//启动着色器程序;
glBindVertexArray(VAO);//绑定VAO;
glDrawArrays(GL_TRIANGLES, 0, 3);
//开始绘制;
//glDrawArrays(GL_TRIANGLES, 0, 3);//第一个参数是要进行绘制的类型,第二个参数制定了顶点数组的开始索引,
//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//注意:6是指一个要绘制六个顶点;!!
//第三个参数是要进行绘制的顶点的长度;
//更新缓冲;
glfwSwapBuffers(window);
//进行检查;
glfwPollEvents();
}
//结束要删除VAO,VBO,以及删除着色器程序;
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);//删除着色器程序;
//结束清楚资源;
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//如果按下的键为回车键;
glfwSetWindowShouldClose(window, true);
}
输出结果如下所示:
这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。
基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。比如说,我们有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。
这正是在这个三角形中发生了什么。我们有3个顶点,和相应的3个颜色,从这个三角形的像素来看它可能包含50000左右的片段,片段着色器为这些像素进行插值颜色。片段插值会被应用到片段着色器的所有输入属性上。
七.自己的着色器类
为了使得在编译,管理着色器时较为方便,我们可以自己创建一个着色器类,它可以从硬盘读取着色器,然后编译并链接它们,并对它们进行错误检测;
我们会把着色器类全部放在在头文件里,主要是为了学习用途,当然也方便移植。我们先来添加必要的include,并定义类结构:
#ifndef SHADER_H
#define SHADER_H
#include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
class Shader
{
public:
// 程序ID
unsigned int ID;
// 构造器读取并构建着色器
Shader(const char* vertexPath, const char* fragmentPath);
// 使用/激活程序
void use();
// uniform工具函数
void setBool(const std::string &name, bool value) const;
void setInt(const std::string &name, int value) const;
void setFloat(const std::string &name, float value) const;
};
#endif
在上面,我们在头文件顶部使用了几个预处理指令(Preprocessor Directives)。这些预处理指令会告知你的编译器只在它没被包含过的情况下才包含和编译这个头文件,即使多个文件都包含了这个着色器头文件。它是用来防止链接冲突的。
着色器类储存了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径,这样我们就可以把源码的文本文件储存在硬盘上了。除此之外,为了让我们的生活更轻松一点,还加入了一些工具函数:use用来激活着色器程序,所有的set…函数能够查询一个unform的位置值并设置它的值。
八.从文件读取
8.1步骤解析
我们使用C++文件流读取着色器内容,储存到几个string对象里:
Shader(const char* vertexPath, const char* fragmentPath)
{
// 1. 从文件路径中获取顶点/片段着色器
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// 保证ifstream对象可以抛出异常:
vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
try
{
// 打开文件
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// 读取文件的缓冲内容到数据流中
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// 关闭文件处理器
vShaderFile.close();
fShaderFile.close();
// 转换数据流到string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch(std::ifstream::failure e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
[...]
下一步,我们需要编译和链接着色器。注意,我们也将检查编译/链接是否失败,如果失败则打印编译时错误,调试的时候这些错误输出会及其重要(你总会需要这些错误日志的):
// 2. 编译着色器
unsigned int vertex, fragment;
int success;
char infoLog[512];
// 顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// 打印编译错误(如果有的话)
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
// 片段着色器也类似
[...]
// 着色器程序
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// 打印连接错误(如果有的话)
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success)
{
glGetProgramInfoLog(ID, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);
use函数非常简单:
void use()
{
glUseProgram(ID);
}
uniform的setter函数也很类似:
void setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
现在我们就写完了一个完整的着色器类。使用这个着色器类很简单;只要创建一个着色器对象,从那一点开始我们就可以开始使用了:
8.2完整代码
完整代码如下:
Shader s.h
#ifndef SHADER_H //先测试x是否被宏定义过
#define SHADER_H //如果没有宏定义下面就宏定义x并编译下面的语句
#include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件
#include <string>
#include <fstream> //file stream ,fstream是C++ STL中对文件操作的合集,包含了常用的所有文件操作。
#include <sstream> //字符串流,可以支持C风格的串流的输入输出操作。
#include <iostream>
#endif //如果已经定义了则编译#endif后面的语句
class Shader
{
public:
//着色器程序ID;
unsigned int ID;
//构造器读取并构建着色器;
Shader(const char* vertexPath, const char* fragmentPath)
{
std::string vertexCode;//用来接收顶点着色器源码的string格式;
std::string fragmentCode;//用来接收片段着色器源码的string格式;
std::ifstream vShaderFile;//ifstream为从硬盘到内存的文件流对象,这里用来接收顶点着色器的文件流对象;
std::ifstream fShaderFile;//这里用来接收片段着色器的文件流对象;
//保证ifstream对象可以抛出异常;
vShaderFile.exceptions(std::ifstream::badbit | std::ifstream::failbit);
fShaderFile.exceptions(std::ifstream::badbit | std::ifstream::failbit);
//开始读源码等操作;
try
{
vShaderFile.open(vertexPath);//读取顶点着色器源码文件路径;
fShaderFile.open(fragmentPath);//读取片段着色器源码文件路径;
std::stringstream vShaderStream, fShaderStream;//创建两个读取流对象,用来临时存放数据;
//将文件流对象读读取流对象里面;
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
//关闭文件处理器;
vShaderFile.close();
fShaderFile.close();
//转换数据流到string;
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
//如果捕捉到异常,则要告诉编译器;
catch (std::istringstream::failure& e)
{
std::cout << "Fail to read the Shader" << std::endl;
}
//声明两个着色器代码的指针,用来存放上面读到的数据,用于下面的编译;
const char* vertexShaderSource = vertexCode.c_str();
const char* fragmentShaderSource = fragmentCode.c_str();
//下面根据源码创建着色器;
//顶点着色器;
unsigned int vertexShader, fragmentShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//将源码附加到顶点着色器上面去;
glCompileShader(vertexShader);
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);//将源码附加到片段着色器上面去;
glCompileShader(fragmentShader);
//着色器程序;
ID = glCreateProgram();
glAttachShader(ID, vertexShader);
glAttachShader(ID, fragmentShader);//链接两个着色器到着色器程序上;
glLinkProgram(ID);
//删除两个着色器对象;
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
}
//启动程序的函数;
void useProgram()
{
glUseProgram(ID);
}
//再加上几个修改Uniform值的函数;
void setBool(const std::string& name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void setInt(const std::string& name, int value) const {
int intLocation = glGetUniformLocation(ID, name.c_str());
glUniform1i(intLocation, value);
}
void setFloat(const std::string& name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
};
main.cpp
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include<E:\OpenGl\练习1.1\3.3.shader_class\shader s.h>
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
int main()
{
//先进行初始化glfw;
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本设置为3;
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本设置为3;
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "MY OPENGL", NULL, NULL);
if (window == NULL)
{
std::cout << "Fail to create a window" << std::endl;
glfwTerminate();//释放资源;
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//创建完告诉将上下文设置为进程上下文;
//对glad进行初始化;
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Fail to initnite glad" << std::endl;
return -1;
}
//引入着色器类,着色器被封装到了class Shader里面;
Shader shader("3.3.shader.vs", "3.3.shader.fs");
float vertices[] = {
// 位置 // 颜色
0.8f, -0.5f, 0.0f, 0.1f, 0.4f, 0.0f, // 右下
-0.8f, -0.5f, 0.0f, 0.6f, 1.0f, 0.6f, // 左下
0.0f, 0.8f, 0.0f, 0.2f, 0.1f, 1.0f // 顶部
};
unsigned int VAO, VBO;
glGenVertexArrays(1, &VAO);//创建VAO;
glGenBuffers(1, &VBO);//创建VBO;
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//链接顶点属性;
//位置属性;
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);//启用颜色属性;
//准备引擎:
while (!glfwWindowShouldClose(window))
{
//准备输入:
processInput(window);
//每一次更新缓冲颜色,之后继续画下一帧;
glClearColor(0.2f, 0.2f, 0.2f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);//清除颜色缓冲单元;
//glUseProgram(shaderProgram);//启动着色器程序;
//启用Shader对象shader的程序启动函数;
shader.useProgram();//启用着色器程序;
glBindVertexArray(VAO);//绑定VAO;
glDrawArrays(GL_TRIANGLES, 0, 3);
//开始绘制;
//glDrawArrays(GL_TRIANGLES, 0, 3);//第一个参数是要进行绘制的类型,第二个参数制定了顶点数组的开始索引,
//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
//注意:6是指一个要绘制六个顶点;!!
//第三个参数是要进行绘制的顶点的长度;
//更新缓冲;
glfwSwapBuffers(window);
//进行检查;
glfwPollEvents();
}
//结束要删除VAO,VBO,以及删除着色器程序;
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
//glDeleteProgram(shaderProgram);//删除着色器程序;
//结束清楚资源;
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//如果按下的键为回车键;
glfwSetWindowShouldClose(window, true);
}
3.3.shader.vs
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
out vec3 OurColor;
void main()
{
gl_Position = vec4(aPos,1.0);
OurColor = aColor;
}
3.3.shader.fs
#version 330 core
out vec4 FragColor;
in vec3 OurColor;
void main()
{
FragColor = vec4(OurColor, 1.0f);
}
这里我把顶点着色器和片段着色器放到文件3.3.shader.vs和3.3.shader.fs中;
九.作业分析:
9.1作业1
要求:修改顶点着色器让三角形上下颠倒
作业1要求三角形上下颠倒,故只需要在顶点着色器中对gl_Position即获取位置的坐标进行修改:
#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
out vec3 OurColor;
void main()
{
//gl_Position = vec4(aPos,1.0);
gl_Position = vec4(aPos.x,-aPos.y,0.0,1.0);
OurColor = aColor;
}
结果如下:
9.2作业2
要求:使用uniform定义一个水平偏移量,在顶点着色器中使用这个偏移量把三角形移动到屏幕右侧
//偏移量;
float positionx = 0.5f;
shader.setFloat("XPosition", positionx);//调用shader中的setFloat方法来对位置进行修改;
这部分代码要加在main.cpp的while渲染循环中,每一帧刷新就可以看到效果;注意其中运用了设置在Shader类中的setBool函数,也可以不用。
在顶点着色器的代码如下:
#version 330 core
layout(location = 0) in vec3 aPos;//顶点的位置值,aPos为绑定的每一个顶点;
layout(location = 1) in vec3 aColor;
uniform float XPosition;//加上了这一行,float变量的名字为XPosition,值为在渲染循环中求得的值;
out vec3 OurColor;
void main()
{
gl_Position = vec4(aPos.x+XPosition,aPos.y,aPos.z,1.0);
OurColor = aColor;
}
结果:
9.3作业3
使用out关键字把顶点位置输出到片段着色器,并将片段的颜色设置为与顶点位置相等(来看看连顶点位置值都在三角形中被插值的结果)。做完这些后,尝试回答下面的问题:为什么在三角形的左下角是黑的?:
代码如下:
#version 330 core
layout(location = 0) in vec3 aPos;//顶点的位置值,aPos为绑定的每一个顶点;
layout(location = 1) in vec3 aColor;
//uniform float XPosition;
//uniform float YPosition;
out vec3 OurColor;
void main()
{
gl_Position = vec4(aPos.x,aPos.y,aPos.z,1.0);
OurColor = aPos;//此处为改动的代码,按要求将颜色值设置为位置的值;
}