# LearnOpenGL-01入门
最好的OpenGL学习资料learnopengl (opens new window)
# 前言
OpenGL是一组图形学接口,不同操作系统实现不同,具体的实现是由显卡制造商负责的,因此需要经常更新显卡驱动。
# OpenGL
Core-profile vs Immediate mode,Immediate mode是3.2版本之前的,固定函数管线,易于使用,但是不灵活,开发者不能控制计算过程。从3.2版本开始Core-profile,并弃用一些旧的功能。
使用现代API非常灵活和高效,但是更难学习。现代API需要开发者真正理解OpenGL和图形学编程。
OpenGL3.3版本包含了最核心的现代API功能,当前最新版本4.6
只有最新的显卡版本才支持比较新的OpenGL版本
OpenGL支持扩展,显卡公司将其实现在显卡驱动中,因此可以不用更新OpenGL版本就可以使用新特性。当一个扩展非常好用,很受欢迎,那很可能会出现在未来的OpenGL版本中。
OpenGL基本上是个大型的状态机器。
GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文、定义窗口参数以及处理用户输入
GLAD是一个OpenGL加载器,可以简化加载过程。
双缓冲(Double Buffer)
应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。 这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上而下逐像素地绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,我们应用双缓冲渲染窗口应用程序。前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换(Swap)前缓冲和后缓冲,这样图像就立即呈显出来,之前提到的不真实感就消除了。
# 你好、三角形
几个重要概念:
- 顶点数组对象:Vertex Array Object,VAO。用来保存顶点属性的配置信息(比如
glVertexAttribPointer的参数、启用的顶点属性数组等),相当于顶点配置的 “快照”,后续绘制时只需绑定 VAO,就能复用所有配置,不用重复设置。- 顶点缓冲对象:Vertex Buffer Object,VBO。GPU 中的一块内存区域,用来存储顶点数据(如坐标、颜色、纹理坐标等),目的是减少 CPU 和 GPU 之间的数据传输,提升性能。
- 元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO。它是 GPU 中的一块内存区域,专门用来存储顶点的索引数据,解决顶点重复问题。
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线管理的。
图形渲染管线接受一组3D坐标,并转变为屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
OpenGL着色器使用GLSL写成的

为了让OpenGL知道我们的坐标和颜色值构成的到底是什么,OpenGL需要你去指定这些数据所表示的渲染类型。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做图元(Primitive),任何一个绘制指令的调用都将把图元传递给OpenGL。这是其中的几个:GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP。
图形渲染管线流程:
- 顶点数据:以数组形式传递三个3D坐标作为图跨渲染管线输入,用来表示一个三角形,这个数组叫顶点数组。
- 顶点着色器:把一个单独的顶点作为输入,把一种3D坐标转为另一种3D坐标?
- 几何着色器:将一组顶点作为输入,这些顶点形成图元,能够通过发出新的顶点来形成新的图元。
- 图元装配:将顶点着色器输出的所有顶点作为输入,并将所有点装配成指定图元的形状。
- 光栅化:把图元映射为最终屏幕上相应的像素,生成宫片段着色器使用的生成供片段着色器(Fragment Shader)使用的片段(Fragment),在片段着色器运行之前会执行裁切(Clipping),丢弃超出视图的所有像素。
- 片段着色器:计算一个像素的最终颜色,包含3D场景的数据(比如光照、阴影、光的颜色等等)。
- 测试与混合:检测片段对应的深度,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。
OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据。
标准化设备坐标(Normalized Device Coordinates, NDC):OpenGL仅当3D坐标在3个轴(x、y和z)上-1.0到1.0的范围内时才处理它,最终显示在屏幕上。
OpenGL 3.3以及和更高版本中,GLSL版本号和OpenGL的版本是匹配的(比如说GLSL 420版本对应于OpenGL 4.2)
# 着色器
着色器是使用一种叫GLSL的类C语言写成的。
着色器开头声明版本,输入变量、输出变量、uniform、main函数
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
uniform type uniform_name;
void main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
GLSL默认基础数据类型:int、float、double、uint、bool
GLSL的向量是一个可以包含2、3、4个分量的容器,分量的类型可以是默认基础数据类型的一种
| 类型 | 含义 |
|---|---|
vecn | 包含n个float分量的默认向量 |
bvecn | 包含n个bool分量的向量 |
ivecn | 包含n个int分量的向量 |
uvecn | 包含n个unsigned int分量的向量 |
dvecn | 包含n个double分量的向量 |
重组:GLSL的向量可以灵活的重组
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
可以使用上面4个字母任意组合来创建一个和原来向量一样长的(同类型)新向量,只要原来向量有那些分量即可。cool
向量作为参数:
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
输入与输出:GLSL定义了in和out关键字明确输入和输出,当有一个着色器的输出变量和下一个着色器阶段的输入匹配,就会传递下去。当类型和名字都一样时,OpenGL会把两个变量链接到一起,他们之间就能发送数据。
顶点着色器从顶点数据中直接接收。layout (location = 0) 表示查询位置属性 1可能代表颜色 等等
uniform是全局属性,全局意味着uniform变量必须在多个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。
使用uniform更新颜色
while(!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
// 渲染
// 清除颜色缓冲
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 记得激活着色器
glUseProgram(shaderProgram);
// 更新uniform颜色
float timeValue = glfwGetTime();
float greenValue = sin(timeValue) / 2.0f + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
// 绘制三角形
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// 交换缓冲并查询IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}
更多属性:
顶点数组包括了位置和顶点颜色
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
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(aPos, 1.0);
ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}
片段着色器:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}
重新定义顶点指针,取颜色的时候注意步长属性、偏移量属性
// 位置属性
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);
# 纹理
为了能够把纹理映射(Map)到三角形上,我们需要指定三角形的每个顶点各自对应纹理的哪个部分。这样每个顶点就会关联着一个纹理坐标(Texture Coordinate)。
纹理坐标的范围通常是从(0, 0)到(1, 1)
纹理环绕方式:
| 环绕方式 | 描述 |
|---|---|
| GL_REPEAT | 对纹理的默认行为。重复纹理图像。 |
| GL_MIRRORED_REPEAT | 和GL_REPEAT一样,但每次重复图片是镜像放置的。 |
| GL_CLAMP_TO_EDGE | 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。 |
| GL_CLAMP_TO_BORDER | 超出的坐标为用户指定的边缘颜色。 |
纹理过滤:
- GL_NEAREST 近邻过滤,有颗粒感
- GL_LINEAR 线性过滤,更平滑
放大和缩小操作可是设置不同的纹理过滤选项。
多级渐远纹理:它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。
在渲染中切换多级渐远纹理级别(Level)时,OpenGL在两个不同级别的多级渐远纹理层之间会产生不真实的生硬边界。就像普通的纹理过滤一样,切换多级渐远纹理级别时你也可以在两个不同多级渐远纹理级别之间使用NEAREST和LINEAR过滤。
多级渐远纹理主要是使用在纹理被缩小的情况下。
采样器(Sampler):把纹理对象传给片段着色器
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
in vec2 TexCoord;
uniform sampler2D ourTexture;
void main()
{
FragColor = texture(ourTexture, TexCoord);
}
GLSL内部的texture函数返回一个纹理采样器,TexCoord是顶点的纹理坐标
将纹理颜色与顶点颜色混合
FragColor = texture(ourTexture, TexCoord) * vec4(ourColor, 1.0);
# 变换
OpenGL没有自带任何矩阵和向量知识,所以必须自己定义数学类和函数。
C++里可以用GLM。GLM只有头文件,便于引入。
Cesium就是使用了自定义的矩阵运算。
三维空间变换使用四元数
glsl有mat2、mat3、mat4
缩放旋转箱子
glm::mat4 trans;
trans = glm::rotate(trans, glm::radians(90.0f), glm::vec3(0.0, 0.0, 1.0));
trans = glm::scale(trans, glm::vec3(0.5, 0.5, 0.5));
把矩阵传递给着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
out vec2 TexCoord;
uniform mat4 transform;
void main()
{
gl_Position = transform * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
第一个参数是uniform位置值,第二个参数表示发送多少个矩阵,第三个表示是否进行转置,最后一个参数是矩阵数据
unsigned int transformLoc = glGetUniformLocation(ourShader.ID, "transform");
glUniformMatrix4fv(transformLoc, 1, GL_FALSE, glm::value_ptr(trans));
先旋转在平移,不改变旋转半径,先平移再旋转会改变旋转半径,角度不变。
# 坐标系统
对我们来说比较重要的总共有5个不同的坐标系统:
- 局部空间(Local Space,或者称为物体空间(Object Space))
- 世界空间(World Space)
- 观察空间(View Space,或者称为视觉空间(Eye Space))
- 裁剪空间(Clip Space)
- 屏幕空间(Screen Space)
为了将坐标从一个坐标系变换到另一个坐标系,需要用到几个变换矩阵,最重要的是模型、视图、投影矩阵。顶点坐标起始与局部空间。这里称为局部坐标,随后会变为世界坐标、观察坐标、裁剪坐标、最后以屏幕坐标结束。

局部空间:物体所在的坐标空间,对象最开始的地方。
世界空间:物体坐标通过模型矩阵变换到世界空间。
观察空间:也被称为摄像机空间或视觉空间,也就是摄像机视角所观察到的空间。
裁剪空间:裁剪空间之外的坐标会被忽略、剩下的坐标会显示在屏幕上。裁剪空间坐标在-1.0到1.0
正射投影矩阵:
glm::ortho(0.0f, 800.0f, 0.0f, 600.0f, 0.1f, 100.0f);
前两个参数指定了平截头体的左右坐标,第三和第四参数指定了平截头体的底部和顶部。通过这四个参数我们定义了近平面和远平面的大小,然后第五和第六个参数则定义了近平面和远平面的距离。这个投影矩阵会将处于这些x,y,z值范围内的坐标变换为标准化设备坐标。

透视投影矩阵
glm::mat4 proj = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);

它的第一个参数定义了fov的值,它表示的是视野(Field of View),并且设置了观察空间的大小。如果想要一个真实的观察效果,它的值通常设置为45.0f,但想要一个毁灭战士(DOOM,经典的系列第一人称射击游戏)风格的结果你可以将其设置一个更大的值。第二个参数设置了宽高比,由视口的宽除以高所得。第三和第四个参数设置了平截头体的近和远平面。我们通常设置近距离为0.1f,而远距离设为100.0f。所有在近平面和远平面内且处于平截头体内的顶点都会被渲染。
#version 330 core
layout (location = 0) in vec3 aPos;
...
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
// 注意乘法要从右向左读
gl_Position = projection * view * model * vec4(aPos, 1.0);
...
}
Z缓冲,也称深度缓冲
深度测试:GLFW会自动生成这样一个缓冲,深度之存储在每个片段的z值中,当片段想要输出颜色是,OpenGL会将它的深度值和z缓冲进行比较,如果当前的片段在其他片段之后,会被丢弃,否则会覆盖。这个过程称为深度测试,是由OpenGL自动完成的。
glEnable(GL_DEPTH_TEST);
显示多个立方体
glm::vec3 cubePositions[] = {
glm::vec3( 0.0f, 0.0f, 0.0f),
glm::vec3( 2.0f, 5.0f, -15.0f),
glm::vec3(-1.5f, -2.2f, -2.5f),
glm::vec3(-3.8f, -2.0f, -12.3f),
glm::vec3( 2.4f, -0.4f, -3.5f),
glm::vec3(-1.7f, 3.0f, -7.5f),
glm::vec3( 1.3f, -2.0f, -2.5f),
glm::vec3( 1.5f, 2.0f, -2.5f),
glm::vec3( 1.5f, 0.2f, -1.5f),
glm::vec3(-1.3f, 1.0f, -1.5f)
};
用不同的模型矩阵渲染10次
glBindVertexArray(VAO);
for(unsigned int i = 0; i < 10; i++)
{
glm::mat4 model;
model = glm::translate(model, cubePositions[i]);
float angle = 20.0f * i;
model = glm::rotate(model, glm::radians(angle), glm::vec3(1.0f, 0.3f, 0.5f));
ourShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}
# 摄像机
欧拉角是可以表示3D空间中任何旋转的三个值。
一种有三种欧拉角:俯仰角Pitch、偏航角Yaw、滚转角Roll.
.png)
摄像机位置:摄像机位置就是世界空间中指向摄像机位置的向量。
摄像机方向:摄像机指向的方向,可以是场景原点,也可以是别的。

方向向量:摄像机方向的相反方向。
上向量:摄像机视线方向向上看90度。
右向量:代表摄像机空间X轴的正方向。上向量(世界坐标)叉乘方向向量得到右向量。
叉乘符合右手定则
摄像机坐标系中:z轴正向为远离原点方向,x轴是右手方向正向(通过世界坐标系中上方向向量叉乘摄像机方向向量),y轴正方向为摄像机视线方向向上90度,由方向向量叉乘右向量得到。
fov 变量如果变小,但是屏幕大小不变,是不是意味着,屏幕需要离物体近一些,这样就放大物体了。
LookAt函数,也就是矩阵: $$ LookAt = \begin{bmatrix} \color{red}{R_x} & \color{red}{R_y} & \color{red}{R_z} & 0 \ \color{green}{U_x} & \color{green}{U_y} & \color{green}{U_z} & 0 \ \color{blue}{D_x} & \color{blue}{D_y} & \color{blue}{D_z} & 0 \ 0 & 0 & 0 & 1 \end{bmatrix} * \begin{bmatrix} 1 & 0 & 0 & -\color{purple}{P_x} \ 0 & 1 & 0 & -\color{purple}{P_y} \ 0 & 0 & 1 & -\color{purple}{P_z} \ 0 & 0 & 0 & 1 \end{bmatrix} $$ 旋转左乘平移矩阵,实际上是先平移后旋转。
# 总结
# VAO与VBO
- 在 OpenGL 中,绘制 3D 图形需要用到顶点缓冲对象(VBO)(存储顶点数据,比如坐标、颜色、纹理坐标),还需要告诉 OpenGL 如何解析这些 VBO 中的数据(比如数据的类型、步长、偏移量等)。
- 而VAO(Vertex Array Object,顶点数组对象) 就相当于一个 “配置清单”或“状态容器”,它会保存所有与顶点数据相关的配置信息(包括 VBO 的绑定、顶点属性的解析规则)。
VAO 不是存储顶点数据的,而是存储 “如何使用顶点数据” 的配置信息的对象。
没有使用VAO,每次绘制图形都需要重复执行以下步骤;
- 绑定 VBO;
- 调用
glVertexAttribPointer设置顶点属性的解析规则; - 启用对应的顶点属性。
有了 VAO 之后,你只需要一次配置这些信息并保存到 VAO 中,后续绘制时只需要绑定 VAO,就能直接复用所有配置,无需重复设置,极大简化了代码逻辑,也提升了渲染效率。
#include <GL/glew.h> // 需先初始化GLEW
#include <GLFW/glfw3.h>
#include <iostream>
// 顶点数据(三角形的三个顶点坐标)
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 左下角
0.5f, -0.5f, 0.0f, // 右下角
0.0f, 0.5f, 0.0f // 顶部
};
int main() {
// 初始化GLFW和窗口(省略,仅保留核心VAO/VBO代码)
// ...
// 1. 创建VBO(存储顶点数据)
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 2. 创建VAO(存储配置信息)
unsigned int VAO;
glGenVertexArrays(1, &VAO);
// 绑定VAO:后续的顶点属性配置都会被保存到这个VAO中
glBindVertexArray(VAO);
// 3. 配置顶点属性(解析规则),这些配置会被VAO记录
// 参数说明:
// 0:顶点属性的索引(对应着色器中的location)
// 3:每个顶点属性有3个分量(x,y,z)
// GL_FLOAT:数据类型是浮点型
// GL_FALSE:是否归一化
// 3 * sizeof(float):步长(每个顶点的字节数)
// (void*)0:偏移量(从顶点数据起始位置的偏移)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
// 启用索引为0的顶点属性
glEnableVertexAttribArray(0);
// 注:可以解绑VBO和VAO,不影响后续使用(因为VAO已经保存了配置)
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// 4. 渲染阶段:只需要绑定VAO,就能直接绘制
while (!glfwWindowShouldClose(window)) {
// 清屏
glClear(GL_COLOR_BUFFER_BIT);
// 绑定VAO(此时会自动恢复之前的顶点属性配置和VBO绑定)
glBindVertexArray(VAO);
// 绘制三角形(从第0个顶点开始,绘制3个顶点)
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
// 释放资源
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// ...
return 0;
}
总结一句话,VAO存储了从哪个VBO找数据,怎么解析数据的规则。
多个VAO与VBO绑定
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
// 1. 三角形的顶点数据(x,y,z)
float triangleVertices[] = {
-0.8f, -0.5f, 0.0f, // 左下角
-0.2f, -0.5f, 0.0f, // 右下角
-0.5f, 0.5f, 0.0f // 顶部
};
// 2. 矩形的顶点数据(使用三角形带,也可以用两个三角形拼接)
float rectangleVertices[] = {
0.2f, -0.5f, 0.0f, // 左下角
0.8f, -0.5f, 0.0f, // 右下角
0.8f, 0.5f, 0.0f, // 右上角
0.2f, 0.5f, 0.0f // 左上角
};
int main() {
// 初始化GLFW和创建窗口(省略,确保OpenGL环境正常)
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "Multiple VAO/VBO", NULL, NULL);
glfwMakeContextCurrent(window);
glewInit();
glViewport(0, 0, 800, 600);
// --------------------------
// 步骤1:创建第一个VAO和VBO(三角形)
// --------------------------
unsigned int VAO1, VBO1;
// 创建VAO和VBO(每个对象对应一个唯一的ID)
glGenVertexArrays(1, &VAO1);
glGenBuffers(1, &VBO1);
// 绑定VAO1,后续配置会保存到VAO1中
glBindVertexArray(VAO1);
// 绑定VBO1并上传三角形数据
glBindBuffer(GL_ARRAY_BUFFER, VBO1);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVertices), triangleVertices, GL_STATIC_DRAW);
// 配置顶点属性(三角形的顶点是3个分量)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 解绑(可选,不影响后续使用)
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// --------------------------
// 步骤2:创建第二个VAO和VBO(矩形)
// --------------------------
unsigned int VAO2, VBO2;
glGenVertexArrays(1, &VAO2);
glGenBuffers(1, &VBO2);
// 绑定VAO2,后续配置会保存到VAO2中
glBindVertexArray(VAO2);
// 绑定VBO2并上传矩形数据
glBindBuffer(GL_ARRAY_BUFFER, VBO2);
glBufferData(GL_ARRAY_BUFFER, sizeof(rectangleVertices), rectangleVertices, GL_STATIC_DRAW);
// 配置顶点属性(矩形的顶点也是3个分量,配置规则和三角形一致,也可以不同)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 解绑
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// --------------------------
// 步骤3:渲染循环(切换VAO绘制不同模型)
// --------------------------
while (!glfwWindowShouldClose(window)) {
// 清屏(黑色背景)
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 绘制三角形:绑定VAO1,直接绘制
glBindVertexArray(VAO1);
glDrawArrays(GL_TRIANGLES, 0, 3); // 3个顶点
// 绘制矩形:绑定VAO2,直接绘制(用GL_QUADS或GL_TRIANGLE_FAN)
glBindVertexArray(VAO2);
// GL_TRIANGLE_FAN:从第0个顶点开始,绘制4个顶点(形成矩形)
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
glfwSwapBuffers(window);
glfwPollEvents();
}
// 步骤4:释放资源(每个VAO/VBO都要删除)
glDeleteVertexArrays(1, &VAO1);
glDeleteVertexArrays(1, &VAO2);
glDeleteBuffers(1, &VBO1);
glDeleteBuffers(1, &VBO2);
glfwDestroyWindow(window);
glfwTerminate();
return 0;
}