OpenGL

OpenGL 学习项目。

开发平台

  • Arch Linux x86_64
  • Kernel 5.16.10-zen1-1-zen
  • GLFW 3
  • GLEW 2.2

编译

mkdir build && \
    cd build && \
    cmake .. && \
    make

使用 GLFW 构建三角形

GlClear

glClear 函数将缓冲区清除为预设值,即重置缓冲区颜色。目标颜色由 glClearColor 确定。例如,在调用 glClear 之前调用了 glClearColor(1, 0, 0, 1),则缓冲区会被重置为红色。

GlBegin

glBegin & glEnd 函数之间包含若干基本的点。

glBegin 的函数参数表示如何呈现这些点。

while (!glfwWindowShouldClose(window)) {
    /* Render here */
    glClear(GL_COLOR_BUFFER_BIT);

    glBegin(GL_TRIANGLES);
    glVertex2f(-0.5f, -0.5f);
    glVertex2f( 0.0f,  0.5f);
    glVertex2f( 0.5f, -0.5f);
    glEnd();

    /* Swap front and back buffers */
    glfwSwapBuffers(window);

    /* Poll for and process events */
    glfwPollEvents();
}

GLEW

从 0 开始

Include

包含 <GL/glew.h> 文件时,#include <GL/glew.h> 必须写在其他有关 OpenGL 的头文件语句之前,否则会出现错误:

In file included from /home/divinerapier/code/c/github.com/divinerapier/opengl/glew-draw-triangles/src/main.cpp:2:
/usr/include/GL/glew.h:85:2: 错误:#error gl.h included before glew.h
   85 | #error gl.h included before glew.h
      |  ^~~~~

因为,在 <GL/glew.h> 文件中会进行判断:

#if defined(__gl_h_) || defined(__GL_H__) || defined(_GL_H) || defined(__X_GL_H)
#error gl.h included before glew.h
#endif

初始化

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

#include <iostream>

int main(void)
{
    GLFWwindow* window;

    if (glewInit() != GLEW_OK)
    {
        std::cout << "ohhhhhhhhh" << std::endl;
        return 1;
    }

    /* Initialize the library */
    if (!glfwInit())
        return -1;
}

但是很遗憾,初始化 glew 失败。

再回到文档,文档中说:

First you need to create a valid OpenGL rendering context and call glewInit() to initialize the extension entry points.

要求先要初始化一个有效的 OpenGL 上下文,然后再初始化 glew。所以,应该将代码移动到 glfwMakeContextCurrent(window); 函数调用之后:

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

#include <iostream>

int main(void)
{
    GLFWwindow* window;

    /* Initialize the library */
    if (!glfwInit())
        return -1;

    /* Create a windowed mode window and its OpenGL context */
    window = glfwCreateWindow(640, 480, "Hello World", NULL, NULL);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }

    /* Make the window's context current */
    glfwMakeContextCurrent(window);

    if (glewInit() != GLEW_OK)
    {
        std::cout << "ohhhhhhhhh" << std::endl;
        return 1;
    }

    // TODO: 正式工作
}

使用更多功能

GLEW 提供强大的运行时机制,根据平台判断哪些 OpenGL 扩展功能可以使用。其头文件中包含了大量的函数指针,以函数 glAttachShader 为例,使用 IDE 层层跳转:

#define glAttachShader GLEW_GET_FUN(__glewAttachShader)

#ifndef GLEW_GET_FUN
#define GLEW_GET_FUN(x) x
#endif

GLEW_FUN_EXPORT PFNGLATTACHSHADERPROC __glewAttachShader;

typedef void (GLAPIENTRY * PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader);

最终,glAttachShader 其实是一个函数指针 typedef void (GLAPIENTRY * PFNGLATTACHSHADERPROC) (GLuint program, GLuint shader); 最终的功能由显示设备提供支持。

GLEW 绘制三角形

使用 GLEW 绘制图形需要自定义 Shader 渲染图形。

数据

GLEW 正确初始化后,可以开始准备数据工作。

描述 2D 三角形一共需要 3 个点,每个点的坐标由 2 个 float 组成:

float positions[6] = {
    -0.5f, -0.5f,
     0.0f,  0.5f,
     0.5f, -0.5f
};

然后,将点的坐标数据并定到 OpenGL 的缓冲区中。

glGenBuffers 函数可以创建若干的命名缓冲区 (generate buffer object names),由于这里只需要显示一个三角形,所以第一个参数 n 传入 1,表示生成一个缓冲区,如果需要生成多个,则需要传入一个已分配好内存的数组。

一个缓冲区:

unsigned int buffer = 0;
// https://docs.gl/gl4/glGenBuffers
glGenBuffers(1, &buffer);

多个缓冲区:

unsigned int buffers[4] = { 0 };
glGenBuffers(sizeof(buffers) / sizeof(buffers[0]), buffers);

缓冲区的本质只是一段内存,而在 OpenGL 中更是仅仅使用一个 unsigned int 类型作为描述符(OpenGL 称之为名称),因此,需要将缓冲区与某种特定的类型绑定,GPU 才能由办法解释缓冲区数据(同一段内存,使用 intfloat 解释的结果是不同的)。

glBindBuffer 函数接收一种缓冲区目标与一个缓冲区名称。当缓冲区名称不存在时,会自动创建一个同名缓冲区。并且,同一时间最多只能有一个缓冲区被绑定到目标上 (When a buffer object is bound to a target, the previous binding for that target is automatically broken)

当成功绑定缓冲区的指定目标上之后,使用 glBufferData 函数用来创建并初始化缓冲区数据。

// https://docs.gl/gl4/glBindBuffer
glBindBuffer(GL_ARRAY_BUFFER, buffer);
// https://docs.gl/gl4/glBufferData
glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), (const void*)(&positions), GL_STATIC_DRAW);

最后,启用顶点:

  • index: 这个目前还不明白,只知道要与 glEnableVertexAttribArray 参数同时为 0
  • size: 每个顶点包含的元素数量,包括 position(位置), normal(法线), color(颜色), 和 texture coordinates(纹理坐标),可取值 (1,2,3,4)
  • type: 每个元素的数据类型,GL_FLOAT 表示以 float 为单位_
  • normalized: 是否为向量(只有方向无大小)
  • stride: 连续的顶点属性之间的字节偏移间隔
  • pointer: 数据偏移量
glEnableVertexAttribArray(0);

// https://docs.gl/gl4/glVertexAttribPointer
// 
// size: 每个顶点元素个数 (此处每个顶点包含 2 个 float)
// type: 每个元素的数据类型 (此处为 float)
// stride: 顶点位置之间的距离(字节数)
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

最后,完整代码如下:

    if (glewInit() != GLEW_OK) {
        std::cout << "ohhhhhhhhh" << std::endl;
        return 1;
    }

    float positions[6] = {
        -0.5f, -0.5f,
         0.0f,  0.5f,
         0.5f, -0.5f
    };


    unsigned int buffer = 0;
    // https://docs.gl/gl4/glGenBuffers
    glGenBuffers(1, &buffer);
    // https://docs.gl/gl4/glBindBuffer
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    // https://docs.gl/gl4/glBufferData
    glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), (const void*)(&positions), GL_STATIC_DRAW);
    // https://docs.gl/gl4/glEnableVertexAttribArray
    glEnableVertexAttribArray(0);


    // https://docs.gl/gl4/glVertexAttribPointer
    // 
    // size: 每个顶点元素个数 (此处每个顶点包含 2 个 float)
    // type: 每个元素的数据类型 (此处为 float)
    // stride: 顶点位置之间的距离(字节数)
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

Shader

图形渲染由 GPU 负责,因此,需要对 GPU 编程,指示 GPU 在哪里画点,如何显示颜色等。这就是 Shader 的作用。

Shader 的本质是一段运行在 GPU 上的程序,正如一般的 c/c++ 编写的程序告诉 CPU 如何工作一样,OpenGL 提供的 GLSL 语言编写的 Shader 程序告诉 GPU 如何工作(渲染)。

本次将使用两种着色器 VertexShader (顶点着色器)FragmentShader (片段着色器)

  • VertexShader 作用于每个顶点,通常是处理从世界空间到裁剪空间(屏幕坐标)的坐标转换,后接光栅化
  • FragmentShader 作用于每个屏幕上的片元(可近似理解为像素),通常是计算颜色。
创建 Shader

Shader 的本质是一个程序,也需要类似于源码,编译等流程。

声明两个字符串变量,分别代表 VertexShaderFragmentShader 的源代码:

std::string vertexShader =
    "#version 330 core\n"
    "\n"
    "layout(location = 0) in vec4 position;\n"
    "\n"
    "void main() {\n"
    "    gl_Position = position;\n"
    "}\n"
    "";
  1. layout(location = 0) 表示使用 glVertexAttribPointer 第一个参数 index=0 对应的数据。
  2. in vec4 position; 中使用 vec4 是因为后续的 gl_Positionvec4 类型,虽然实际每个顶点是 vec2(glVertexAttribPointer 的第二个参数 size=2)类型
  3. in vec4 position; 中的 position 表示每一个顶点的 position 属性部分
std::string fragmentShader =
    "#version 330 core\n"
    "\n"
    "layout(location = 0) out vec4 color;\n"
    "\n"
    "void main() {\n"
    "    color = vec4(0.0, 1.0, 0.0, 1.0);\n" // 0: 黑色 1: 白色 范围: 0-1 (类比 0-255) 顺序: rgba
    "}\n"
    "";

编译 Shader 源码:

static unsigned int ComplieShader(unsigned int type, const std::string& source) {
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str();
    // https://docs.gl/gl4/glShaderSource
    // void glShaderSource(GLuint shader, GLsizei count, const GLchar * *string, const GLint * length);
    // 
    // sets the source code in shader to the source code in the array of strings specified by string. 
    // 替换 Shader 中的代码
    // 
    // id: Shader 对象的描述符。
    // 
    // count: string 数组的长度
    // 
    // string: 代码数组,类型为 const char**,因此,可以传入多个 Shader 代码,数量为 count
    // 
    // length: 长度数组,类型为 const int*,数组中的每个元素表示对应 string 参数元素的字符串长度,NULL 表示字符串以 NULL 结束
    // If length is NULL, each string is assumed to be null terminated.
    // 
    // The source code strings are not scanned or parsed at this time; they are simply copied into the specified shader object.
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    int result = 0;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result);
    // 错误处理
    if (GL_FALSE == result) {
        int length = 0;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
        char* message = (char*)alloca(length * sizeof(char));
        glGetShaderInfoLog(id, length, &length, message);
        std::cout <<
            "Failed compile " <<
            ((type == GL_VERTEX_SHADER) ? "vertex" : "fragment") <<
            " shader: " << source <<
            " error: " << message <<
            std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

// CreateProgram 输入 Shader 源码,返回相应的 Shader 程序
static unsigned int CreateProgram(const std::string& vertexShader, const std::string& fragmentShader) {
    unsigned int program = glCreateProgram();
    unsigned int vs = ComplieShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = ComplieShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);
    glValidateProgram(program);
    glDeleteShader(vs);
    glDeleteShader(fs);
    return program;
}

最后,在 c/c++ 的源码中调用:

unsigned int program = CreateProgram(vertexShader, fragmentShader);
glUseProgram(program);

// ...

glDeleteProgram(program);
return 0;

暂不清楚

  1. Vertex 有哪些属性(Attributes)
  2. 函数 glVertexAttribPointer 的第一个参数需要开发者自行维护从 0 开始自增吗?
  3. glDeleteShaderglDetachShader 的区别,如何管理 Shader

相关链接