🍲 😋
神奇的正方形 ??
在上一节中,我们绘制了一个墨绿色的三角形,这一节,我们来实现 “代码封装”,并绘制彩色正角形和变色三角形。 上一篇文章地址链接: 《【OpenGL学习笔记】计算机图形学②——【渲染管线 顶点着色器 片元着色器 VAO VBO】》.
零、成果预览图:
?????? ????
??● 左图是彩色三角形,右图是变色正方形。
一、着色器的初步了解:
??● 从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。
??● 着色器是使用一种叫 GLSL(全称 OpenGL Shading Language) 的类C语言写成的。GLSL 是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。
??● 着色器的开头总是要声明版本,接着是 输入和输出变量、uniform 和 main 函数。每个着色器的入口点都是 main 函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。对于 uniform ,后面会对其进行讲解。
??● 一个典型的着色器的模板 为:
#version version_number
in vector_type in_variable_name;
out vector_type out_variable_name;
uniform type uniform_name;
void main()
{
...
out_variable_name = 处理过的结果;
}
??● GLSL 中的向量是一个可以包含有1、2、3 或者 4 个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式( n 代表分量的数量):
类型 | 含义 |
---|
vec+n | 包含n 个 float 分量的向量 | bvec+n | 包含n 个 bool 分量的向量 | ivec+n | 包含n 个 int 分量的向量 | uvec+n | 包含n 个 unsigned int 分量的向量 | dvec+n | 包含n 个 double 分量的向量 |
??◆ 多数时候我们使用 “vec+n ”,因为 float 足够满足大多数要求了。
??◆ 一个向量的分量可以通过 vec.x 这种方式获取,这里 x 是指这个向量的第一个分量。我们可以分别使用 “ .x、.y、.z 和 .w ” 来获取它们的第 1、2、3、4 个分量。GLSL 也允许你对颜色使用 rgba ,或是对纹理坐标使用 stpq 访问相同的分量。
??◆ 举个一个片元着色器的栗子:(环境 VS2010)
二、顶点/片元着色器 (文本文件 txt)
??● 顶点着色器与片元着色器之间的联系: ???我们一般先通过顶点着色器,在输入端获得颜色,作为顶点属性,再传到片元着色器中进行处理,最后输出出来。
??● 虽然着色器是各自独立的小程序,但它们都是一个整体的一部分。GLSL 定义了 in 和 out 关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。
??● 顶点着色器应该接收的是一种特殊形式的输入。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用 location 这一元数据指定输入变量。书写格式:layout (location = 0) 。顶点着色器需要为它的输入提供一个额外的 layout 标识,这样我们才能把它链接到顶点数据。
??◆ 比如说,在渲染彩色三角形时,我们的顶点着色器 (文本文件 txt) 是这样写的:
#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec3 color;
out vec3 ourColor;
void main()
{
gl_Position = vec4(position, 1.0f);
ourColor = color;
}
??● 另一个是片段着色器,它需要一个 vec4 颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。
??● 如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方中声明一个输出,并在接收方中声明一个类似的输入。当类型和名字都一样的时候,OpenGL 就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。为了展示这是如何工作的,我们会稍微改动一下之前教程里的那个着色器,让顶点着色器为片段着色器决定颜色。
??◆ 比如说,在渲染彩色三角形时,我们的片元着色器 (文本文件 txt ) 是这样写的:
#version 330 core
in vec3 ourColor;
out vec4 FragColor;
void main()
{
FragColor = vec4(ourColor, 1.0f);
}
三、创建我们自己的着色器类(Shader.h)
??● 前面提到的着色器都是单个的、独立的,现在我们要创建一个着色器类,把它们有机地联合起来。
??◆ 一个典型的着色器类的模板 如下
#pragma once
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
using namespace std;
class Shader
{
private:
GLuint vertex, fragment;
public:
GLuint Program;
Shader( const GLchar *vertexPath, const GLchar *fragmentPath )
{
......
......
try
{
......
......
......
......
} catch( ifstream::failure e ){
cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<endl;
}
......
......
......
......
}
~Shader()
{
glDetachShader(this->Program, vertex);
glDetachShader(this->Program, fragment);
glDeleteShader(vertex);
glDeleteShader(fragment);
glDeleteProgram(this->Program);
}
void Use()
{
glUseProgram(this->Program);
}
};
??● 代码的细节补充:(后面要渲染的彩色三角形和变色正方形,都是用这个通用的着色器)
#pragma once
#include<string>
#include<fstream>
#include<sstream>
#include<iostream>
using namespace std;
class Shader
{
private:
GLuint vertex, fragment;
public:
GLuint Program;
Shader( const GLchar *vertexPath, const GLchar *fragmentPath )
{
string vertexCode;
string fragmentCode;
ifstream vShaderFile;
ifstream fShaderFile;
vShaderFile.exceptions(ifstream::badbit);
fShaderFile.exceptions(ifstream::badbit);
try
{
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
stringstream vShaderStream, fShaderStream;
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
vShaderFile.close();
fShaderFile.close();
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
} catch( ifstream::failure e ){
cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<endl;
}
const GLchar *vShaderCode = vertexCode.c_str();
const GLchar *fShaderCode = fragmentCode.c_str();
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
GLint flag;
GLchar infoLog[512];
glGetShaderiv(vertex, GL_COMPILE_STATUS, &flag);
if( !flag )
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"<<infoLog<<endl;
}
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
glGetShaderiv(fragment, GL_COMPILE_STATUS, &flag);
if( !flag )
{
glGetShaderInfoLog(fragment, 512, NULL, infoLog);
cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"<<infoLog<<endl;
}
this->Program = glCreateProgram();
glAttachShader(this->Program, vertex);
glAttachShader(this->Program, fragment);
glLinkProgram(this->Program);
if( !flag )
{
glGetProgramInfoLog(this->Program, 512, NULL, infoLog);
cout<<"ERROR::SHADER::PROGRAM::LINKING_FAILED\n"<<infoLog<<endl;
}
glDeleteShader(vertex);
glDeleteShader(fragment);
}
~Shader()
{
glDetachShader(this->Program, vertex);
glDetachShader(this->Program, fragment);
glDeleteShader(vertex);
glDeleteShader(fragment);
glDeleteProgram(this->Program);
}
void Use()
{
glUseProgram(this->Program);
}
};
四、绘制彩色三角形(主函数)
??● 常规的绘制流程(输出一个三角形)如下: ????◆ 第一步:引入相应的库 ????◆ 第二步:编写顶点位置和颜色 ????◆ 第三步:编写顶点着色器 ????◆ 第四步:编写片元着色器(也称片段着色器) ????◆ 第五步:编写着色器程序 ????◆ 第六步:设置链接顶点属性 ????◆ 第七步:设置顶点缓冲对象(VBO)【一般都是和后面的 VAO 一起出现】 ????◆ 第八步:设置顶点数组对象(VAO) (也称顶点阵列对象) ????◆ 第九步:绘制三角形
??● 但因为我们把 “第二步、第三步、第四步、第五步、第六步” 这几步都写入了相应的 “Shader.h”、“shader_v.txt”、“shader_f.txt” 里面【如上图所示】,已经封装好了,所以在主函数中,我们只用写剩下的几步即可:
#include <iostream>
using namespace std;
#define GLEW_STATIC
#include <glew.h>
#include <glfw3.h>
#include "Shader.h"
GLfloat vertices_1[] =
{
0.0f, 0.5f, 0.0f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f
};
const GLint WIDTH = 800, HEIGHT = 600;
int main()
{
glfwInit();
GLFWwindow* window_1 = glfwCreateWindow(WIDTH, HEIGHT, "A Beautiful Triangle", nullptr, nullptr);
int screenWidth_1, screenHeight_1;
glfwGetFramebufferSize(window_1, &screenWidth_1, &screenHeight_1);
glfwMakeContextCurrent(window_1);
glewInit();
Shader ourShader = Shader("shader_v.txt", "shader_f.txt");
GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));
glEnableVertexAttribArray(1);
while (!glfwWindowShouldClose(window_1))
{
glViewport(0, 0, screenWidth_1, screenHeight_1);
glfwPollEvents();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ourShader.Use();
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);
glfwSwapBuffers(window_1);
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
??● 运行结果:
五、索引缓冲对象(EBO)
??● 如果我们要绘制一个正方形的话,能想到的是用两个三角形来拼凑。这个时候就要用到 EBO 了。
1、VBO、VAO 与 EBO 之间的联系与区别
??● VBO、VAO 与 EBO 之间的联系与区别: ?? ?? ??① 顶点缓冲对象 VBO 是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标、顶点法向量、顶点颜色数据等。在渲染时,可以直接从 VBO 中取出顶点的各类属性数据,由于 VBO 在显存而不是在内存中,不需要从CPU传输数据,所以处理效率更高。 ???所以可以理解为 VBO 就是显存中的一个存储区域,可以保持大量的顶点属性信息。并且可以开辟很多个 VBO ,每个 VBO 在 OpenGL 中有它的唯一标识 ID ,这个 ID 对应着具体的 VBO 的显存地址,通过这个 ID 可以对特定的 VBO 内的数据进行存取操作。
??② VAO 是一个保存了所有顶点数据属性的状态结合,它存储了顶点数据的格式以及顶点数据所需的 VBO 对象的引用。 ???因为 VBO 保存了一个模型的顶点属性信息,每次绘制模型之前需要绑定顶点的所有信息。当数据量很大时,重复这样的动作变得非常麻烦。VAO 可以把这些所有的配置都存储在一个对象中,每次绘制模型时,只需要绑定这个 VAO 对象就可以了。 ???另外,VAO 本身并没有存储顶点的相关属性数据,这些信息是存储在 VBO 中的,VAO 相当于是对很多个 VBO 的引用,把一些 VBO 组合在一起作为一个对象统一管理。
??③ 索引缓冲对象 EBO 相当于 OpenGL 中的顶点数组的概念,是为了解决同一个顶点多次重复调用的问题,可以减少内存空间浪费,提高执行效率。当需要使用重复的顶点时,通过顶点的位置索引来调用顶点,而不是对重复的顶点信息重复记录,重复调用。 ???EBO 中存储的内容就是顶点位置的索引 indices,EBO 跟 VBO 类似,也是在显存中的一块内存缓冲器,只不过 EBO 保存的是顶点的索引。
2、EBO
??● 关于 VAO 和 VBO,在上一节已经细致地讲解了,下面就讲 EBO。
??● 首先,我们现在要绘制正方形,则需要 4 个顶点。在主函数中我们将 4 个顶点的位置信息给出。并用顶点位置的索引 indices 数组将它们 “缝合” 起来。示意图如下:
??● 代码如下:
GLfloat vertices_1[] =
{
0.5f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
-0.5f, 0.5f, 0.0f,
};
GLuint indices_1[] =
{
0, 1, 3,
1, 2, 3
};
??● 然后,在创建完 VAO 和VBO 后,再创建 EBO 并绑定,用 glBufferData(以GL_ELEMENT_ARRAY_BUFFER为参数)把索引存储到 EBO 中:
GLuint EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW);
??● 当用 EBO 绑定顶点索引的方式绘制模型时,需要使用 glDrawElements 而不是 glDrawArrays :
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
??◆ glDrawElements() 函数说明: ????① 第一个参数:绘制的模式 ????② 第二个参数:绘制的顶点个数 ????③ 第三个参数:索引的数据类型 ????④ 第四个参数:可选的 EBO 中偏移量设定
六、Uniform
??● 若要画一个颜色跟着时间变化的正方形,着色器的传入就是随时间变化的。以往的知识满足不了这一点,这时就需要用到 Uniform。
??● Uniform 是一种从 CPU中的应用 向 GPU中的着色器 发送数据的方式,但 uniform 和顶点属性有些不同。首先,uniform 是全局的(Global)。全局意味着 uniform 变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。其次,无论你把 uniform 值设置成什么,uniform 会一直保存它们的数据,直到它们被重置或更新。
??● 修改过后的顶点着色器:
#version 330 core
layout(location = 0) in vec3 position;
void main()
{
gl_Position = vec4(position, 1.0f);
}
??● 修改过后的片元着色器:
#version 330 core
out vec4 FragColor;
uniform vec4 time;
void main()
{
FragColor = time;
}
??● “ Shader.h ”头文件,不用修改(和彩色三角形的一样)。
??● 这个 uniform 现在还是空的。我们还没有给它添加任何数据。首先需要用 glGetUniformLocation() 函数找到着色器中 uniform 属性的索引 (即位置值) 。当我们得到 uniform 的索引后,就可以用 glUniform…() 相关函数来更新它的值了。
float time = glfwGetTime();
float redValue = sin(time) / 2.0f + 0.5f;
float greenValue = 1 - redValue;
int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time");
glUniform4f(vertexColorLocation, redValue, greenValue, 0.0f, 1.0f );
??◆ 补充说明:因为OpenGL在其核心是一个 C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数。glUniform 函数是一个典型例子,它有一个特定的后缀。标识设定为 uniform 的类型时,可能的后缀有:
后缀 | 含义 |
---|
n +f | 函数需要一个n 个 float 作为它的值 | n +i | 函数需要一个n 个 int 作为它的值 | n +ui | 函数需要一个n 个 unsigned int 作为它的值 | fv | 函数需要一个 float 向量/数组作为它的值 |
七、绘制变色正方形(主函数)
??● 万事俱备,只欠东风:
#include <iostream>
using namespace std;
#define GLEW_STATIC
#include<glew.h>
#include<glfw3.h>
#include"Shader.h"
GLfloat vertices_1[] =
{
0.5f, 0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
-0.5f, -0.5f, 0.0f,
-0.5f, 0.5f, 0.0f,
};
GLuint indices_1[] =
{
0, 1, 3,
1, 2, 3
};
const GLint WIDTH = 600, HEIGHT = 600;
int main()
{
glfwInit();
GLFWwindow* window_1 = glfwCreateWindow(WIDTH, HEIGHT, "Learn OpenGL Triangle test", nullptr, nullptr);
int screenWidth_1, screenHeight_1;
glfwGetFramebufferSize(window_1, &screenWidth_1, &screenHeight_1);
glfwMakeContextCurrent(window_1);
glewInit();
Shader ourShader = Shader("shader_v.txt", "shader_f.txt");
GLuint VAO, VBO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
GLuint EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW);
while (!glfwWindowShouldClose(window_1))
{
glViewport(0, 0, screenWidth_1, screenHeight_1);
glfwPollEvents();
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ourShader.Use();
float time = glfwGetTime();
float redValue = sin(time) / 2.0f + 0.5f;
float greenValue = 1 - redValue;
int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time");
glUniform4f(vertexColorLocation, redValue, greenValue, 0.0f, 1.0f );
glBindVertexArray(VAO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
glfwSwapBuffers(window_1);
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteBuffers(1, &EBO);
glfwTerminate();
return 0;
}
??● 运行结果::
八、小结(整体的思维导图)
??● 绘制流程(输出一个三角形): ????◆ 第一步:引入相应的库 ????◆ 第二步:编写顶点属性 ????◆ 第三步:编写顶点着色器 ????◆ 第四步:编写片元着色器(也称片段着色器) ????◆ 第五步:编写着色器程序 ????◆ 第六步:设置链接顶点属性 ????◆ 第七步:设置顶点缓冲对象(VBO) ????◆ 第八步:设置顶点数组对象(VAO) ????◆ 第九步:设置索引缓冲对象(EBO) ????◆ 第十步:绘制三角形
??● 把“第二步、第三步、第四步、第五步、第六步” 这几步封装在相应的 “Shader.h”、“shader_v.txt”、“shader_f.txt” 里面即可。再在主函数中,写剩下的几步即可。
九、参考附录:
[1] 《LearnOpenGL CN —— 着色器》 链接: https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/.
[2] 《OpenGL图形渲染管线、VBO、VAO、EBO概念及用例》 链接: https://blog.csdn.net/weixin_30735745/article/details/95616490.
上一篇文章地址链接: 《【OpenGL学习笔记】计算机图形学②——【渲染管线 顶点着色器 片元着色器 VAO VBO】》.
?? ??
|