神奇的正方形 ☁️
在上一节中,我们绘制了一个墨绿色的三角形,这一节,我们来实现 “代码封装”,并绘制彩色正角形和变色三角形。
上一篇文章地址链接: 【OpenGL学习笔记②】——OpenGL基础【渲染管线 顶点着色器 片元着色器 VAO VBO 万字总结】.
下一篇文章地址链接: 【OpenGL学习笔记④】——纹理贴图【SOIL2的配置+绘制木板 + 纹理环绕】.
OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.
● 左图是彩色三角形,右图是变色正方形。
● 从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。
● 着色器是使用一种叫 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)
● 顶点着色器与片元着色器之间的联系:
我们一般先通过顶点着色器,在输入端获得颜色,作为顶点属性,再传到片元着色器中进行处理,最后输出出来。
● 虽然着色器是各自独立的小程序,但它们都是一个整体的一部分。GLSL 定义了 in 和 out 关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。
● 顶点着色器应该接收的是一种特殊形式的输入。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用 location 这一元数据指定输入变量。书写格式:layout (location = 0)
。顶点着色器需要为它的输入提供一个额外的 layout 标识,这样我们才能把它链接到顶点数据。
◆ 比如说,在渲染彩色三角形时,我们的顶点着色器 (文本文件 txt) 是这样写的:【注:这很关键,请把该 txt 和下面的 txt 一起放到同 .cpp 同一个目录下】
// 文件名为 “shader_v.txt”
#version 330 core // 3.30版本
layout(location = 0) in vec3 position; // 位置变量的属性位置值为0
layout(location = 1) in vec3 color; // 颜色变量的属性位置值为1
out vec3 ourColor; // 颜色输出
void main()
{
gl_Position = vec4(position, 1.0f); // 核心函数(位置信息赋值)
ourColor = color;
}
● 另一个是片段着色器,它需要一个 vec4 颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。
● 如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方中声明一个输出,并在接收方中声明一个类似的输入。当类型和名字都一样的时候,OpenGL 就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。为了展示这是如何工作的,我们会稍微改动一下之前教程里的那个着色器,让顶点着色器为片段着色器决定颜色。
◆ 比如说,在渲染彩色三角形时,我们的片元着色器 (文本文件 txt ) 是这样写的:
// 文件名为 “shader_f.txt”
#version 330 core // 3.30版本
in vec3 ourColor; // 输入(3维)颜色向量
out vec4 FragColor; // 输出到四个浮点数构成的一个(4维)向量 FragColor
void main()
{
FragColor = vec4(ourColor, 1.0f); // 核心函数(颜色信息赋值)
}
● 前面提到的着色器都是单个的、独立的,现在我们要创建一个着色器类,把它们有机地联合起来。
◆ 一个典型的着色器类的模板
如下
#pragma once // 为了避免同一个头文件被包含(include)多次
#include
#include
#include
#include
using namespace std;
// 我们自己的着色器
class Shader
{
private:
GLuint vertex, fragment; // 顶点着色器 和 片元着色器
public:
GLuint Program; // 着色器程序的ID
// Constructor(着色器构造函数)
Shader( const GLchar *vertexPath, const GLchar *fragmentPath ) // 分别是主函数传进来的关于顶点/片段着色器GLSL的路径
{
// 文件读取系列的变量定义
......
// 异常机制处理:保证ifstream对象可以抛出异常:
......
try
{
// 打开文件
......
// 读取文件的缓冲内容到数据流中
......
// 关闭文件处理器
......
// 转换数据流到string
......
} catch( ifstream::failure e ){ // 发生异常时输出
cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<endl;
}
/* 将 string 类型的字符串转化为 char数组 类型 */
......
/* 顶点着色器 */
......
/* 片元着色器 */
......
/* 着色器程序 */
......
}
// Deconstructor(着色器的析构函数)
~Shader()
{
glDetachShader(this->Program, vertex);
glDetachShader(this->Program, fragment);
glDeleteShader(vertex);
glDeleteShader(fragment);
glDeleteProgram(this->Program);
}
// 渲染的时候调用
void Use()
{
glUseProgram(this->Program);
}
};
● 代码的细节补充:(后面要渲染的彩色三角形和变色正方形,都是用这个通用的着色器)
#pragma once // 为了避免同一个头文件被包含(include)多次
#include
#include
#include
#include
using namespace std;
#include"glew-2.2.0\include\GL\glew.h" // 注:这一部分要根据个人情况进行设定
#include"glfw-3.3.4.bin.WIN32\include\GLFW\glfw3.h"
// 我们自己的着色器
class Shader
{
private:
GLuint vertex, fragment; // 顶点着色器 和 片元着色器
public:
GLuint Program; // 着色器程序的ID
// Constructor(着色器构造函数)
Shader( const GLchar *vertexPath, const GLchar *fragmentPath )
{
// 文件读取系列的变量定义
string vertexCode;
string fragmentCode;
ifstream vShaderFile;
ifstream fShaderFile;
// 异常机制处理:保证ifstream对象可以抛出异常:
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();
// 转换数据流到string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
} catch( ifstream::failure e ){ // 发生异常时输出
cout<<"ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ"<<endl;
}
/* 将 string 类型的字符串转化为 char数组 类型 */
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);
}
// Deconstructor(析构函数)
~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
using namespace std;
#define GLEW_STATIC
#include
#include
#include "Shader.h"
/* 编写各顶点位置与颜色 */
GLfloat vertices_1[] =
{ // position // color
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"); // 相对路径
/* 设置顶点缓冲对象(VBO) + 设置顶点数组对象(VAO) */
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); // 通道 0 打开
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6*sizeof(GLfloat), (GLvoid*)(3*sizeof(GLfloat)));
glEnableVertexAttribArray(1); // 通道 1 打开
// draw loop 画图循环
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); // 绑定 VAO
glDrawArrays(GL_TRIANGLES, 0, 3); // 画三角形 从第0个顶点开始 一共画3次
glBindVertexArray(0); // 解绑定
glfwSwapBuffers(window_1);
}
glDeleteVertexArrays(1, &VAO); // 释放资源
glDeleteBuffers(1, &VBO);
glfwTerminate(); // 结束
return 0;
}
● 运行结果:
● 如果我们要绘制一个正方形的话,能想到的是用两个三角形来拼凑。这个时候就要用到 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 保存的是顶点的索引。
● 关于 VAO 和 VBO,在上一节已经细致地讲解了,下面就讲 EBO。
● 首先,我们现在要绘制正方形,则需要 4 个顶点。在主函数中我们将 4 个顶点的位置信息给出。并用顶点位置的索引 indices 数组将它们 “缝合” 起来。示意图如下:
● 代码如下:
/* 编写各顶点位置 */
GLfloat vertices_1[] =
{
//position
0.5f, 0.5f, 0.0f, // top right 0
0.5f, -0.5f, 0.0f, // bottom right 1
-0.5f, -0.5f, 0.0f, // bottom left 2
-0.5f, 0.5f, 0.0f, // top left 3
};
/* 四个顶点的连接信息给出来 */
GLuint indices_1[] =
{
0, 1, 3, // 序号为 0、1、3 的顶点组合成一个三角形
1, 2, 3 // 序号为 1、2、3 的顶点组合成一个三角形
};
● 然后,在创建完 VAO 和VBO 后,再创建 EBO 并绑定,用 glBufferData(以GL_ELEMENT_ARRAY_BUFFER为参数)把索引存储到 EBO 中:
GLuint EBO;
glGenBuffers(1, &EBO); // 绑定 EBO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 使用 glBindBuffer 函数把新创建的索引缓冲对象绑定到 GL_ELEMENT_ARRAY_BUFFER目标上
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices_1), indices_1, GL_STATIC_DRAW); // GL_STATIC_DRAW:静态的画图(因为要频繁地读)
● 当用 EBO 绑定顶点索引的方式绘制模型时,需要使用 glDrawElements 而不是 glDrawArrays :
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 绑定 EBO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 画两个三角形 从第0个顶点开始 一共画 6 次(顺序为0,1,3,1,2,3)
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // 解绑定 EBO
◆ glDrawElements() 函数说明:
① 第一个参数:绘制的模式
② 第二个参数:绘制的顶点个数
③ 第三个参数:索引的数据类型
④ 第四个参数:可选的 EBO 中偏移量设定
● 若要画一个颜色跟着时间变化的正方形,着色器的传入就是随时间变化的。以往的知识满足不了这一点,这时就需要用到 Uniform。
● Uniform 是一种从 CPU中的应用 向 GPU中的着色器 发送数据的方式,但 uniform 和顶点属性有些不同。首先,uniform 是全局的(Global)。全局意味着 uniform 变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。其次,无论你把 uniform 值设置成什么,uniform 会一直保存它们的数据,直到它们被重置或更新。
● 修改过后的顶点着色器:
#version 330 core // 3.30版本
layout(location = 0) in vec3 position; // 位置变量的属性位置值为0
void main()
{
gl_Position = vec4(position, 1.0f); // 核心函数(位置信息赋值)
}
● 修改过后的片元着色器:
#version 330 core // 3.30版本
out vec4 FragColor; // 输出是四个浮点数构成的一个向量 RGB+aerfa
uniform vec4 time; // 在OpenGL程序代码中设定这个变量(uniform:实时地变量表示)
void main()
{
FragColor = time; // 颜色随时间变化
}
● “ Shader.h ”头文件,不用修改(和彩色三角形的一样)。
● 这个 uniform 现在还是空的。我们还没有给它添加任何数据。首先需要用 glGetUniformLocation() 函数找到着色器中 uniform 属性的索引 (即位置值) 。当我们得到 uniform 的索引后,就可以用 glUniform…() 相关函数来更新它的值了。
float time = glfwGetTime(); // 获取时间(运行的秒数)
float redValue = sin(time) / 2.0f + 0.5f; // 红色数值计算,范围[0,1]
float greenValue = 1 - redValue; // 绿色数值计算,范围[0.1]。且满足 “redValue + greenValue = 1”
int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time"); // 找到 “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
using namespace std;
#define GLEW_STATIC
#include
#include
#include"Shader.h"
/* 编写各顶点位置 */
GLfloat vertices_1[] =
{
//position
0.5f, 0.5f, 0.0f, // top right 0
0.5f, -0.5f, 0.0f, // bottom right 1
-0.5f, -0.5f, 0.0f, // bottom left 2
-0.5f, 0.5f, 0.0f, // top left 3
};
/* 四个顶点的连接信息给出来 */
GLuint indices_1[] =
{
0, 1, 3, // 序号为 0、1、3 的顶点组合成一个三角形
1, 2, 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"); // 相对路径
/* 设置顶点缓冲对象(VBO) + 设置顶点数组对象(VAO) */
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); // 通道 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);
// draw loop 画图循环
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; // 红色数值计算,范围[0,1]
float greenValue = 1 - redValue; // 绿色数值计算,范围[0.1]。且满足 “redValue + greenValue = 1”
int vertexColorLocation = glGetUniformLocation(ourShader.Program, "time");
glUniform4f(vertexColorLocation, redValue, greenValue, 0.0f, 1.0f );
glBindVertexArray(VAO); // 绑定 VAO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); // 绑定 EBO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 画两个三角形 从第0个顶点开始 一共画6次
glBindVertexArray(0); // 解绑定 VAO
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0); // 解绑定 EBO
// 交换缓冲
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学习笔记②】——OpenGL基础【渲染管线 顶点着色器 片元着色器 VAO VBO 万字总结】.
下一篇文章地址链接: 【OpenGL学习笔记④】——纹理贴图【SOIL2的配置+绘制木板 + 纹理环绕】.
OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.
⭐️ ⭐️