【OpenGL】Triangle、VBO、VAO、EBO

GPU有成千上万的小核心,每个核心都可以跑专门的小程序,这种程序称为shader。

GPU有固定的shader流水线,我们也可以通过自定义重写shader,精细的控制流水线的每一部分。

蓝色区域是我们可以重写的shader

image

VBO:顶点数据仓库

  • 功能
    VBO是显存中的二进制数据块,负责高效存储顶点数据(如坐标、颜色、法线等),大幅减少CPU-GPU通信开销。
  • 特点
    • 纯数据容器:以连续二进制流存储,例如 [x1,y1,z1,r1,g1,b1,x2,y2,z2,...]
    • 灵活使用模式:支持静态(GL_STATIC_DRAW)、动态(GL_DYNAMIC_DRAW)等存储策略。

image

配置时,行为是状态机,只支持载入各种类型的一种对象。shader运行时,GPU会并发从VAO索引取出数据执行

1. 生成缓冲对象

使用 glGenBuffer 函数创建一个新的 VBO,并返回其 ID。

GLuint vbo;
glGenBuffers(1, &vbo); // 1 表示一次性创建几个,后一个参数提供数组指针,将ID回存入数组

2. 绑定缓冲对象

使用 glBindBuffer 函数将创建的 VBO 绑定为当前的顶点缓冲对象。

OPenGL状态机配置阶段只支持激活一个VBO,所以后续数据传入也必须指定这个缓冲区类型。

glBindBuffer(GL_ARRAY_BUFFER, vbo);

3. 上传数据

使用 glBufferData 函数将顶点数据上传到显卡内存。

float vertices[] = {
    // 顶点数据,例如三角形的三个顶点
    0.0f,  1.0f, 0.0f,
   -1.0f, -1.0f, 0.0f,
    1.0f, -1.0f, 0.0f
};

//GL_STATIC_DRAW 表示数据不会频繁更改。
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

传入到之前绑定好类型和对象的缓冲区

数据传到的缓冲区类型要和绑定的一致

// 绑定 VBO_A 并传输数据
glBindBuffer(GL_ARRAY_BUFFER, vboA);
glBufferData(GL_ARRAY_BUFFER, sizeof(dataA), dataA, GL_STATIC_DRAW); // 数据传至 vboA

// 绑定 VBO_B 并传输数据
glBindBuffer(GL_ARRAY_BUFFER, vboB);
glBufferData(GL_ARRAY_BUFFER, sizeof(dataB), dataB, GL_STATIC_DRAW); // 数据传至 vboB

VAO:数据解析蓝图

  • 功能
    VAO定义VBO中数据的解析规则,通过顶点属性指定数据结构、类型和用途,实现数据与着色器的对接。
  • 核心配置
    • 属性指针glVertexAttribPointer):定义数据起始偏移、类型(如GL_FLOAT)、向量维度(如3D坐标)等。
    • 属性启用glEnableVertexAttribArray):激活对应属性通道,与着色器layout(location = N)绑定。
  • 优势
    • 状态封装:绘制时只需绑定VAO,自动恢复所有属性配置。
    • 多VBO支持:可关联多个VBO(如坐标VBO、颜色VBO),通过不同属性指针区分。

其同一存储在显存中

1. 生成 VAO
使用 glGenVertexArrays 函数创建一个新的 VAO,并返回其 ID。

GLuint VAO;
glGenVertexArrays(1, &VAO);

2. 绑定 VAO

使用 glBindVertexArray 函数将创建的 VAO 绑定为当前的顶点数组对象。绑定 VAO 后,所有后续的顶点属性配置(如 VBO 绑定、顶点属性指针设置)都会存储在这个 VAO 中。

glBindVertexArray(VAO);

后续 glVertexAttribPointerglEnableVertexAttribArray、VBO的绑定状态,会自动存入 VAO

3. 生成并绑定VBO

GLfloat vertices[] = {
    // 顶点坐标
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

GLuint VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

4. 设置顶点属性解析方式

image

// 设置顶点属性解析方式
glVertexAttribPointer(
	0, // 逻辑索引,shader中使用location = n 获取,独立于VBO,不能重复
	3, // 每顶点分量数 3(vec3)
	GL_FLOAT, //数据类型 GL_FLOAT
	GL_FALSE,  // 无归一化
	3 * sizeof(float), // 步长(字节跨度)到下一个相同顶点的字节长度
	(void*)0 // 偏移量,相对于VBO起始位置
);
glEnableVertexAttribArray(0); // 激活0索引的数据传输通道,供shader使用location从VBO读取数据给shader
  • 逻辑索引:shader中使用location = n 获取。最多15个
    • 每个VAO相当于一个shader程序环境,逻辑索引对于shader来说不能重复冲突,所以逻辑索引每个VAO内不能重复。
  • 分量数:分量的值精度所占字节是一开始就确定的,再依靠分量数可以获取到顶点的所有分量。
  • 步长:顶点数据之间可以有间隔,或者插入其他数据。所以需要用步长来确定下一个相同顶点

glVertexAttribPointer 时,VAO会将当前绑定的VBO的引用和配置记录下,可以记录无限个绑定的。渲染时批量取出

逻辑索引用于在 shader 中取用数据,glVertexAttribPointer 的配置会自动生效,返回解析好的数据。

layout (location = 0) in vec3 aPos;

如果glVertexAttribPointer配置是3向量接受是2向量,会截断的第3向量

graph TB A[初始化阶段] --> B[创建 VAO 并绑定] B --> C1[绑定 vbo_position] C1 --> D1[配置属性0 位置] B --> C2[绑定 vbo_color] C2 --> D2[配置属性1 颜色] A --> E[解绑 VAO 和 VBO] F[渲染阶段] --> G[绑定 VAO] G --> H[自动恢复属性0→vbo_position] G --> I[自动恢复属性1→vbo_color] G --> J[调用 glDrawArrays]

image

完整流程示例

#include <iostream>
#include <cstdlib>

#include <GL/glew.h>
#include <GLFW/glfw3.h>

// shader硬编码可以定义一个宏 #define GET_STR(x) #x 会自动加双引号和换行
#define GET_STR(x) #x

float vertices[] = {
    -0.5f, -0.5f, 0.0f, // 左下角
     0.5f, -0.5f, 0.0f, // 右下角
     0.0f,  0.5f, 0.0f  // 顶部
};

const char *vertexShaderSource = R"(
    #version 330 core
    layout (location = 0) in vec3 aPos;
    void main()
    {
        gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
    }
)";

const char *fragmentShaderSource = R"(
    #version 330 core
    out vec4 FragColor;
    void main()
    {
        FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); // 绘制一个橙色三角形
    }
)";

void processInput(GLFWwindow *window) {
    // 检查ESC键状态
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, true); // 退出程序
    }
}

int main(int, char**) {
    
    // 初始化GLFW
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return EXIT_FAILURE;
    }

    // 设置OpenGL版本和配置文件
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心配置
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    // 创建窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "My OpenGL Game", NULL, NULL);
    if (window == NULL) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate(); // 退出
        return -1;
    }

    // 设置当前上下文
    glfwMakeContextCurrent(window); // 将这个窗口作为当前glfw上下文

    // 初始化GLEW
    glewExperimental = GL_TRUE;  // 需要在glewInit()之前设置
    if (glewInit() != GLEW_OK) {
        std::cerr << "Failed to initialize GLEW" << std::endl;
        glfwTerminate();
        return EXIT_FAILURE;
    }

    // 设置视口,左下角起始坐标点,可以绘制的宽高
    glViewport(0, 0, 800, 600);

    // VAO
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    // VBO
    unsigned int VBO;
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // set vertex attribute pointers
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);


    // vertex shader
    unsigned int vertexShader;
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); // count: 几个字串
    glCompileShader(vertexShader);

    // fragment shader
    unsigned int fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    // shader program
    unsigned int shaderProgram;
    shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);


    // 主渲染循环
    // 当前输入作用于当前帧
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents(); // 获取键盘和鼠标事件传递到window上下文
        processInput(window); // 判断上下文中的事件

        // 设置背景色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清空缓冲区后填充的颜色,默认值,背景色
        glClear(GL_COLOR_BUFFER_BIT); // 清空缓冲区颜色

        glBindVertexArray(VAO);
        glUseProgram(shaderProgram);
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 交换缓冲区
        glfwSwapBuffers(window); // 双缓冲区,一个用于显示,一个用于绘制
    }

    glfwTerminate();

    return 0;
}

面剔除

法向量反过来的不渲染,法向量方向根据点的顺序左手法则确定。

// 开启面剔除
glEnable(GL_CULL_FACE);
glCullFace(GL_BACK); // 剔除背面
glCullFace(GL_FRONT); // 剔除正面
1.VBO存储 VAO配置点的解析规则 开启逻辑通道
2.shader从逻辑通道读取数据
3. 规定shader绘制的点数和形状
#include <iostream>
#include <cstdlib>

#include <GL/glew.h>
#include <GLFW/glfw3.h>

// shader硬编码可以定义一个宏 #define GET_STR(x) #x 会自动加双引号和换行
#define GET_STR(x) #x

float vertices[] = {
    -0.5f, -0.5f, 0.0f, // 左下角
    0.5f, -0.5f, 0.0f, // 右下角
    0.0f,  0.5f, 0.0f  // 顶部

    ,0.0f,  0.5f, 0.0f
    ,-0.5f, -0.5f, 0.0f
    ,-0.5f, 0.7f, 0.0f
};

const char *vertexShaderSource = R"(
    #version 330 core
    layout (location = 0) in vec3 aPos;
    void main()
    {
        gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
    }
)";

const char *fragmentShaderSource = R"(
    #version 330 core
    out vec4 FragColor;
    void main()
    {
        FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); // 绘制一个橙色三角形
    }
)";

void processInput(GLFWwindow *window) {
    // 检查ESC键状态
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, true); // 退出程序
    }
}

int main(int, char**) {
    
    // 初始化GLFW
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return EXIT_FAILURE;
    }

    // 设置OpenGL版本和配置文件
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心配置
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    // 创建窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "My OpenGL Game", NULL, NULL);
    if (window == NULL) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate(); // 退出
        return -1;
    }

    // 设置当前上下文
    glfwMakeContextCurrent(window); // 将这个窗口作为当前glfw上下文

    // 初始化GLEW
    glewExperimental = GL_TRUE;  // 需要在glewInit()之前设置
    if (glewInit() != GLEW_OK) {
        std::cerr << "Failed to initialize GLEW" << std::endl;
        glfwTerminate();
        return EXIT_FAILURE;
    }

    // 设置视口,左下角起始坐标点,可以绘制的宽高
    glViewport(0, 0, 800, 600);
    // 开启面剔除
    glEnable(GL_CULL_FACE);
    glCullFace(GL_BACK); // 剔除背面

    // VAO
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    // VBO
    unsigned int VBO;
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // set vertex attribute pointers
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);


    // vertex shader
    unsigned int vertexShader;
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); // count: 几个字串
    glCompileShader(vertexShader);

    // fragment shader
    unsigned int fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    // shader program
    unsigned int shaderProgram;
    shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    // 主渲染循环
    // 当前输入作用于当前帧
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents(); // 获取键盘和鼠标事件传递到window上下文
        processInput(window); // 判断上下文中的事件

        // 设置背景色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清空缓冲区后填充的颜色,默认值,背景色
        glClear(GL_COLOR_BUFFER_BIT); // 清空缓冲区颜色

        glBindVertexArray(VAO);
        glUseProgram(shaderProgram);
        glDrawArrays(GL_TRIANGLES, 0, 6); // 绘制三角形, count: 顶点数量

        // 交换缓冲区
        glfwSwapBuffers(window); // 双缓冲区,一个用于显示,一个用于绘制
    }

    glfwTerminate();

    return 0;
}

EBO

索引缓冲对象(Element Buffer Object)

float vertices[] = {
	// 第一个三角形
    -0.5f, -0.5f, 0.0f, // 左下角
    0.5f, -0.5f, 0.0f, // 右下角
    0.0f,  0.5f, 0.0f  // 顶部
	// 第二个三角形
    ,0.0f,  0.5f, 0.0f
    ,-0.5f, -0.5f, 0.0f
    ,-0.5f, 0.7f, 0.0f
};

显然,有顶点叠加,矩形的4个点变成6个顶点,有50%的额外开销。或者对于相接的三角形也是如此。

那么我们不直接使用点,而是用点的索引来构造三角形,点的数据只用有一份。

float vertices[] = {
    -0.5f, -0.5f, 0.0f, // 0
    0.5f, -0.5f, 0.0f, // 1
    0.0f,  0.5f, 0.0f  // 2
    // ,0.0f,  0.5f, 0.0f
    // ,-0.5f, -0.5f, 0.0f
    ,-0.5f, 0.7f, 0.0f // 3
};
// 0,1,2    2,1,3 这样来构造三角形
float vertices[] = {
    -0.5f, -0.5f, 0.0f, // 0
    0.5f, -0.5f, 0.0f, // 1
    0.0f,  0.5f, 0.0f  // 2
    // ,0.0f,  0.5f, 0.0f
    // ,-0.5f, -0.5f, 0.0f
    ,-0.5f, 0.7f, 0.0f // 3
};


1、创建EBO

// EBO
unsigned int EBO;
glGenBuffers(1, &EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

2、传索引数据

unsigned indices[] = { // 注意索引从0开始
		0, 1, 3, // 第一个三角形
		1, 2, 3 // 第二个三角形
	}
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

3、状态机绑定EBO

	glBindVertexArray(VAO);
	glUseProgram(shaderProgram);
	// glDrawArrays(GL_TRIANGLES, 0, 6); // 绘制三角形, count: 顶点数量, 直接通过VBO顶点绘制

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
	glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);// 通过EBO索引绘制

状态机

OPenGL是状态机

配置时,同一时间只能有一个VAO和VBO进入状态机,然后使用glVertexAttribPointer关联VBO到VAO的栏位,保存VBO解析配置。

image

image

EBO的配置与VBO类似,但是是独占式的,配置时只能绑定到VAO唯一的EBO索引,渲染时也只能载入唯一的EBO来索引作用于多个VBO。

image

VAO 仅保存最后一次绑定的 EBO 引用,而非多个 EBO 的集合
EBO单独对于各个VBO索引,而不是将所有VBO作为集合索引

取出VBO数据时只看所有配置过的索引,不看当前绑定的什么VBO。而EBO需要看当前绑定的EBO

#include <iostream>
#include <cstdlib>

#include <GL/glew.h>
#include <GLFW/glfw3.h>

float vertices[] = {
    -0.5f, -0.5f, 0.0f, // 0
    0.5f, -0.5f, 0.0f, // 1
    0.0f,  0.5f, 0.0f, // 2
    -0.5f, 0.7f, 0.0f // 3
};

unsigned indices[] = { // 注意索引从0开始
	0, 1, 3, // 第一个三角形
	1, 2, 3 // 第二个三角形
};

const char *vertexShaderSource = R"(
    #version 330 core
    layout (location = 0) in vec3 aPos;
    void main()
    {
        gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
    }
)";

const char *fragmentShaderSource = R"(
    #version 330 core
    out vec4 FragColor;
    void main()
    {
        FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); // 绘制一个橙色三角形
    }
)";

void processInput(GLFWwindow *window) {
    // 检查ESC键状态
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) {
        glfwSetWindowShouldClose(window, true); // 退出程序
    }
}

int main(int, char**) {
    
    // 初始化GLFW
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return EXIT_FAILURE;
    }

    // 设置OpenGL版本和配置文件
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); // 使用核心配置
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    // 创建窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "My OpenGL Game", NULL, NULL);
    if (window == NULL) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate(); // 退出
        return -1;
    }

    // 设置当前上下文
    glfwMakeContextCurrent(window); // 将这个窗口作为当前glfw上下文

    // 初始化GLEW
    glewExperimental = GL_TRUE;  // 需要在glewInit()之前设置
    if (glewInit() != GLEW_OK) {
        std::cerr << "Failed to initialize GLEW" << std::endl;
        glfwTerminate();
        return EXIT_FAILURE;
    }

    // 设置视口,左下角起始坐标点,可以绘制的宽高
    glViewport(0, 0, 800, 600);
    // 开启面剔除
    // glEnable(GL_CULL_FACE);
    // glCullFace(GL_BACK); // 剔除背面

    // VAO
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    // VBO
    unsigned int VBO;
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO); // 绑定VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // set VBO
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // EBO
    unsigned int EBO;
    glGenBuffers(1, &EBO);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // vertex shader
    unsigned int vertexShader;
    vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL); // count: 几个字串
    glCompileShader(vertexShader);

    // fragment shader
    unsigned int fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    // shader program
    unsigned int shaderProgram;
    shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    // 主渲染循环
    // 当前输入作用于当前帧
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents(); // 获取键盘和鼠标事件传递到window上下文
        processInput(window); // 判断上下文中的事件

        // 设置背景色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清空缓冲区后填充的颜色,默认值,背景色
        glClear(GL_COLOR_BUFFER_BIT); // 清空缓冲区颜色

        glBindVertexArray(VAO);
        glUseProgram(shaderProgram);
        // glDrawArrays(GL_TRIANGLES, 0, 6); // 绘制三角形, count: 顶点数量, 直接通过VBO顶点绘制

        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);// 通过EBO索引绘制

        // 交换缓冲区
        glfwSwapBuffers(window); // 双缓冲区,一个用于显示,一个用于绘制
    }

    glfwTerminate();

    return 0;
}
posted @ 2025-07-27 03:49  丘狸尾  阅读(39)  评论(0)    收藏  举报