OpenGL入门——使用EBO绘制三角形

发布时间 2023-09-03 21:28:22作者: 一只小瓶子

上一节OpenGL入门——第一个三角形(1) - 一只小瓶子 - 博客园 (cnblogs.com)介绍了opengl怎么使用VAO和VBO绘制一个三角形


这一节介绍一下使用EBO绘制

 

元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。

为什么会需要用到元素缓冲对象呢?因为上一节我们提到了绘制三角形就是输入3个顶点进行绘制,但是当我们在构建一个平面需要用到很多个三角形的时候,这里面就会有一些三角形的顶点是是重叠的。最简单的例子就是一个矩形,可以看出是两个三角形,但是这两个三角形有两个顶点重叠了

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

如上所示,右下角和左上角出现了两次,这样就会产生额外的开销,矩形其实使用4个顶点就够了。同样的,如果一个模型由成千上万个三角形组成的,就会产生一大堆浪费。

对于这个物体,OpenGL提供了EBO缓冲对象,存储绘制的顶点索引(称为索引绘制)。这样我们就不需要重复定义顶点了,如下所示

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

unsigned int indices[] = {
    // 注意索引从0开始! 
    // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
    // 这样可以由下标代表顶点组合成矩形

    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

使用EBO对象和VAO、VBO类似,先创建一个EBO对象,把函数调用放在绑定和解绑函数调用之间

    //生成EBO对象,缓冲ID为EBO
    unsigned int EBO;
    glGenBuffers(1, &EBO);

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);//缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);//数据传入缓冲内存中
        ...
        ...

    while (!glfwWindowShouldClose(window))
    {
        ...
                ...
        ///绘制物体
        glUseProgram(shaderProgram);//激活程序对象

        glBindVertexArray(VAO);
        
        //使用EBO绘制, 缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);//缓冲目标GL_ELEMENT_ARRAY_BUFFER
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//绘制图元为三角形,绘制顶点数量6,索引类型,偏移量0
        ...
        ...
    }

这里看一下VAO、VBO、EBO三者的关系:

如上图所示,在绑定VAO时,绑定的最后一个EBO对象存储为VAO的元素缓冲区对象,所以在绘制的时候绑定到VAO也会自动绑定该EBO。

注意事项:在VAO未解绑前,不可以解绑EBO,否则会没有这个EBO配置了。

 

完整代码

//GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include "shader.h"
#include "stb_image.h"

const char *vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 aPos;// 位置变量的属性位置值为 0 \n"
"void main()\n"
"{\n"
"   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\0";

const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
"   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);//\n"
"}\0";

//改变窗口大小
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    glViewport(0, 0, width, height);
}

//输入
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//点击ESC键退出绘制
        glfwSetWindowShouldClose(window, true);
}

GLFWwindow* init_window()
{
    ///窗口初始化
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号,当API以不兼容的方式更改时,该值会增加。
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号,当特性被添加到API中时,它会增加,但是它保持向后兼容。
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//使用核心模式,不兼容已废弃函数

    //创建glfw窗口
    GLFWwindow* window = glfwCreateWindow(800, 600, "ping-window", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "failed to create GLFW window" << std::endl;
        glfwTerminate();//释放/删除之前的分配的所有资源
        return nullptr;
    }
    glfwMakeContextCurrent(window);//将窗口的上下文设置为当前线程的主上下文
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//注册为调整窗口回调函数

    //GLAD是用来管理OpenGL的函数指针的,在调用任何OpenGL的函数之前初始化GLAD
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))//给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数
    {
        std::cout << "failed to intialize GLAD" << std::endl;
        return nullptr;
    }

    glViewport(0, 0, 800, 600);//处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600)

    return window;
}

int triangle_by_EBO()
{
    GLFWwindow* window = init_window();

    ///定义着色器
    //创建一个顶点着色器对象,注意还是用ID来引用的
    unsigned int vertexShader;
    vertexShader = glCreateShader(GL_VERTEX_SHADER);

    //着色器源码附加到着色器对象上
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL
    glCompileShader(vertexShader);//编译源码
    int  success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

    //创建一个片段着色器对象,注意还是用ID来引用的
    unsigned int fragmentShader;
    fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);//编译源码
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

    //创建一个着色器对程序
    unsigned int shaderProgram;
    shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);//把之前编译的着色器附加到程序对象上
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);//glLinkProgram链接它们
    glGetProgramiv(shaderProgram, GL_COMPILE_STATUS, &success);//用glGetProgramiv检查是否编译成功
    if (!success)
    {
        glGetShaderInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl;
    }

    //链接后即可删除
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);//*/

    CShader shader("hello_triangle.vs", "hello_triangle.fs");

    //定义顶点对象
    float vertices[] = {
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
    };

    unsigned int indices[] = {
        // 注意索引从0开始! 
        // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
        // 这样可以由下标代表顶点组合成矩形

        0, 1, 3, // 第一个三角形
        1, 2, 3  // 第二个三角形
    };

    //生成VAO对象,缓冲ID为VAO
    unsigned int VAO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);//绑定VAO,从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO,供之后使用

    //生成VBO对象,缓冲ID为VBO
    unsigned int VBO;
    glGenBuffers(1, &VBO);//第一个参数GLsizei是要生成的缓冲对象的数量,第二个GLuint是要输入用来存储缓冲对象名称的数组

    //生成EBO对象,缓冲ID为EBO
    unsigned int EBO;
    glGenBuffers(1, &EBO);

    //绑定到目标对象,VBO变成了一个顶点缓冲类型
    glBindBuffer(GL_ARRAY_BUFFER, VBO);//第一个就是缓冲对象的类型,第二个参数就是要绑定的缓冲对象的名称
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//数据传入缓冲内存中,GL_STATIC_DRAW:数据不会或几乎不会改变; GL_DYNAMIC_DRAW:数据会被改变很多; GL_DYNAMIC_DRAW:数据会被改变很多

    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);//不要在VAO处于活动状态时取消绑定EBO,因为绑定的元素缓冲区对象存储在VAO中;保持EBO绑定。

    //设置顶点属性指针,如何解析顶点数据
    /*
    第一个参数指定我们要配置的顶点属性,顶点着色器中使用layout(location = 0)定义
    第二个参数指定顶点属性的大小
    第三个参数指定数据的类型
    第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间
    第五个参数步长(Stride),它告诉我们在连续的顶点属性组之间的间隔
    最后一个参数的类型是void*,数据在缓冲中起始位置的偏移量(Offset)
    */
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);//启用顶点属性layout(location = 0),顶点属性默认是禁用的
    glBindBuffer(GL_ARRAY_BUFFER, 0);//设置完属性,解绑VBO

    glBindVertexArray(0);//配置完VBO及其属性,解绑VAO


    //绘制模式为线条GL_LINE,填充面GL_FILL
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//正反面

    while (!glfwWindowShouldClose(window))
    {
        processInput(window);


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


        ///绘制物体
        glUseProgram(shaderProgram);//激活程序对象

        glBindVertexArray(VAO);
        
        //glDrawArrays(GL_TRIANGLES, 0, 3);//使用VAO绘制:绘制图元为三角形,起始索引0,绘制顶点数量3
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//使用EBO绘制:绘制图元为三角形,绘制顶点数量6,索引类型,偏移量0

        //glBindVertexArray(0);//只绘制一个物体,不需要重复绑定和解绑VAO
        //glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);//不要在VAO处于活动状态时取消绑定EBO,因为绑定的元素缓冲区对象存储在VAO中;保持EBO绑定。

        glfwSwapBuffers(window);//交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲)
        glfwPollEvents();//检查有没有触发什么事件
    }

    //释放对象
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);//VAO解绑后,可以解绑EBO。
    glDeleteProgram(shaderProgram);

    std::cout << "finish!" << std::endl;
    glfwTerminate();//释放/删除之前的分配的所有资源
    return 0;
}

int main()
{
    triangle_by_EBO();

    return 0;
}

运行结果

 PS.为了能看出是绘制的两个三角形,这里使用的是绘制线框模式

    //绘制模式为线条GL_LINE,填充面GL_FILL
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//正反面