OpenGL入门笔记

文中代码和图片都是抄自LearnOpenGL网站和中文版网站。对网站上讲解不清楚的地方,大家可以参考本文中我的理解。 本文设计内容包括以下几点:

  • 1.什么是OpenGL
  • 2.初始动作,GLFW和GLEW
  • 3.OpenGL渲染管线
  • 4.顶点缓冲对象(VBO)、顶点数组对象(VAO)、索引缓冲对象(EBO)

  • OpenGL入门笔记
  • 什么是OpenGL
  • 初始动作GLFW和GLEW
  • OpenGL渲染管线
  • 渲染管线着色器介绍
  • 定义顶点数据
  • 定义着色器
  • 着色管线装配
  • 顶点缓冲对象VBO顶点数组对象VAO索引缓冲对象EBO


1.什么是OpenGL

http://bullteacher.com/2-opengl.html网站上有详细介绍,下面内容抄自该网站:OpeGL是Khronos Group开发维护的一个规范,规范定义了每个函数应该如何执行以及返回什么结果。OpenGL库实际的开发者通常是显卡厂商。

通过这个网站介绍,我们可以看到OpenGL与计算机视觉常用的库OpenCV截然不同。不要把这两个再搞混了。


先上一段OpenGL中画彩色三角形的代码,别怕,我们会在下面进行简单介绍,代码来源: https://learnopengl.com/code_viewer.php?code=getting-started/shaders-interpolated

#include <iostream>

// GLEW
#define GLEW_STATIC
#include <GL/glew.h>

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


// Function prototypes
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);

// Window dimensions
const GLuint WIDTH = 800, HEIGHT = 600;

// Shaders
const GLchar* vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 position;\n"
    "layout (location = 1) in vec3 color;\n"
    "out vec3 ourColor;\n"
    "void main()\n"
    "{\n"
    "gl_Position = vec4(position, 1.0);\n"
    "ourColor = color;\n"
    "}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
    "in vec3 ourColor;\n"
    "out vec4 color;\n"
    "void main()\n"
    "{\n"
    "color = vec4(ourColor, 1.0f);\n"
    "}\n\0";

// The MAIN function, from here we start the application and run the game loop
int main()
{
    // Init GLFW
    glfwInit();
    // Set all the required options for GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

    // Create a GLFWwindow object that we can use for GLFW's functions
    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
    glfwMakeContextCurrent(window);

    // Set the required callback functions
    glfwSetKeyCallback(window, key_callback);

    // Set this to true so GLEW knows to use a modern approach to retrieving function pointers and extensions
    glewExperimental = GL_TRUE;
    // Initialize GLEW to setup the OpenGL Function pointers
    glewInit();

    // Define the viewport dimensions
    glViewport(0, 0, WIDTH, HEIGHT);


    // Build and compile our shader program
    // Vertex shader
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // Check for compile time errors
    GLint success;
    GLchar 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
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // Check for compile time 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
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    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);


    // Set up vertex data (and buffer(s)) and attribute pointers
    GLfloat vertices[] = {
        // Positions         // Colors
         0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // Bottom Right
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // Bottom Left
         0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f   // Top 
    };
    GLuint VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // Bind the Vertex Array Object first, then bind and set vertex buffer(s) and attribute pointer(s).
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // Position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);
    // Color attribute
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);

    glBindVertexArray(0); // Unbind VAO


    // Game loop
    while (!glfwWindowShouldClose(window))
    {
        // Check if any events have been activiated (key pressed, mouse moved etc.) and call corresponding response functions
        glfwPollEvents();

        // Render
        // Clear the colorbuffer
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // Draw the triangle
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glBindVertexArray(0);

        // Swap the screen buffers
        glfwSwapBuffers(window);
    }
    // Properly de-allocate all resources once they've outlived their purpose
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    // Terminate GLFW, clearing any resources allocated by GLFW.
    glfwTerminate();
    return 0;
}

// Is called whenever a key is pressed/released via GLFW
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);
}

2.初始动作,GLFW和GLEW

代码中引入了两个库,GLFW和GLEW,在main函数中,也首先使用了这两个库中的函数。这两个库都是辅助编写OpenGL程序的库,除了名称相似外,功能上其实并不相同。

GLFW是一个方便创建OpenGL窗口的库(红宝书中使用的是glut)。比如定义窗口大小、处理用户输入等功能。

GLEW(OpenGL Extension Wrangler)的作用是可以简化获取函数地址的过程,并且包含跨平台使用OpenGL高级函数的方法。即在编程中简化函数的使用。

3.OpenGL渲染管线

渲染管线、着色器介绍

使用OpenGL绘制一个三角形,我们需要确定的是三角形三个点的顶点坐标。通过顶点坐标来绘制三角形。

我们可以将渲染管线看作是工厂的加工流水线,而顶点坐标看作是加工原材料。原材料经过工厂流水线,加工成成品;顶点经过渲染管线形成三角形图像显示在屏幕上。

我们这个OpenGL中的加工流水线也分为多个部分,每个部分进行着不同的加工工序,就像加工手机流水线中有的装芯片有的装摄像头等等。

以下是渲染管线中的各个部分:

图来自:http://bullteacher.com/5-hello-triangle.html

图中是渲染时的各个部分,细心的同学发现了蓝色方框的部分,作者DjangoX大神翻译叫做着色器,英文名字叫shader。这部分比较特别,虽然叫着色器,但其实功能并不局限于“着色”或“上色”。着色器其实就是图中渲染管线蓝色阶段处理顶点属性(位置、颜色等)的小程序,这些小程序跑在GPU上。

OpenGL功能强大但麻烦的地方就在于,顶点和着色器都是可以用户编辑定义的(原材料和流水线都是可以自己提供的)。我们既要给出数据(定义顶点属性),又要说明数据怎么放的(着色管线装配),该怎么处理(着色器中处理)。 我们既要提供原材料,又要自建流水线,还要告诉流水线工人如何装配原材料。

好吧,关于自定义着色器其实还是有限制的:必须定义顶点着色器和像素着色器(红宝书中翻译为片元着色器),另外的可以使用默认的。

当我们把着色器定义好,把各个分散的着色器链接在一起,形成一条可以完整衔接的“流水线”时,这条“流水线”大程序被称为着色器程序(只比着色器多了两个字,但是不一样哦)。

说了这么多,那该如何定义顶点数据和着色器呢?

下面我们看看代码怎么写

定义顶点数据

上面代码中,定义了顶点属性的数组:

GLfloat vertices[] = {
        // Positions         // Colors
         0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,  // Bottom Right
        -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,  // Bottom Left
         0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f   // Top 
    };

数组中,每行表示一个顶点属性,前三个浮点数表示顶点三维坐标,后三个浮点数表示顶点的颜色属性(RGB)。

定义着色器

代码中定义了着色器:

// Shaders
const GLchar* vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 position;\n"
    "layout (location = 1) in vec3 color;\n"
    "out vec3 ourColor;\n"
    "void main()\n"
    "{\n"
    "gl_Position = vec4(position, 1.0);\n"
    "ourColor = color;\n"
    "}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
    "in vec3 ourColor;\n"
    "out vec4 color;\n"
    "void main()\n"
    "{\n"
    "color = vec4(ourColor, 1.0f);\n"
    "}\n\0";

代码中写的可是字符串啊?没错,我们可以使用字符串来定义着色器。之后会对着色器的这段字符串使用编译函数后才能使用。写着色器这么长串字我们需要使用一种特别的语言来写:GLSL(OpenGL Shading Language),非常类似C++语言。

那为什么写了两个字符串,其实这两个字符串是两个着色器,第一个用作顶点着色器,第二个用作像素着色器。

看一下顶点着色器字符里的代码,解释一下着色器里代码的含义:

#version 330 core 
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;
out vec3 ourColor;
void main()
{
gl_Position = vec4(position, 1.0);
ourColor = color;
}

#version 330 core 其中330表示使用OpenGL 3.3;core表示使用OpenGL的核心模式(core-profile)。

layout (location = 0) in vec3 position; 其中 in 表示输入变量,即从外面要接受的变量。顶点数组就是要接收的变量。layout (location = 0) 是布局限定符,这有什么用呢?上面的顶点数组中表示了顶点的坐标和颜色,我们决定分开来接受这些数据,先接受坐标的数据(location = 0),再接收颜色的数据(location = 1)。

out 意思与 in 正好相反,表示要输出的变量。输出到哪里呢?按照渲染管线图中的箭头方向输出,从顶点着色器将会传到像素着色器。从像素着色器再传给下一个部分,最后显示出来。

main函数是着色器的入口,main中对顶点数据进行处理。

来定义和编译着色器吧:

// Vertex shader
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);//编译着色器
着色管线装配

接下来,把数据传入着色器就可以了吧。NO、NO,这段傻乎乎的着色器并不知道我们的顶点数组中的浮点数哪些是表示坐标,哪些表示颜色呢。用下面代码告诉他们:

// Position attribute
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);
    // Color attribute
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));
    glEnableVertexAttribArray(1);

我就直接抄写DjangoX大神文章内容进行解释:

  • 第一个参数指定我们要配置哪一个顶点属性。记住,我们在顶点着色器中使用layout(location = 0)定义了顶点属性——位置(position)的location。这样要把顶点属性的location设置为0,因为我们希望把数据传递到这个顶点属性中,所以我们在这里填0。
  • 下一个参数指定顶点属性的大小。顶点属性是vec3类型,它由3个数值组成。 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*是由浮点数组成的)。
  • 下个参数定义我们是否希望数据被标准化。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。
  • 第五个参数叫做步长(stride),它告诉我们在连续的顶点属性之间间隔有多少。由于下个位置数据在6个GLfloat后面的位置,我们把步长设置为6 * sizeof(GLfloat)。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。
  • 最后一个参数有古怪的GLvoid*的强制类型转换。它我们的位置数据在缓冲中起始位置的偏移量。由于位置数据是数组的开始,所以这里是0。

经过解释,着色器就知道该怎么拿数据了。

最后,把上面的两个着色器链接成一个完整的着色器程序,就可以使用啦。

// Link shaders
    GLuint shaderProgram = glCreateProgram();//创建着色器程序
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

看来功能强大的OpenGL着色器的使用的确很麻烦,不过编写流程我们都清楚了。但是,现在还没有结束呢。顶点数据并不能直接传入着色器中,要真正用起来,还需要下面这些知识。

4.顶点缓冲对象(VBO)、顶点数组对象(VAO)、索引缓冲对象(EBO)

  • 1.顶点缓冲对象(VBO)

OpenGL中着色器要获取顶点的数据,必须将数据放入一块特殊的内存中读取,这块特殊的内存区域就是缓冲。而题目中的这些对象就是缓冲中的对象,将数据放入这些对象中就可以成功得进行读取。

首先看顶点缓冲对象(vertex buffer objects, VBO),从字面理解,它用来存储顶点属性的缓冲对象。

创建一个顶点缓冲对象:

GLuint VBO;
glGenBuffers(1, &VBO);

将VBO绑定到GL_ARRAY_BUFFER上,然后将数据放入新建的VBO中:

glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

接下来,我们绘制图形的步骤如下:

// 0. 复制顶点数组到缓冲中提供给OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid * )0);
glEnableVertexAttribArray(0);

// 2. 当我们打算渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);

// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
  • 2.顶点数组对象(VAO)

下面来看顶点数组对象VAO,其实顶点数组对象(VAO)就是为了方便操作VBO而存在滴。

上三个对象的关系图:

图来自:http://bullteacher.com/5-hello-triangle.html

从名字可以看出顶点数组对象就像数组一样,将顶点属性数据放在一起。 如果我们有很多的顶点,而且这些顶点数据有的有一个属性,有的有两个,有的数据前面的数表示坐标,有的数据前面的数表示颜色。这样我们对每一个数据组都要新建VBO,然后设置顶点属性指针。

每次都这样做会很麻烦,为了方便,需要使用VAO,新建VAO后,VAO之后新建的VBO属性解读方式就会与VAO绑定在一起。相当于给这个VBO贴了一个标签,以后我们再使用这种方式解读属性,直接绑定VAO就可以了。

  • 3.索引缓冲对象(EBO) 索引缓冲对象(EBO)当然是跟索引有关系,那是对谁的索引呢?其实就是VBO中数据的索引。通过索引,我们可以更方便地对每个顶点进行方位。

比如说我们要绘制两个三角形:

GLfloat 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   // 左上角
};

那使用EBO呢?如下,因为三角形有一条共同边,这样可以少存了一个顶点的数据:

GLfloat 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   // 左上角
};

GLuint indices[] = { // 起始于0!

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