zl程序教程

您现在的位置是:首页 >  其它

当前栏目

OpenGL-02-创建三角形,矩形

创建 02 OpenGL 矩形 三角形
2023-09-11 14:22:30 时间

OpenGL-02-创建三角形/矩形

逐步解析

原文:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/#_3

本篇,主要对此文做个人总结以及思路简化

这里首先要讲解一些基本知识

对于OpenGL,我们需要至少设置一个顶点着色器和一个片段着色器。通过他们我们才能绘制三角形

此外,OpenGL着色器使用GLSL语言

这是一段十分基础的GLSL顶点着色器源代码

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

着色器源代码硬编码在C风格的字符串中,后续会用到

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

创建顶点着色器

通过glCreateShader(GL_VERTEX_SHADER);创建顶点着色器对象,输入参数为着色器类型,这里是顶点着色器

并通过 glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);语句将之前的GLSL源代码附加到着色器对象上,在编译着色器即可。

    // build and compile our shader program   创建和编码 着色器程序
    // ------------------------------------
    // vertex shader   顶点着色器
    //创建一个着色器对象
    //通过glCreateShader函数进行着色器创建,输入参数为着色器类型
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    //把这个着色器源码附加到着色器对象上,然后编译它
    //参数:把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量
    //这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    //编译着色器
    glCompileShader(vertexShader);
    // check for shader compile errors
    //检查着色器编译是否成功
    int success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

同理,片段着色器的创建也是一样的

//片段着色器源代码
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"
                                   "}\n\0";
    // fragment shader   片段着色器
    //通过glCreateShader函数进行着色器创建
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    //将片段着色器附加到着色器对象上,然后编译它
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    //编译着色器
    glCompileShader(fragmentShader);
    // check for shader compile errors
    //检查着色器是否编译成功
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

创建了两个着色器对象并编译他们后,我们还需要将他们链接到一个用来渲染的着色器程序上。

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。

通过unsigned int shaderProgram = glCreateProgram();创建着色器程序

并通过glAttachShader函数将之前的两个着色器附加到这个着色器程序上

再通过glLinkProgram函数链接这些着色器,即可。

最后再将两个着色器对象删除,因为已经链接到了着色器程序上,我们就不在需要他们。

    // link shaders     链接着色器
    //创建一个着色器程序
    //着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。
    //如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,
    //然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
    unsigned int shaderProgram = glCreateProgram();
    //将顶点着色器附加到着色器程序上
    glAttachShader(shaderProgram, vertexShader);
    //将片段着色器附加到着色器程序上
    glAttachShader(shaderProgram, fragmentShader);
    //使用glLinkProgram链接这些着色器
    glLinkProgram(shaderProgram);
    // check for linking errors
    //检查 链接着色器是否成功
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    // 将着色器连接到程序对象之后,删除着色器对象
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

至此,我们已经完成了着色器的创建,最后只通过以下代码需要激活着色器程序即可

        glUseProgram(shaderProgram);

到这里,虽然已经有了能够渲染的着色器程序。但是我们还没有东西给他去渲染

接下来我将解释,如果去渲染一个三角形或者矩形,先说三角形

绘制三角形

首先我们要指定三角形的顶点

    float vertices[] = {//三角形
            -0.5f, -0.5f, 0.0f, // left
            0.5f, -0.5f, 0.0f, // right
            0.0f,  0.5f, 0.0f  // top
    };

由于我们要绘制的是2D三角形,因此将z轴都设置为0。

有了顶点后,接下来我需要先介绍两个极其重要的概念

VBO(顶点缓冲对象)

顶点缓冲对象VBO是在显卡存储空间中开辟出的一块内存缓存区,用于存储顶点的各类属性信息,如顶点坐标,顶点法向量,顶点颜色数据等。在渲染时,可以直接从VBO中取出顶点的各类属性数据,由于VBO在显存而不是在内存中,不需要从CPU传输数据,处理效率更高。

所以可以理解为VBO就是显存中的一个存储区域,可以保持大量的顶点属性信息。并且可以开辟很多个VBO,每个VBO在OpenGL中有它的唯一标识ID,这个ID对应着具体的VBO的显存地址,通过这个ID可以对特定的VBO内的数据进行存取操作。

简单来说,VBO就是用来存储上面我们指定的顶点坐标

首先创建VBO

    unsigned int VBO;

然后使用glGenBuffers函数和一个缓冲ID生成一个VBO对象:

创建的VBO可用来保存不同类型的顶点数据,创建之后需要通过分配的ID绑定(bind)制定的VBO,

    glGenBuffers(1, &VBO);

对于同一类型的顶点数据一次只能绑定一个VBO。绑定操作通过glBindBuffer来实现,第一个参数指定绑定的数据类型,可以是GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER或者GL_PIXEL_UNPACK_BUFFER中的一个。

简单来说,下面这行代码就是指定VBO要绑定的缓存类型,上面这些就是缓存类型

    glBindBuffer(GL_ARRAY_BUFFER, VBO);

接下来调用glBufferData把用户定义的数据传输到当前绑定的显存缓冲区中。

glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。

第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:

  • GL_STATIC_DRAW :数据不会或几乎不会改变。
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

顶点数据传入GPU之后,还需要通知OpenGL如何解释这些顶点数据,这个工作由函数glVertexAttribPointer完成:

   glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
  • 第一个参数指定顶点属性位置值,与顶点着色器中layout(location=0)对应。
  • 第二个参数指定顶点属性大小,顶点属性是一个vec3,它由3个值组成,所以大小是3。
  • 第三个参数指定数据类型。
  • 第四个参数定义是否希望数据被标准化。
  • 第五个参数是步长(Stride),指定在连续的顶点属性之间的间隔,这里每个数据都有三个float,每组数据之前相差3个float的长度
  • 第六个参数表示我们的位置数据在缓冲区起始位置的偏移量。

顶点属性glVertexAttribPointer默认是关闭的,使用时要以顶点属性位置值为参数调用glEnableVertexAttribArray开启

    glEnableVertexAttribArray(0);

不过这样仍待不够,这里还需要引入第二个重要概念

VAO(顶点数组)

顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中

OpenGL的核心模式要求我们使用VAO,所以它知道该如何处理我们的顶点输入。如果我们绑定VAO失败,OpenGL会拒绝绘制任何东西。

VAO是一个保存了所有顶点数据属性的状态结合,它存储了顶点数据的格式以及顶点数据所需的VBO对象的引用。

VAO本身并没有存储顶点的相关属性数据,这些信息是存储在VBO中的,VAO相当于是对很多个VBO的引用,把一些VBO组合在一起作为一个对象统一管理。

接下来我们创建VAO对象

    unsigned int VAO;

创建方式和创建VBO很相似,都是通过缓存ID进行创建

    glGenVertexArrays(1, &VAO);

要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。

    glBindVertexArray(VAO);

至此,对于VAO与VBO的创建就完成了

他们的完整代码如下,注释里,理解不了的后面会讲,主要看代码

    //创建VBO(顶点缓冲对象),VAO(顶点数组对象),EBO(索引缓冲对象)
    unsigned int VBO;
    unsigned int VAO;
//    unsigned int EBO;
    //通过缓冲ID生成一个VAP对象
    glGenVertexArrays(1, &VAO);
    //使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
    glGenBuffers(1, &VBO);
    //同样,通过glGenBuffers函数和缓冲ID生成EBO对象
//    glGenBuffers(1, &EBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    //使用glBindVertexArray绑定VAO
    glBindVertexArray(VAO);
    //顶点缓冲对象的缓冲类型是:GL_ARRAY_BUFFER
    //OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型
    //我们使用glBindBuffer将新创建的缓冲,绑定到GL_ARRAY_BUFFER上
    //从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
    //第四个参数指定我们希望显卡如何管理给定的数据
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    //绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER
//    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    //通过glBufferData将索引复制到缓冲里
//    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    //通过glVertexAttribPointer告诉OpenGL如何解析顶点数据
    //第一个参数为我们要配置的顶点属性,
    // 还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?
    // 它可以把顶点属性的位置值设置为0。
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    //我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;
    //顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,
    // 建立了一个顶点和一个片段着色器,
    // 并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,
    //启用顶点属性
    glEnableVertexAttribArray(0);

OpenGL中所有的图形都是通过分解成三角形的方式进行绘制,glDrawArrays函数负责把模型绘制出来,它使用当前激活的着色器,当前VAO对象中的VBO顶点数据和属性配置来绘制出来基本图形。

glDrawArrays (GLenum mode, GLint first, GLsizei count)

  • 第一个参数表示绘制的类型,有三种取值:

    • 1.GL_TRIANGLES:每三个顶之间绘制三角形,之间不连接;
    • 2.GL_TRIANGLE_FAN:以V0V1V2,V0V2V3,V0V3V4,……的形式绘制三角形;
    • 3.GL_TRIANGLE_STRIP:顺序在每三个顶点之间均绘制三角形。这个方法可以保证从相同的方向上所有三角形均被绘制。以V0V1V2,V1V2V3,V2V3V4……的形式绘制三角形;
  • 第二个参数定义从缓存中的哪一位开始绘制,一般定义为0;

  • 第三个参数定义绘制的顶点数量;

在渲染指令中加上这行代码:

        glDrawArrays(GL_TRIANGLES, 0, 3);

这样就能完成三角形的绘制

三角形完整注释代码

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
//屏幕大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

#pragma region 着色器源码
//顶点着色器源代码
const char *vertexShaderSource = "#version 330 core\n"
                                 "layout (location = 0) in vec3 aPos;\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"
                                   "}\n\0";

#pragma endregion

int main()
{

#pragma region 窗口初始化与配置
    // glfw: initialize and configure   初始化与配置
    // ------------------------------
    //glfw初始化
    glfwInit();
    //glfw配置
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

#pragma endregion

    // glfw window creation    创建窗口
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    //回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers   通过glad管理所有的OpenGL函数指针
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

#pragma region shader
    // build and compile our shader program   创建和编码 着色器程序
    // ------------------------------------
    // vertex shader   顶点着色器
    //创建一个着色器对象
    //通过glCreateShader函数进行着色器创建,输入参数为着色器类型
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    //把这个着色器源码附加到着色器对象上,然后编译它
    //参数:把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量
    //这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    //编译着色器
    glCompileShader(vertexShader);
    // check for shader compile errors
    //检查着色器编译是否成功
    int success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }


    // fragment shader   片段着色器
    //通过glCreateShader函数进行着色器创建
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    //将片段着色器附加到着色器对象上,然后编译它
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    //编译着色器
    glCompileShader(fragmentShader);
    // check for shader compile errors
    //检查着色器是否编译成功
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }


    // link shaders     链接着色器
    //创建一个着色器程序
    //着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。
    //如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,
    //然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
    unsigned int shaderProgram = glCreateProgram();
    //将顶点着色器附加到着色器程序上
    glAttachShader(shaderProgram, vertexShader);
    //将片段着色器附加到着色器程序上
    glAttachShader(shaderProgram, fragmentShader);
    //使用glLinkProgram链接这些着色器
    glLinkProgram(shaderProgram);
    // check for linking errors
    //检查 链接着色器是否成功
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    // 将着色器连接到程序对象之后,删除着色器对象
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    glUseProgram(shaderProgram);

#pragma endregion

    // set up vertex data (and buffer(s)) and configure vertex attributes
    //顶点输入
    // ------------------------------------------------------------------
    float vertices[] = {//三角形
            -0.5f, -0.5f, 0.0f, // left
            0.5f, -0.5f, 0.0f, // right
            0.0f,  0.5f, 0.0f  // top
    };
//为了绘制矩形,我们可以通过绘制两个三角形,来绘制举行
//但是会造成产生两个顶点的额外开销,当这个数量变大后,会造成大量的资源浪费
//    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个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。

//    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, 3, // 第一个三角形
//            1, 2, 3  // 第二个三角形
//    };


    //创建VBO(顶点缓冲对象),VAO(顶点数组对象),EBO(索引缓冲对象)
    unsigned int VBO;
    unsigned int VAO;
//    unsigned int EBO;
    //通过缓冲ID生成一个VAP对象
    glGenVertexArrays(1, &VAO);
    //使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
    glGenBuffers(1, &VBO);
    //同样,通过glGenBuffers函数和缓冲ID生成EBO对象
//    glGenBuffers(1, &EBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    //使用glBindVertexArray绑定VAO
    glBindVertexArray(VAO);
    //顶点缓冲对象的缓冲类型是:GL_ARRAY_BUFFER
    //OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型
    //我们使用glBindBuffer将新创建的缓冲,绑定到GL_ARRAY_BUFFER上
    //从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
    //第四个参数指定我们希望显卡如何管理给定的数据
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    //绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER
//    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    //通过glBufferData将索引复制到缓冲里
//    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    //通过glVertexAttribPointer告诉OpenGL如何解析顶点数据
    //第一个参数为我们要配置的顶点属性,
    // 还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?
    // 它可以把顶点属性的位置值设置为0。
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    //我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;
    //顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,
    // 建立了一个顶点和一个片段着色器,
    // 并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,

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

    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
//    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
    // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
//    glBindVertexArray(0);


    // uncomment this call to draw in wireframe polygons.
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    //OpenGL线框模式
//    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        //清空并填充背景颜色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // draw our first triangle
        //激活着色器程序

        //绑定顶点数组对象
//        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
        //它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。
        glDrawArrays(GL_TRIANGLES, 0, 3);
//        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        // glBindVertexArray(0); // no need to unbind it every time

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    //
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
//    glDeleteBuffers(1, &EBO);
    glDeleteProgram(shaderProgram);

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}
/*
int main()
{
#pragma region windows
    glfwInit();
    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);//MacOS系统

    //参数分别为:长,宽,窗口标题,后面两个参数暂时忽略
    GLFWwindow* window = glfwCreateWindow(800, 600, "MyFirstWindow", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    glViewport(0, 0, 800, 600);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

//    while(!glfwWindowShouldClose(window))
//    {
//        glfwSwapBuffers(window);
//        glfwPollEvents();
//    }
    while (!glfwWindowShouldClose(window))
    {
        processInput(window);//通过esc关闭窗口

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
#pragma endregion
    return 0;
}
 */

绘制矩形

绘制矩形,矩形其实就是两个三角形组成的,因此我们重新制定顶点

    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个顶点。

因此,这里我们需要引入一个重要概念

EBO(索引缓冲对象)

索引缓冲对象EBO相当于OpenGL中的顶点数组的概念,是为了解决同一个顶点多洗重复调用的问题,可以减少内存空间浪费,提高执行效率。当需要使用重复的顶点时,通过顶点的位置索引来调用顶点,而不是对重复的顶点信息重复记录,重复调用。

EBO中存储的内容就是顶点位置的索引indices,EBO跟VBO类似,也是在显存中的一块内存缓冲器,只不过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, 3, // 第一个三角形
            1, 2, 3  // 第二个三角形
    };

既然指定了索引,那么我们就需要有东西来存储它,这个东西就是EBO

接下来,我们创建EBO

    unsigned int EBO;

同样,通过glGenBuffers函数和缓冲ID生成EBO对象

glGenBuffers(1, &EBO);

绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

通过glBufferData将索引复制到缓冲里

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

此时,我们绘制的方式不是直接通过访问VBO顶点来绘制了,这时候我们是通过EBO绑定顶点索引的方式绘制模型,需要使用glDrawElements而不是glDrawArrays

        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。

第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。

第三个参数是索引的类型,这里是GL_UNSIGNED_INT。

最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。

这里放上加了EBO创建的,包括VBO,VAO的完整注释代码

    //创建VBO(顶点缓冲对象),VAO(顶点数组对象),EBO(索引缓冲对象)
    unsigned int VBO;
    unsigned int VAO;
    unsigned int EBO;
    //通过缓冲ID生成一个VAP对象
    glGenVertexArrays(1, &VAO);
    //使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
    glGenBuffers(1, &VBO);
    //同样,通过glGenBuffers函数和缓冲ID生成EBO对象
    glGenBuffers(1, &EBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    //使用glBindVertexArray绑定VAO
    glBindVertexArray(VAO);
    //顶点缓冲对象的缓冲类型是:GL_ARRAY_BUFFER
    //OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型
    //我们使用glBindBuffer将新创建的缓冲,绑定到GL_ARRAY_BUFFER上
    //从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
    //第四个参数指定我们希望显卡如何管理给定的数据
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    //绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    //通过glBufferData将索引复制到缓冲里
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    //通过glVertexAttribPointer告诉OpenGL如何解析顶点数据
    //第一个参数为我们要配置的顶点属性,
    // 还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?
    // 它可以把顶点属性的位置值设置为0。
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    //我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;
    //顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,
    // 建立了一个顶点和一个片段着色器,
    // 并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,

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

在渲染指定中加上代码:

        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

这样我们就能完成矩形的绘制

矩形绘制完整注释代码

#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include <iostream>

void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);

// settings
//屏幕大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

#pragma region 着色器源码
//顶点着色器源代码
const char *vertexShaderSource = "#version 330 core\n"
                                 "layout (location = 0) in vec3 aPos;\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"
                                   "}\n\0";

#pragma endregion

int main()
{

#pragma region 窗口初始化与配置
    // glfw: initialize and configure   初始化与配置
    // ------------------------------
    //glfw初始化
    glfwInit();
    //glfw配置
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

#pragma endregion

    // glfw window creation    创建窗口
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    //回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // glad: load all OpenGL function pointers   通过glad管理所有的OpenGL函数指针
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

#pragma region shader
    // build and compile our shader program   创建和编码 着色器程序
    // ------------------------------------
    // vertex shader   顶点着色器
    //创建一个着色器对象
    //通过glCreateShader函数进行着色器创建,输入参数为着色器类型
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    //把这个着色器源码附加到着色器对象上,然后编译它
    //参数:把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量
    //这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL。
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    //编译着色器
    glCompileShader(vertexShader);
    // check for shader compile errors
    //检查着色器编译是否成功
    int success;
    char infoLog[512];
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }


    // fragment shader   片段着色器
    //通过glCreateShader函数进行着色器创建
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    //将片段着色器附加到着色器对象上,然后编译它
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    //编译着色器
    glCompileShader(fragmentShader);
    // check for shader compile errors
    //检查着色器是否编译成功
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }


    // link shaders     链接着色器
    //创建一个着色器程序
    //着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。
    //如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,
    //然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。
    unsigned int shaderProgram = glCreateProgram();
    //将顶点着色器附加到着色器程序上
    glAttachShader(shaderProgram, vertexShader);
    //将片段着色器附加到着色器程序上
    glAttachShader(shaderProgram, fragmentShader);
    //使用glLinkProgram链接这些着色器
    glLinkProgram(shaderProgram);
    // check for linking errors
    //检查 链接着色器是否成功
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    // 将着色器连接到程序对象之后,删除着色器对象
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    glUseProgram(shaderProgram);

#pragma endregion

    // set up vertex data (and buffer(s)) and configure vertex attributes
    //顶点输入
    // ------------------------------------------------------------------
//    float vertices[] = {//三角形
//            -0.5f, -0.5f, 0.0f, // left
//            0.5f, -0.5f, 0.0f, // right
//            0.0f,  0.5f, 0.0f  // top
//    };
//为了绘制矩形,我们可以通过绘制两个三角形,来绘制举行
//但是会造成产生两个顶点的额外开销,当这个数量变大后,会造成大量的资源浪费
//    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个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。

    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, 3, // 第一个三角形
            1, 2, 3  // 第二个三角形
    };


    //创建VBO(顶点缓冲对象),VAO(顶点数组对象),EBO(索引缓冲对象)
    unsigned int VBO;
    unsigned int VAO;
    unsigned int EBO;
    //通过缓冲ID生成一个VAP对象
    glGenVertexArrays(1, &VAO);
    //使用glGenBuffers函数和一个缓冲ID生成一个VBO对象
    glGenBuffers(1, &VBO);
    //同样,通过glGenBuffers函数和缓冲ID生成EBO对象
    glGenBuffers(1, &EBO);
    // bind the Vertex Array Object first, then bind and set vertex buffer(s), and then configure vertex attributes(s).
    //使用glBindVertexArray绑定VAO
    glBindVertexArray(VAO);
    //顶点缓冲对象的缓冲类型是:GL_ARRAY_BUFFER
    //OpenGL允许我们同时绑定多个缓冲,只要他们是不同的缓冲类型
    //我们使用glBindBuffer将新创建的缓冲,绑定到GL_ARRAY_BUFFER上
    //从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    //我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:
    //第四个参数指定我们希望显卡如何管理给定的数据
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    //绑定索引缓冲对象,缓冲对象类型为:GL_ELEMENT_ARRAY_BUFFER
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    //通过glBufferData将索引复制到缓冲里
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
    //通过glVertexAttribPointer告诉OpenGL如何解析顶点数据
    //第一个参数为我们要配置的顶点属性,
    // 还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗?
    // 它可以把顶点属性的位置值设置为0。
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    //我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;
    //顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,
    // 建立了一个顶点和一个片段着色器,
    // 并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,

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

    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
//    glBindBuffer(GL_ARRAY_BUFFER, 0);

    // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
    // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
//    glBindVertexArray(0);


    // uncomment this call to draw in wireframe polygons.
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    //OpenGL线框模式
//    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        //清空并填充背景颜色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // draw our first triangle
        //激活着色器程序

        //绑定顶点数组对象
//        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
        //它使用当前激活的着色器,之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。
//        glDrawArrays(GL_TRIANGLES, 0, 3);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
        // glBindVertexArray(0); // no need to unbind it every time

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // optional: de-allocate all resources once they've outlived their purpose:
    // ------------------------------------------------------------------------
    //
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
//    glDeleteBuffers(1, &EBO);
    glDeleteProgram(shaderProgram);

    // glfw: terminate, clearing all previously allocated GLFW resources.
    // ------------------------------------------------------------------
    glfwTerminate();
    return 0;
}

// process all input: query GLFW whether relevant keys are pressed/released this frame and react accordingly
// ---------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow *window)
{
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}

// glfw: whenever the window size changed (by OS or user resize) this callback function executes
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
    // make sure the viewport matches the new window dimensions; note that width and
    // height will be significantly larger than specified on retina displays.
    glViewport(0, 0, width, height);
}
/*
int main()
{
#pragma region windows
    glfwInit();
    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);//MacOS系统

    //参数分别为:长,宽,窗口标题,后面两个参数暂时忽略
    GLFWwindow* window = glfwCreateWindow(800, 600, "MyFirstWindow", NULL, NULL);
    if (window == NULL)
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);

    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    glViewport(0, 0, 800, 600);
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

//    while(!glfwWindowShouldClose(window))
//    {
//        glfwSwapBuffers(window);
//        glfwPollEvents();
//    }
    while (!glfwWindowShouldClose(window))
    {
        processInput(window);//通过esc关闭窗口

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    glfwTerminate();
#pragma endregion
    return 0;
}
 */

线框模式(Wireframe Mode)

要想用线框模式绘制你的三角形,你可以通过glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)函数配置OpenGL如何绘制图元。第一个参数表示我们打算将其应用到所有的三角形的正面和背面,第二个参数告诉我们用线来绘制。之后的绘制调用会一直以线框模式绘制三角形,直到我们用glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)将其设置回默认模式。