🍋
文章目录
神奇的三角形 ☁️
我尽可能地细讲,多打比方,帮助像我一样的初学者友好地理解。
上一篇文章地址链接:《【2021年最新版】openGL flew flgw flut 环境配置 + 绘制出“Hello World!” 》.
下一篇文章地址链接:《【OpenGL学习笔记③】——着色器【GLSL Uniform 彩色三角形 ⭐变色正方形⭐】》.
OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.
一、图形渲染管线的初步了解:
● 在 OpenGL 中,任何事物都在 3D 空间中,而屏幕和窗口却是 2D 的空间,这导致 OpenGL 的大部分工作都是关于把 3D 坐标转变为适应你屏幕的 2D 坐标。
● 3D 坐标转为 2D 坐标的处理过程是由 OpenGL 的图形渲染管线管理的。(Graphics Pipeline,大多译为管线,实际上指的是 一堆原始图形数据途经一个输送管道,期间经过各种变化处理,最终出现在屏幕的过程 )
● 为什么叫 “管线” 呢? 因为图形的渲染过程就像是:管的一端输入,管的另一端输出。输入是一些 “顶点坐标、画图规则”,而输出就是 “实际图画”。而中间那条 “管子” ,就像是一个通用的 “容器”,什么样的 “输入” 都能接受。就像生活中的水管,即能传输水,也能传输油,还能传输天然气。
● 它们具有并行执行的特性,也就是说,就像自来水厂向你家输水一样,可以多条水管一起输过来。而当今大多数大脑的显卡上,都有成千上万的小处理核心(这个就像是那成千上万的“水管”),它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
● 图形渲染管线可以被划分为两个主要部分:第一部分把 3D 坐标转换为 2D 坐标,第二部分把 2D 坐标转变为实际有颜色的像素。
● 图形渲染管线的阶段流程图:
① 图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标(后面会解释)。
② 图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入,并将所有的顶点装配成指定图元的形状。
③ 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它会通过新产生的顶点构造出新的图元来生成其他形状(但变形不变样)。
④ 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。
⑤ 片段着色器(也称为片元着色器)的主要目的是计算一个像素的最终颜色。
● 可以看到,图形渲染管线非常复杂,我这里写的还是冰山一角。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。
在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。
二、绘制流程(输出一个三角形)
◆ 第一步:引入相应的库
#include <iostream>
using namespace std;
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
◆ 第二步:编写顶点位置
● 我们要渲染一个三角形,一共要指定三个顶点,每个顶点都有一个 3D 位置:
GLfloat vertices_1[] =
{
0.0f, 0.5f, 0.0f, // 上顶点
-0.5f, -0.5f, 0.0f, // 左顶点
0.5f, -0.5f, 0.0f, // 右顶点
};
● 补充:OpenGL 是一个 3D 图形库,所以我们在 OpenGL 中指定的所有坐标都是 3D 坐标(x、y和z)。仅当 3D 坐标在3个轴 (x、y和z) 上都在 -1.0 到 1.0 的范围内时才处理它。这个是 标准化设备坐标 (Normalized Device Coordinates, NDC)。
● 因为我们绘制的是一个平面(即 2D )的三角形,所以我们将它顶点的 z 坐标设置为 “ 0.0f ”。
● 定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。
◆ 第三步:编写顶点着色器
● 编写顶点着色器需要用到 “着色器语言GLSL(OpenGL Shading Language) ”,就像我们和英国人交流得用 English 。然后编译这个着色器,这样我们就可以在程序中使用它了:
// 顶点着色器
const GLchar* vertexCode_1 = "#version 330 core\n" // 3.30版本(版本申明)
"layout(location = 0) in vec3 position_1;\n" // 三个浮点数 vector 向量表示位置。position是变量,并储存了这三个向量
"void main()\n"
"{\n"
"gl_Position = vec4(position_1, 1.0f);\n" // 核心函数(位置信息赋值)
"}\n";
● 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的 gl_Position变量 ,它在幕后是 vec4 类型的。然后,声明输入变量可以使用 in 关键字。由于我们的输入是一个 3 分量的向量,我们必须把它转换为 4 分量的。我们可以把 vec3 的数据作为 vec4 构造器的参数,同时把 “透视值分量” 设置为1.0f(后面课程会解释为什么)来完成这一任务。
● 有了顶点着色器这个 “模子”,我们在主函数里面就可以创建 顶点着色器对象 了。就像小时候玩的橡皮泥一样,有了压橡皮泥的模子,压出几个五角星就是很容易的事了。
● 在创建着色器对象时,注意要用 ID 来引用。所以我们让这个顶点着色器为 unsigned int 数据类型(注:在OpenGL里面,“GLuint ” 实际上就是 “unsigned int”,只不过换了个样子),然后用 glCreateShader 创建这个着色器:
GLuint vertexShader_1 = glCreateShader(GL_VERTEX_SHADER); // 创建顶点着色器对象
● 注:我们看 glCreateShader 这串英文也知道,它是创建一个 “着色器” 通用的函数,它不仅能创建现在都顶点着色器,还能创建后面的片段着色器。只要传参时,传入相应的标识符就可以了。这里我们传入的是关于顶点着色器的标识符 GL_VERTEX_SHADER 。
● 下一步我们把这个着色器源码附加到着色器对象上,然后**编译(Compile)**它:【这里我稍微提一下,编译是啥意思,就是把那些我们看得懂的字符,转变成计算机能读得懂的0和1】
glShaderSource(vertexShader_1, 1, &vertexCode_1, NULL); // 将顶点着色器的内容传进来
glCompileShader(vertexShader_1); // 编译顶点着色器
● glShaderSource 函数:
▼ 第一个参数:要编译的着色器对象。
▼ 第二个参数:指定传递的源码字符串数量,这里只有一个。
▼ 第三个参数:顶点着色器真正的源码。我们传入的是地址。
▼ 第四个参数:读数据时的起始位。
● 如何检测 glCompileShader 编译是否成功了呢?如果没成功的话,我们也想知道错误是什么,这样我们才能修复它们。检测编译时错误可以通过以下代码来实现:
GLint flag; // 用于判断编译是否成功的标识符
GLchar infoLog[512]; // 我用的512个字符来装错误信息(如果出错了的话)
glGetShaderiv(vertexShader_1, GL_COMPILE_STATUS, &flag); // 获取编译状态
if( !flag )
{
glGetShaderInfoLog(vertexShader_1, 512, NULL, infoLog); // 如果出错,用 glGetShaderInfoLog函数 来获取错误消息
cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"<<infoLog<<endl;
}
◆ 第四步:编写片元着色器(也称片段着色器)
● 片元着色器的作用是处理由光栅化阶段生成的每个片元,最终计算出每个像素的最终颜色。
● 和编写顶点着色器的过程相似。片段着色器只需要一个输出变量,这个变量是一个 4 分量向量,它表示的是最终的输出颜色,这里我们命名为 FragColor_1。然后,声明输出变量可以使用 out 关键字。我们设置 Alpha 值为1.0(1.0代表完全不透明)的墨绿色的 vec4 赋值给颜色输出。
// 片元着色器
const GLchar* fragmentCode_1 = "#version 330 core\n" // 版本信息3.3
"out vec4 FragColor_1;\n" // 输出是四个浮点数构成的一个向量 RGB+aerfa
"void main()\n"
"{\n"
"FragColor_1 = vec4(0.5f, 0.75f, 0.25f, 1.0f);\n" // 核心函数(颜色信息赋值)
"}\n";
● 编译片段着色器的过程与顶点着色器类似,只不过我们使用 GL_FRAGMENT_SHADER 常量作为着色器类型:
GLuint fragmentShader_1 = glCreateShader(GL_FRAGMENT_SHADER); // 创建片元着色器对象
glShaderSource(fragmentShader_1, 1, &fragmentCode_1, NULL); // 将片元着色器的内容传进来
glCompileShader(fragmentShader_1); // 编译顶点着色器
● 这里的 glShaderSource函数、glCompileShader函数 和顶点着色器使用的是一样的。
● 同样滴,我们也可以用同样的判断机制来看看 片元着色器 的编译是否成功:
glGetShaderiv(fragmentShader_1, GL_COMPILE_STATUS, &flag); // 获取编译状态
if( !flag )
{
glGetShaderInfoLog(fragmentShader_1, 512, NULL, infoLog); // 如果出错,用 glGetShaderInfoLog函数 来获取错误消息
cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"<<infoLog<<endl;
}
◆ 第五步:编写着色器程序
● 着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的东西。 它的名字里最突出的就是 “程序” 两个字,它是一个流程,它是把先前创建的 “着色器模子” 拼接在一起的 “超级模子”,可以进行 “一条龙服务”。
● 如果要使用刚才编译的着色器,那么我们必须把它们 连接(Link) 为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
● 当连接着色器至一个程序的时候,它会把每个着色器的输出 连接到 下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
● 创建一个着色器程序:
GLuint shaderProgram_1 = glCreateProgram(); // glCreateProgram 函数会创建一个着色器程序,并返回新创建程序对象的 ID 引用。
glAttachShader(shaderProgram_1, vertexShader_1); // 用 glLinkProgram 把之前编译的 顶点着色器 附加到程序对象上
glAttachShader(shaderProgram_1, fragmentShader_1); // 用 glLinkProgram 把之前编译的 片元着色器 附加到程序对象上
glLinkProgram(shaderProgram_1); // 最后用 glLinkProgram 链接
● 就像编译着色器的一样,我们也可以检测链接着色器程序是否成功,并获取相应的日志:
glGetProgramiv(shaderProgram_1, GL_LINK_STATUS, &flag);
if( !flag )
{
glGetProgramInfoLog(shaderProgram_1, 512, NULL, infoLog); // 如果出错,用 glGetProgramInfoLog函数 来获取错误消息
cout<<"ERROR::SHADER::PROGRAM::LINKING_FAILED\n"<<infoLog<<endl;
}
● 在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:(这可以释放资源,节省空间)
glDeleteShader(vertexShader_1);
glDeleteShader(fragmentShader_1);
● 在我们需要用编写好的 着色器程序 时,只需要调用 glUseProgram 函数,用刚创建的着色器程序对象作为它的参数,来激活这个程序对象即可:
glUseProgram(shaderProgram_1);
◆ 第六步:设置链接顶点属性
● 顶点着色器允许我们输入任何形式的顶点属性。这具有很强的灵活性,但它也意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定 OpenGL 该如何 “解释” 输入给它的顶点数据。
● 我们的顶点缓冲数据会被解析为下面这样子:
① 1 字节(Byte) = 8 比特位(bit)
② 一个顶点位置有
x
x
x、
y
y
y、
z
z
z 三个都是 4 字节的浮点值(float)。
③
x
x
x、
y
y
y、
z
z
z 这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。
④ 数据中第一个值在缓冲开始的位置。
● 有了这些信息我们就可以使用 glVertexAttribPointer 函数告诉 OpenGL 该如何解析顶点数据(应用到逐个顶点属性上)了:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0); // 启用顶点属性(注:顶点属性默认是禁用的)。
▼ 第一个参数:顶点属性。这个是 我们在最初,顶点着色器中使用 “layout(location = 0)” 定义 position 顶点属性的位置值(Location)。
▼ 第二个参数:顶点属性的大小。顶点属性是一个 vec3 ,它由 3 个值组成,所以大小是3。
▼ 第三个参数:数据的类型。这里是 GL_FLOAT (注:GLSL中 vec* 都是由浮点数值组成的)。
▼ 第四个参数:是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到 0 到 1 之间(注:对于有符号型signed数据是-1)。我们把它设置为GL_FALSE。
▼ 第五个参数:设置步长(Stride)。它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在 3 个 float 之后,我们把步长设置为 “3 * sizeof(float)”。
▼ 第六个参数:起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。因为最后一个参数的类型是 “void*”,所以我们需要进行这个奇怪的强制类型转换。
◆ 第七步:设置顶点缓冲对象(VBO)【一般都是和后面的 VAO 一起出现】
● 我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这含有这三个顶点的内存。(打比方说,没有顶点缓冲这个机制时,我们只能看 “沙画动画”,而有了顶点缓冲这个机制时,我们就可以看 “定格动画” 了)
● 使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。
● 顶点缓冲对象是我们在 OpenGL 出现的一个重要的 OpenGL 对象(就比方说,它是 “OpenGL社会” 里面一个有身份有地位的任人物,我们可以找他办很多事)。就像 OpenGL 中的其它对象一样,这个缓冲对象也有一个独一无二的 ID ,所以我们可以使用 glGenBuffers 函数和一个缓冲 ID 生成一个 VBO 对象:
GLuint VBO; // 顶点缓存对象
glGenBuffers(1, &VBO); // VBO 主要负责传输数据 绑定 VBO
● OpenGL 有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。我们可以使用 glBindBuffer 函数把新创建的顶点缓冲对象绑定到 GL_ARRAY_BUFFER 目标上:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
● 从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的顶点缓冲对象(VBO)。然后我们可以调用 glBufferData 函数,它会把之前定义的顶点数据复制到缓冲的内存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW); // GL_STATIC_DRAW:静态的画图(频繁地读)
● glBufferData 是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。
▼ 第一个参数:目标缓冲的类型。将当前的顶点缓冲对象绑定到GL_ARRAY_BUFFER目标上。
▼ 第二个参数:传输数据的大小。用一个简单的 sizeof 计算出顶点数据大小就行。
▼ 第三个参数:实际数据。
▼ 第四个参数:显卡管理数据的形式。它有三种形式:
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
● 因为三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。
◆ 第八步:设置顶点数组对象(VAO) (也称顶点阵列对象)
● 顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个 VAO 中。
● 这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的 VAO 就行了。这使得在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的 VAO 就行了。刚刚设置的所有状态都将存储在 VAO 中。(就好像,我们已经把菜谱写好了,想炒什么菜,直接去找到菜谱,拿给小厨子去炒,就不用我们站在旁边一句话一个指点地教他炒了)
● 创建一个 VAO 和创建一个 VBO 很类似:
GLuint VAO; // 顶点阵列对象
glGenVertexArrays(1, &VAO); // VAO 对应的一种 关联信息(顶点着色器和位置相映射) 绑定 VAO
● 要想使用 VAO,要做的只是使用 glBindVertexArray 绑定 VAO 。从绑定之后起,我们应该绑定 与相应配置对应的 VBO 和属性指针,之后解绑 VAO 供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把 VAO 绑定到希望使用的设定上就行了:
glBindVertexArray(VAO); // 绑定 VAO 和 VBO
//glBindVertexArray(0); // 解绑定(VAO 和 VBO)的代码
● 一般当你打算绘制多个物体时,你首先要 生成/配置 所有的 VAO(和必须的 VBO),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的 VAO,绑定它,绘制完物体后,再解绑 VAO 即可。
● 这里提一下 VAO 和 VBO 之间的关系,请教过老师后,老师是这么说的:“我对 VAO , VBO 是这样理解的。我们用顶点着色器处理顶点数据的时候,着色器程序是没有输入的。它调用的时候需要的数据怎么获得呢,就需要 VAO 来帮忙。但是 VAO 只是帮助如何解读,不是数据,数据的导入和索引是VBO负责。所以一条管线的着色器必须有一个 VAO 来解释对应的 VBO 中的数据。比如我们课上,position 就是需要的输入变量,但是调用着色器程序时没有写输入参数。这个变量就需要从 VAO 寻找解读方式,然后用这个解读方式在对应的 VBO 中解读数据。因为此刻 VBO 对应的是在显存里的数据,都是二进制代码,如何解读成有意义的数据格式就是 VAO 来负责并关联到 position 上。”
◆ 第九步:绘制三角形
● OpenGL给我们提供了 glDrawArrays 函数,它使用当前已激活的着色器,包括之前定义的顶点属性配置,和 VBO 的顶点数据(通过 VAO 间接绑定)来绘制图元。
glDrawArrays(GL_TRIANGLES, 0, 3); // 画三角形 从第0个顶点开始 一共画3次
▼ 第一个参数:绘制OpenGL图元的类型。我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。
▼ 第二个参数:顶点数组的起始索引。我们这里填0。
▼ 第三个参数:绘制的顶点个数。这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点)。
三、完整代码
/* 第一步:引入相应的库 */
#include <iostream>
using namespace std;
#define GLEW_STATIC
#include <GL/glew.h>
#include <GLFW/glfw3.h>
/* 第二步:编写顶点位置 */
GLfloat vertices_1[] =
{
0.0f, 0.5f, 0.0f, // 上顶点
-0.5f, -0.5f, 0.0f, // 左顶点
0.5f, -0.5f, 0.0f, // 右顶点
};
/* 第三步:编写顶点着色器 */
const GLchar* vertexCode_1 = "#version 330 core\n" // 3.30版本
"layout(location = 0) in vec3 position_1;\n" // 三个浮点数vector向量表示位置 position变量名
"void main()\n"
"{\n"
"gl_Position = vec4(position_1, 1.0f);\n" // 核心函数(位置信息赋值)
"}\n";
/* 第四步:编写片元着色器(也称片段着色器) */
const GLchar* fragmentCode_1 = "#version 330 core\n" // 版本信息
"out vec4 FragColor_1;\n" //输出是四个浮点数构成的一个向量 RGB+aerfa
"void main()\n"
"{\n"
"FragColor_1 = vec4(0.5f, 0.75f, 0.25f, 1.0f);\n"
"}\n";
const GLint WIDTH = 800, HEIGHT = 600;
// DrawArray 、 VAO 这两个要重点理解,其他只要知道大致模块功能就行了。
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();
/* 第三步:编写顶点着色器 */
GLuint vertexShader_1 = glCreateShader(GL_VERTEX_SHADER); // 创建顶点着色器对象
glShaderSource(vertexShader_1, 1, &vertexCode_1, NULL); // 将顶点着色器的内容传进来
glCompileShader(vertexShader_1); // 编译顶点着色器
GLint flag; // 用于判断编译是否成功
GLchar infoLog[512]; // 512个字符
glGetShaderiv(vertexShader_1, GL_COMPILE_STATUS, &flag); // 获取编译状态
if( !flag )
{
glGetShaderInfoLog(vertexShader_1, 512, NULL, infoLog); // 缓冲池
cout<<"ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"<<infoLog<<endl;
}
/* 第四步:编写片元着色器(也称片段着色器) */
GLuint fragmentShader_1 = glCreateShader(GL_FRAGMENT_SHADER); // 创建片元着色器对象
glShaderSource(fragmentShader_1, 1, &fragmentCode_1, NULL); // 将顶点着色器的内容传进来
glCompileShader(fragmentShader_1); // 编译顶点着色器
glGetShaderiv(fragmentShader_1, GL_COMPILE_STATUS, &flag); // 获取编译状态
if( !flag )
{
glGetShaderInfoLog(fragmentShader_1, 512, NULL, infoLog); // 缓冲池
cout<<"ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n"<<infoLog<<endl;
}
/* 第五步:创建着色器程序 */
GLuint shaderProgram_1 = glCreateProgram();
glAttachShader(shaderProgram_1, vertexShader_1);
glAttachShader(shaderProgram_1, fragmentShader_1);
glLinkProgram(shaderProgram_1);
glGetProgramiv(shaderProgram_1, GL_LINK_STATUS, &flag);
if( !flag )
{
glGetProgramInfoLog(shaderProgram_1, 512, NULL, infoLog);
cout<<"ERROR::SHADER::PROGRAM::LINKING_FAILED\n"<<infoLog<<endl;
}
glDeleteShader(vertexShader_1);
glDeleteShader(fragmentShader_1);
/* 第七步:设置顶点缓冲对象(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); // GL_STATIC_DRAW:静态的画图(频繁地读)
/* 第六步:设置链接顶点属性 */
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// draw loop 画图循环
while (!glfwWindowShouldClose(window_1))
{
glViewport(0, 0, screenWidth_1, screenHeight_1);
glfwPollEvents();
glClearColor(0.0f, 0.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
/* 第九步:绘制三角形 */
glUseProgram(shaderProgram_1); // 渲染调用着色器程序
glBindVertexArray(VAO); // 绑定 VAO
glDrawArrays(GL_TRIANGLES, 0, 3); // 画三角形 从第0个顶点开始 一共画3次
glBindVertexArray(0); // 解绑定
glfwSwapBuffers(window_1);
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram_1);
glfwTerminate();
return 0;
}
运行结果:
四、小结(整体的思维导图)
● 绘制流程(输出一个三角形):
◆ 第一步:引入相应的库
◆ 第二步:编写顶点位置
◆ 第三步:编写顶点着色器
◆ 第四步:编写片元着色器(也称片段着色器)
◆ 第五步:编写着色器程序
◆ 第六步:设置链接顶点属性
◆ 第七步:设置顶点缓冲对象(VBO)【一般都是和后面的 VAO 一起出现】
◆ 第八步:设置顶点数组对象(VAO) (也称顶点阵列对象)
◆ 第九步:绘制三角形
五、参考附录:
[1] 《LearnOpenGL CN —— 你好,三角形》
链接: https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/.
[2] 《解析顶点着色器和片元着色器》
链接: https://www.jianshu.com/p/a771639ffbbb.
[3] 《OpenGL 顶点和片元着色器简介》
链接: https://blog.csdn.net/qq_38907791/article/details/96706752.
上一篇文章地址链接:《【2021年最新版】openGL flew flgw flut 环境配置 + 绘制出“Hello World!” 》.
下一篇文章地址链接:《【OpenGL学习笔记③】——着色器【GLSL Uniform 彩色三角形 ⭐变色正方形⭐】》.
OpenGL总学习目录: https://blog.csdn.net/Wang_Dou_Dou_/article/details/121240714.
⭐️ ⭐️