美文网首页
[OpenGL]VBO,VAO和EBO详解

[OpenGL]VBO,VAO和EBO详解

作者: 太刀 | 来源:发表于2021-02-26 01:37 被阅读0次
美女

这篇文章通过几个简单实例,讨论了OpenGL编程中的 VBO,VAO 和 EBO 概念。

1. VBO 和 VAO

1.1 VBO(Vertex Buffer Object) 顶点缓冲对象

VBO 是显存中的一片缓冲区域,存放从内存中提交过来的顶点数据。GPU 绘制时,需要对 VBO 中的数据进行解析,以便将数据正确的提交给着色器中对应的属性。例如,GPU 需要知道 VBO 中哪一块数据是某个顶点的坐标,哪一块数据是某个顶点的顶点颜色等,通过调用 glVertexAttribPointer 方法来设置解析规则,GPU 能够取到正确的数据供着色器使用

1.2 VAO(Vertex Array Object)顶点数组对象

上述 glVertexAttribPoint的调用结果被记录到 VAO 中,最终绘制的时候直接通过 VAO 中存储的指针去缓冲区取数据,而不需要再重复解析 VBO,VAO 和 VBO 的关系大概是这样:

VAO和VBO的关系
  • VBO 是纯数据的缓冲区,示意图中分别用两个VBO来保存三角形的顶点位置和顶点颜色数据
  • VAO 是一个数组,保存每一类顶点属性的解析结果,OpenGL中貌似最多支持 16 种顶点属性,这里的顶点属性就是 glVertexAttribPointer 方法的第一个参数指定的,通常0表示顶点坐标,1表示顶点颜色
  • 使用 VAO 的好处是,你只需要针对 VBO 做一次解析,将结果存储到 VAO 中,每一帧渲染使用 VAO 的指针来访问缓冲区数据,而不需要每一帧都做解析
    [OpenGL]绘制三角形 这篇文章中,我们实现了一个完整的绘制三角形的程序,完整的代码都贴在了该文章的最后一部分,我们这里来看看核心的绘制部分代码如下
// draw_triangle.h
#include "shader_common.h"

const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

int draw_trangle()
{   
    // 初始化 OpenGL
    init_opengl();
    
    // 编译和链接着色器
    GLuint shaderProgram = compile_shader(vertexShaderSource, fragmentShaderSource);

    // 顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f  // Top   
    };

    // 申请缓冲区
    GLuint VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);  

    // 绑定VAO,表示在这之后针对 VBO 的解析都会记录在 VAO 中
    glBindVertexArray(VAO);

    // 提交数据
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 使用Buffer中的数据在 VAO 生成 0 号顶点属性的指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);

    // 启用 0 号顶点属性
    glEnableVertexAttribArray(0);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO
    glBindVertexArray(0);
    
    while (!glfwWindowShouldClose(window))
    {
        // 处理事件
        glfwPollEvents();

        // 清屏
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 指定着色器程序
        glUseProgram(shaderProgram);

        // 绑定VAO
        glBindVertexArray(VAO);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 解绑 VAO
        glBindVertexArray(0);

        // 双缓冲交换
        glfwSwapBuffers(window);
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);   
    glfwTerminate();
    return 0;
}

可以看到,我们在初始化时将 0 号属性的解析

 // 使用Buffer中的数据在 VAO 生成 0 号顶点属性的指针
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);

记录在了 VAO 中,在后续的渲染循环中,我们没有再访问 VBO,而是直接通过绑定 VAO 来调用渲染命令:

       // 绑定VAO
        glBindVertexArray(VAO);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);
1.3 VAO 对应多个 VBO

我们来实现一个程序,将三角形的顶点和颜色数据分别用两个 VBO 提交,然后解析到同一个 VAO 中,再绑定这个 VAO 进行绘制。我们需要在上述的代码中做如下几个更改:

  • 首先,着色器代码需要支持除顶点位置(0号顶点属性)之外的另外一个属性:顶点颜色,也就是 1 号顶点属性,着色器代码修改如下
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"layout (location = 1) in vec4 color;\n"
"out vec4 v_color;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"v_color = color;\n"
"}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
"in vec4 v_color;\n"
"layout(location=0) out vec4 o_fragColor;\n"
"void main()\n"
"{\n"
"o_fragColor = v_color;\n"
"}\n\0";
  • 其次,我们需要在内存中分配两个区域分别存放顶点的位置和颜色信息

    // 顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f  // Top   
    };

    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };
  • 再次,在渲染循环之前分别提交顶点位置和颜色信息,绑定到不同的 VBO,并申请 VAO 记录两个 VBO 的解析结果
// 申请缓冲区
    GLuint VBO, VAO, VBO2;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &VBO2);

    // 绑定VAO
    glBindVertexArray(VAO);

    // 提交数据和解析规则
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, VBO2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO
    glBindVertexArray(0);
1.4 渲染部分完整代码

这里的初始化方法 init_opengl和着色器编译方法 compile_shader 可以查看 [OpenGL]绘制三角形 文章的代码部分,渲染部分的完整代码如下

int draw_trangle()
{
    // 初始化 OpenGL
    init_opengl();

    // 编译和链接着色器
    GLuint shaderProgram = compile_shader(vertexShaderSource, fragmentShaderSource);

    // 顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f  // Top   
    };

    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };

    // 申请缓冲区
    GLuint VBO, VAO, VBO2;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &VBO2);

    // 绑定VAO
    glBindVertexArray(VAO);

    // 提交数据和解析规则
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, VBO2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO
    glBindVertexArray(0);

    while (!glfwWindowShouldClose(window))
    {
        // 处理事件
        glfwPollEvents();

        // 清屏
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 指定着色器程序
        glUseProgram(shaderProgram);

        // 绑定VAO
        glBindVertexArray(VAO);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 解绑 VAO
        glBindVertexArray(0);

        // 双缓冲交换
        glfwSwapBuffers(window);
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glfwTerminate();
    return 0;
}

渲染的结果:


渲染结果

2. EBO

2.1 绘制两个三角形

我们来考虑绘制两个三角形的情形,假如我们复用上述三角形其中两个顶点,另外增加一个顶点,构成一个新的三角形,首先能想到有两种方法可以绘制两个三角形:

  • 第一种方法,增加顶点位置数组和颜色数组的长度到6,复用刚才的绘制代码
    // 顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f,  // Top 
         -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  -1.0f, 0.0f,  // Down
    };
    // 颜色数据
    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };

在调用 glDrawArrays方法时要修改传参,渲染6个顶点

glDrawArrays(GL_TRIANGLES, 0, 6);
  • 第二种方法,分别使用两个 VAO 保存两个三角形的解析数据进行绘制,绘制部分的代码如下:
int draw_trangle()
{
    // 初始化 OpenGL
    init_opengl();

    // 编译和链接着色器
    GLuint shaderProgram = compile_shader(vertexShaderSource, fragmentShaderSource);

    // 三角形1顶点数据
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f,  // Top      
    };

    // 三角形2顶点数据
    GLfloat vertices2[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  -1.0f, 0.0f,  // Down   
    };

    // 颜色数据
    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        0.0f, 0.5f, 0.5f, 1.0f
    };

    // 申请缓冲区
    GLuint VBO1, VBO2, ColorVBO, VAO1, VAO2;
    glGenVertexArrays(1, &VAO1);
    glGenVertexArrays(1, &VAO2);
    glGenBuffers(1, &VBO1);
    glGenBuffers(1, &VBO2);
    glGenBuffers(1, &ColorVBO);

    // 绑定VAO
    glBindVertexArray(VAO1);

    // 提交数据和解析规则
    glBindBuffer(GL_ARRAY_BUFFER, VBO1);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, ColorVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 绑定VAO
    glBindVertexArray(VAO2);

    // 提交数据和解析规则
    glBindBuffer(GL_ARRAY_BUFFER, VBO2);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices2), vertices2, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, ColorVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // 解绑VAO
    glBindVertexArray(0);

    while (!glfwWindowShouldClose(window))
    {
        // 处理事件
        glfwPollEvents();

        // 清屏
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 指定着色器程序
        glUseProgram(shaderProgram);

        // 绑定VAO
        glBindVertexArray(VAO1);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 绑定VAO
        glBindVertexArray(VAO2);

        // 绘制指令
        glDrawArrays(GL_TRIANGLES, 0, 3);

        // 解绑 VAO
        glBindVertexArray(0);

        // 双缓冲交换
        glfwSwapBuffers(window);
    }

    glDeleteVertexArrays(1, &VAO1);
    glDeleteBuffers(1, &VBO1);
    glfwTerminate();
    return 0;
}

两种方法渲染结果相同,示意图


image.png
2.2 优化

上述种方法,其实都有顶点数据的冗余,包括位置和颜色数据的冗余,6个顶点我们需要使用 6 个 float3 来表示位置和颜色,当需要渲染的顶点数时会造成更大的冗余,我们看看如何针对这个问题来进行优化,事实上我们复用了两个顶点,也就是说其实 4 个顶点数据就够用了,为此我们引入了EBO(Element Buffer Object),索引缓冲对象,来解决数据冗余的问题。
使用 EBO 来解决上面两个三角形数据冗余问题的思路是这样的:只保存4个顶点数据,引入EBO来存储两个三角形对于顶点数据的索引:

    // 四个顶点,有两个复用
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.0f,
         0.0f,  -1.0f, 0.0f,
    };

    // 四个颜色,两个复用
    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };

    GLuint indices[] = {
        0, 1, 2,    // 第一个三角形使用的顶点下标
        0, 1, 3     // 第二个三角形使用的顶点下标
    };

简单来说就是,VBO 中存放的是去重后的顶点数据,当顶点复用数目较多时可以节省很多存储空间,另外单独开辟一个EBO缓冲区来存储每个顶点的实际数据在 VBO 中对应的下标值,在提交时将EBO信息也提交,计算结果得到 VAO,绘制时绑定 VAO 来访问顶点数据。我这里使用了相同的下标数组 indices 来使用顶点坐标和顶点颜色数据,你也可以使用另外的数组来指定不同三角形使用的颜色值,如

GLuint colorIndices[] = {
    0, 1, 2,
    1, 2, 3
};

只要在下面为 VAO 绑定属性时传递正确的数据就可以了。使用 EBO 来绘制两个三角形的完整逻辑代码:

int draw_trangle()
{
    init_opengl();

    GLuint shaderProgram = compile_shader(vertexShaderSource, fragmentShaderSource);

    // 四个顶点,有两个复用
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f,
         0.5f, -0.5f, 0.0f,
         0.0f,  0.5f, 0.0f,
         0.0f,  -1.0f, 0.0f,
    };

    // 四个颜色,两个复用
    GLfloat colors[] = {
        1.0f, 0.0f, 0.0f, 1.0f,
        0.0f, 1.0f, 0.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f,
        0.0f, 0.0f, 1.0f, 1.0f
    };

    GLuint indices[] = {
        0, 1, 2,    // 第一个三角形使用的顶点下标
        0, 1, 3     // 第二个三角形使用的顶点下标
    };

    // 缓冲区生成
    GLuint VBO, ColorVBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &ColorVBO);
    glGenBuffers(1, &EBO);
    glBindVertexArray(VAO);

    // 解析并提交位置属性
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    // 解析并提交颜色属性
    glBindBuffer(GL_ARRAY_BUFFER, ColorVBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(1);

    // 解绑缓冲区
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);   

    while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 指定着色器
        glUseProgram(shaderProgram);

        // 绑定 VAO
        glBindVertexArray(VAO);
        
        // 根据索引绘制
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        glBindVertexArray(0);

        glfwSwapBuffers(window);
    }

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glfwTerminate();
    return 0;
}

需要特别注意的几点:

  • 索引数组的类型必须用 GLuint,不要误用 GLfloat,否则将无法得到你想要的绘制结果
  • 注意颜色数据的尺寸
  • 绘制的 API 发生了变化,不再是 glDrawArrays 而是 glDrawElements,需要注意传参的顺序。

相关文章

  • [OpenGL]VBO,VAO和EBO详解

    这篇文章通过几个简单实例,讨论了OpenGL编程中的 VBO,VAO 和 EBO 概念。 1. VBO 和 VAO...

  • opengl绘制三角形

    步骤 编译和链接着色器程序 创建并且绑定VAO, VBO, EBO 使用着色器程序和VAO,并且渲染 VBO 用来...

  • Opengl ES之EBO

    前面我们介绍了VBO与VAO等缓冲对象,今天我们来介绍一下EBO。 对于VBO或VAO可以查看之前的文章:Open...

  • LearnOpenGL VAO VBO EBO

    可以理解为EBO是最后一个VBO, EBO是专门用来处理索引的 有的教程说EBO是最后一个特殊的VAO, 也可以...

  • OpenGL - 关于顶点属性的理解

    标准化设备坐标 坐标 坐标 + 颜色 坐标 + 颜色 + 纹理 VBO - VAO - EBO(或者叫IBO) 关...

  • Opengl ES之VBO和VAO

    前言 本文主要介绍了什么是VBO/VAO,为什么需要使用VBO/VAO以及如何使用VBO和VAO。 VBO 什么是...

  • OpenGL ES VAO、VBO、EBO、FBO、PBO、TB

    面试中经常被问到的 OpenGL ES 对象,你知道的有哪些? 该原创文章首发于微信公众号:字节流动 VBO 和 ...

  • OpenGL系列之十:VAO、VBO、EBO的应用

    目录 相关文章 OpenGL系列之一:OpenGL第一个程序[https://www.jianshu.com/p/...

  • 11 - OpenGL学习之EBO VAO VBO 理解与使用

    前言:由于ES 是针对手持设备设计的,而这类设备资源非常紧张,所以 ES引入 EBO, VAO,VBO的概念,它们...

  • OpenGL中的VAO和VBO

    对我来说,这是两个非常容易忘的概念,细节也不太好记忆。所以我简单粗暴地总结一下二者。可能未必精确,但get到了大致...

网友评论

      本文标题:[OpenGL]VBO,VAO和EBO详解

      本文链接:https://www.haomeiwen.com/subject/axwpfltx.html