ChernoOpenGL_Tutorial(1):1~12

前言

OpenGL文档:https://docs.gl/
GLFW官网:https://www.glfw.org/
GLEW官网:http://glew.sourceforge.net/
khronos官网:https://www.khronos.org/
khronos OpenGL官网:https://www.khronos.org/opengl/
khronos OpenGL wiki:https://www.khronos.org/opengl/wiki

1. Welcome to OpenGL

OpenGL是一个图形API,允许我们访问显卡。

OpenGL的核心本身是一种规范,类似于C++规范。实际上它并没有确定任何的代码或类似的事情,它只是一个告诉你可以利用这些 API 做什么的规范,而不提供任何的实现。这意味着它本身肯定不是一个库,因为它本身是没有任何代码的。

因此OpenGL本身只是一个规范。而真正实现那些OpenGL函数的是你的显卡厂商。因此如果你使用NVIDIA的GPU,那么你的显卡驱动程序就会包含OpenGL的实现。并且所有的显卡厂商,比如AMD、Intel等,他们会有他们自己的实现。所以每个厂商对OpenGL的实现都会有轻微的不同。这就是为什么在有些情况下,有些代码在基于NVIDIA显卡的驱动程序上能工作,但在AMD电视或者其他的显卡上显得有一些不同,甚至产生bug。

OpenGL是跨平台的。所以你可以只写一份OpenGL代码就可以在Windows、Mac、Linux、iOS、安卓系统上运行。

事实上为平台专门编写的API,像是 Windows 的 Direct3D,一般比跨平台的 API 好。

记住,实际编写这些代码的人不是微软,微软的确和显卡厂商合作过要出更好的代码,但是到头来由于编写API的人职责不均衡,微软的愿望未能达成。所以关于“API哪家强”的争论是没有意义的,平台原生的东西往往更好一些。

Cherno 最喜欢的图形API设计是Direct3d11. 对于是那些在写游戏引擎或者渲染引擎的人们来说,可能更倾向于写一个基于OpenGL的Direct3D的包装器。所以换而言之,他们可能底层用的OpenGL,但是将实际的API接口写的很像DIrect3D。因为讲真Direct3D11的确是创造图形API的好办法。

在这个系列将学习现代OpenGL。

之所以有老的OpenGL和新的OpenGL的区别的原因是,OpenGL在90年代发布,那时的情况和现在完全不同。老的和现代的最大的区别是shader。(可编程渲染管线)

我们打算使用 C++ 编写OpenGL程序,因为 Cherno 认为 C++ 是最适合这种工作的语言。

2. Setting up OpenGL and Creating a Window in C++

在这个系列教程中,希望能写出在Windows、Mac、Linux这三个平台中都可以运行的代码,这意味着我们需要一些特定的方法来设置并打开窗口,并且特定于平台。换句话来说,就是在Windows创建窗口要使用Win32 API,因为你创建窗口的方式是特定于你使用的操作系统,这是操作系统级别的东西。

如果Cherno在写一个游戏引擎的话会这样做:花时间自己用现有的系统API为每个平台设置一个窗口。但是这毕竟是OpenGL教程,因此关注点不在于如何设置窗口,而是仅仅写OpenGL代码。

正因如此希望使用一个能提供基本实现这些的库,而且这个库要做的就是为我们提供合适的平台。换句话来说它将为我提供一个可以在 Windows Mac 和 Linux 下实现创建窗口并管理的代码,以便于每个人都能遵循规范,而且有很多库允许我们去做这些。

这里使用的为 GLFW 库。这是一个非常轻量级的库,它将为我们做的仅仅是创建一个窗口,创建一个OpenGL环境,或者允许我们使用一些例如 Input 一样基础的东西。这就是它要做的,不像SDL(Simple DirectMedia Layer),它实际上像是一个渲染器、一个框架。我不需要任何东西,而是需要我们去写我们自己的东西。我要做的就是将第一步自动化:想创建一个窗口并且不需要为每个平台编写不同的代码。

TheChernoCppTutorial 49节 曾讲过:

对于Windows的这些二进制文件,是拿32位的还是64位的呢?这与你实际的操作系统没有任何关系,而是和你的目标应用程序相关。
如果你正在编译的应用程序是win32程序(x86),那么就要32位的二进制文件,当然你的操作系统却很可能是win10 64位。

这里我们选择 32 位 glfw (https://www.glfw.org/download.html):
在这里插入图片描述

在新建工程的 solution directory 下建立一个 dependencies 的文件夹:
在这里插入图片描述

配置好glfw库,如下:
在这里插入图片描述
把官方文档贴进去,可以看到我们使用了一个 OpenGL 函数,但是没有链接到 OpenGL:
在这里插入图片描述
因此 ctrl + f7 编译没问题,但是 build 即加上链接步骤就会报错。

Application.obj : error LNK2019: unresolved external symbol __imp__glClear@4 referenced in function _main
......

在这里插入图片描述
因此这里我们先填上 opengl32.lib ,可以看到上面那个报错没有了,但是仍然有其他的报错。事实上,第一次解决的符号问题只是一个设备的通知问题,这是基于Windows类型的api。

比如这里又报错:

error LNK2019: unresolved external symbol __imp__RegisterDeviceNotificationW@12 referenced in function __glfwPlatformInit

我们Google一下这个:RegisterDeviceNotificationW,在MSDN中,就可以发现:
在这里插入图片描述
其在 User32.lib 中。

然后接着build,继续查看,重复这样的步骤,最终我们填上:
在这里插入图片描述
当然这都是手动的操作,事实上在vs中新建一个空的project的时候这些都会做好。

用 legacy OpenGL 绘制三角形,只需要短短五行:
在这里插入图片描述

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

完整代码:

#include <GLFW/glfw3.h>

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);

    /* Loop until the user closes the window */
    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();
    }

    glfwTerminate();
    return 0;
}

3. Using Modern OpenGL in C++

第二节我们设置了一个OpenGL窗口和叫做glfw的上下文库。它给我们创建跨平台窗口和为OpenGL定义图像上下文的能力,在我们可以做任何有关绘图的地方。

获取OpenGL函数,它不是你要真正地去下载的某样东西,它事实上在你的图像驱动里面,OpenGL函数是被用在你的gpu驱动里面。我们现在要做的是为了去用任意比OpenGL1.1新的函数,我们需要去获取,进入到那些驱动里面,拉出函数(即获取到它们的声明,然后链接函数),并且调用它们。因此需要驱动dll文件,然后检索到库里面的函数的函数指针,这是我们要做的。

新的OpenGL经过了1.1,基本上现代OpenGL有一大堆函数。但是有两个问题:

  1. 不跨平台。
    到达驱动然后从里面拉出函数,需要用一些win32 api调用外来的Windows加载库,并且加载函数指针,所有的这些都只适用于Windows。
  2. 函数这么多,需要手动检索它们然后为它们写代码,很麻烦!

所以我们要做的是使用另外一个库。因此我们知道,这个库实际上很简单,基本上做的就是提供OpenGL api的规范,函数的声明,符号的声明,常量,所有的东西在一个头文件里面。然后那些幕后文件,C++文件在里面,找到你在使用的图像驱动,找到相应的dll文件,然后加载所有的函数指针,那就是它做的所有。其实做的是苦力活。

要使用的这个库叫 gluw 或者 glew 或者是 opengl extension wrangler,也有另一个库叫 glad,是opengl有点特殊的一个扩展。我们在这里使用 glew 库。

glew库链接:http://glew.sourceforge.net/

查阅文档我们知道:

  1. 不能从glew里面使用opengl函数,直到你调用了glewInit()
  2. 在调用glewInit()之前,需要创建一个有效的opengl渲染上下文(rendering context)

提醒:
如果你在使用一个新的库,先看文档。

这个 s 代表 static,下图的 glew32.lib 是和 dll 配对使用的,从大小也可以看出,因此这里我们选择 glew32s.lib:
在这里插入图片描述
如果我们按这样的顺序:

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

会报错:

 fatal error C1189: #error:  gl.h included before glew.h

双击查看发现在这里报错:
在这里插入图片描述
因此可以发现,意思是之前就已经定义了 _gl_h_ 之类的,即和报错信息一样,说明在<GLFW/glfw3.h>中已经定义过了,所以要更换一下顺序:

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

但是使用 glewInit 还是会报错,发现:
在这里插入图片描述
从变灰的地方我们发现,我们没有预定义宏 GLEW_STATIC,因此还需要:
在这里插入图片描述
我们在glew的doc目录中可以看一下官方文档,可以得知在 glewInit 之前必须要创建一个有效的 OpenGL 渲染上下文,因此在这里 glewInit 需要在 glfwMakeContextCurrent(window); 之后调用:

通过下面代码查看版本:

std::cout << glGetString(GL_VERSION) << std::endl;

输出:

4.6.0 - Build 30.0.100.9805

查阅官方文档(由于我们是 OpenGL4.6版本,所以这里要选gl4去看):
在这里插入图片描述
我们写成函数如下:

unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW);

我们得知第四个参数 usage (上面为GL_STATIC_DRAW)只是一个暗示,让gl去选择实现方式,和性能有关。

调用drawcall的方式:
在这里插入图片描述
在这里插入图片描述
因为之前已经绑定buffer了(选择好状态了),所以就能调用 drawcall (模式,开始位置,绘制数量)

4. Vertex Buffers and Drawing a Triangle in OpenGL

我们需要两个:vertex buffer 和 shader

因为这是 OpenGL 的 buffer,所以不像常规在C++中申请内存,我们这里需要在GPU上(VRAM)申请buffer。

OpenGL自身是一个巨大的状态机(State Machine):一系列的变量描述OpenGL此刻应当如何运行。OpenGL的状态通常被称为OpenGL上下文(Context)。我们通常使用如下途径去更改OpenGL状态:设置选项,操作缓冲。最后,我们使用当前OpenGL上下文来渲染。

参考:
https://learnopengl-cn.github.io/01%20Getting%20started/01%20OpenGL/

因此OpenGL工作的方式是:我希望你选择这个buffer,我希望你选择这个shader,然后draw,本身是一个巨大的状态机而这些都属于它的状态(比如选择这个buffer的状态,选择这个shader的状态)。

并且由于其是一个状态机,如果我们要申请一个buffer,例如:

unsigned int buffer;
glGenBuffers(1, &buffer); // 1 表示 申请一个缓冲区

这里的buffer其实是返回的 id ,因为是一个状态机,这个 id 就代表了这个有效的状态的这个 buffer。因此我们是告诉OpenGL去选择这个状态,而不是说去把这个buffer传递给绘制的函数去绘制(也就是说,不是像一般的函数那样,有一个参数buffer*,然后传递参数个函数)。

因此要选择这个buffer,即选择这个状态,就需要绑定:

glBindBuffer(GL_ARRAY_BUFFER, buffer);

完整代码:

#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 << "Error!" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl;

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

    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW); // position 内存中的data,通过 glBufferData 拷贝到 gpu 的显存中

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);

        glDrawArrays(GL_TRIANGLES, 0, 3); // mode, first, count

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

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

    glfwTerminate();
    return 0;
}

因为glBindBuffer,所以知道用这个buffer;这里结果是一片黑,因为还没有描述这个buffer。。且看下集。

5. Vertex Attributes and Layouts in OpenGL

我们需要告诉OpenGL,在内存里面有什么,是如何布局的。所以要有 Vertex Attributes 。在OpenGL中这个函数叫 glVertext(),一个顶点属性指针;在着色器那边,你也需要去匹配上布局,这布局是你用C++类定义在cpu里面的。

相关函数:glVertexAttribPointer,链接:https://docs.gl/gl4/glVertexAttribPointer

位置、纹理、坐标、法线、颜色…这些都是 attribute ,因此 glVertexAttribPointer 的第一个参数 index 就是告诉我们这个属性是什么索引。比如一个顶点有三个属性(attribute): 位置、纹理、法线,希望位置在 index 0,纹理在 index 1,法线在 index 2 ;因此就需要有描述它们布局(layout)的 Vertex Attributes ,那么相应的 glVertexAttribPointer 这个函数也应该调用三次,每次用 index 去描述。

第二个参数size和第三个type一起,比如position为两个浮点数,size就是2,type就是GL_FLOAT;第四个normalized就是看你,如果比较懒,比如 0~255 的颜色值按理要转化到 0~1 上去给着色器去用,如果懒得自己去做可以让OpenGL帮你去做。

第五个参数stride,第六个参数pointer;前者实际上就是每个顶点之间的字节数量(每个顶点的大小),比如前面的例子:位置、纹理、法线,position 三维,texcoord 2维,normal 3维,因此分别占:12字节、8字节、12字节,因此 stride 就是32字节。(stride步幅,其实就是指针从一个顶点走到另一个顶点的步长)。而对于最后一个参数 pointer ,拿刚刚的例子为例,对于位置 就是0(偏移),纹理则是12,法线则是20,即偏移量。

那么对于第四节我们的内容,就应该是:

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

unsigned int buffer;
glGenBuffers(1, &buffer);
glBindBuffer(GL_ARRAY_BUFFER, buffer);
glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW); // position 内存中的data,通过 glBufferData 拷贝到 gpu 的显存中

// 由于是一个状态机,还需要启动这个 attribute
glEnableVertexAttribArray(0);

// 只有一个位置属性,故index为0;两个浮点,且不需要normalize,于是步长为两个浮点,最后的pointer为0
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

注意,要是比如说多了一个属性,那么在描述该属性的时候,最后一个参数pointer就应该填:

(const void*)8

即强制转化成一个指针的形式。

当然这不是一个好方法,正式做的时候比如用结构体去定义顶点,然后用宏的偏移量来计算这是什么而不是直接写一个8.

至此运行已经可以出现一个白色三角形了,这是因为如果没有提供你自己的着色器的话,GPU驱动会为你提供一个默认的着色器。但是OpenGL标准里面没有内容,这依赖你的gpu驱动,所以还不太好。

6. How Shaders Work in OpenGL

比如一个大三角形,做一次乘法运算,顶点着色器由于就三个顶点,所以就三次;而要是是片段着色器由于大三角形覆盖像素很多,所以会做很多次运算。因此要注意开销,哪些应该在顶点着色器去做需要注意。

在游戏引擎中,实时生成着色器和复杂化时非常常见。

7. Writing a Shader in OpenGL

Cherno 在实际程序中之所以用 unsigned int 而不是 GLuint 这样的,是因为 Cherno 要去解决多种api,所以更习惯于用 C++ 类型。

本节课源代码:

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

static unsigned int CompileShader(unsigned int type, const std::string& source) // 第二个参数是因为 GLenum 是 unsigned int 的
{
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str(); // 返回的是一个指向 string 内部的指针,因此这个 string 必须存在才有效
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    // Error handling
    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result); // iv: integer, vector
    if (result == GL_FALSE)
    {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);

        // alloca 是C语言给你的一个函数,它让你在堆栈上动态地分配,根据你的判断来使用
        // 因为我们不想在堆上分配,所以调用了这个函数
        char* message = (char*)alloca(length * sizeof(char)); 
        glGetShaderInfoLog(id, length, &length, message);
        std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << " shader!" << std::endl;
        std::cout << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

// 从文件中获取,然后编译、链接,生成buffer id,用于之后的绑定
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
    unsigned int program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);

    // 查阅文档我们知道,验证的状态会被存储为程序对象状态的一部分你可以调用,比如用 glGetProgram 查询实际结果是什么之类的
    glValidateProgram(program);

    // 因为已经被链接到一个程序中了,所以如果你愿意可以删除中间部分
    // 在C++中编译东西的时候会得到诸如 .obj 这样的中间文件,shader 也是如此
    // 还有一些其他的函数,比如 glDetachShader 之类的它会删除源代码
    // 但是cherno不喜欢碰那些函数。
    // 首先清理这些也不是必要的,因为它占用的内存很少,但是保留着色器的源代码对于调试之类的是很重要的
    // 因此很多引擎根本不会去调用 glDetachShader 这些
    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

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 << "Error!" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl;

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

    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW); // position 内存中的data,通过 glBufferData 拷贝到 gpu 的显存中

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

    std::string vertexShader =
        "#version 330 core\n"
        "\n"
        "layout(location = 0) in vec4 position;\n"
        "\n"
        "void main()\n"
        "{\n"
        "   gl_Position = position;\n"
        "}\n";

    std::string fragmentShader =
        "#version 330 core\n"
        "\n"
        "layout(location = 0) out vec4 color;\n"
        "\n"
        "void main()\n"
        "{\n"
        "   color = vec4(1.0, 0.0, 0.0, 1.0);\n"
        "}\n";

    unsigned int shader = CreateShader(vertexShader, fragmentShader);
    glUseProgram(shader);

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);

        glDrawArrays(GL_TRIANGLES, 0, 3); // mode, first, count

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

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

    glDeleteProgram(shader);

    glfwTerminate();
    return 0;
}

写了 CompileShader 和 CreateShader。

其中调用 glDeleteShader ,注意:

因为已经被链接到一个程序中了,所以如果你愿意可以删除中间部分
在C++中编译东西的时候会得到诸如 .obj 这样的中间文件,shader 也是如此
还有一些其他的函数,比如 glDetachShader 之类的它会删除源代码
但是cherno不喜欢碰那些函数。
首先清理这些也不是必要的,因为它占用的内存很少,但是保留着色器的源代码对于调试之类的是很重要的
因此很多引擎根本不会去调用 glDetachShader 这些

运行应该是一个红色的三角形。

这里还有一些要注意的地方:

  1. 写顶点着色器的时候,是vec4而实际我们只传了两个浮点数,这是因为 gl_Position 必须是 vec4 的,不然在main里头也需要强制转换
  2. layout(location = 0) 就和 glVertexAttribPointer 对应了,表示这个实际属性位于索引 0 处
  3. 在fragment shader中,layout(location = 0) 可以保持,但是不是必要的,默认就是 0

8. How I Deal with Shaders in OpenGL

有的人喜欢分成两个文件:一个文件专门为 vertex shader,而一个专门为 fragment shader;但是cherno喜欢直接合并成一个文件这样。当然如果以后比如一个顶点着色器要搭配一堆片段着色器,那还是会分成多个文件这样。

于是这一讲我们开始不用字符串,用文件链接两个shader,文件结构如下:
在这里插入图片描述
shader代码如下:

#shader vertex
#version 330 core

layout(location = 0) in vec4 position;

void main()
{
    gl_Position = position;
};

#shader fragment
#version 330 core

layout(location = 0) out vec4 color;

void main()
{
    color = vec4(1.0, 0.0, 0.0, 1.0);
};

其实就是去parse逐行解析这个shader,解析到 #shader 就去判断是什么类型的shader,然后再去填入string stream中,这一节新增函数如下:

struct ShaderProgramSource
{
    std::string VertexSource;
    std::string FragmentSource;
};

static ShaderProgramSource ParseShader(const std::string& filepath)
{
    std::ifstream stream(filepath);

    enum class ShaderType
    {
        NONE = -1, VERTEX = 0, FRAGMENT = 1
    };

    std::string line;
    std::stringstream ss[2];
    ShaderType type = ShaderType::NONE;
    while (std::getline(stream, line))
    {
        if (line.find("#shader") != std::string::npos)
        {
            if (line.find("vertex") != std::string::npos)
            {
                type = ShaderType::VERTEX;
            }
            else if (line.find("fragment") != std::string::npos)
            {
                type = ShaderType::FRAGMENT;
            }
        }
        else
        {
            // 把 type 作为 string stream 的 index,更清晰
            ss[(int)type] << line << '\n';
        }
    }

    return { ss[0].str(), ss[1].str() };
}

完整的代码如下:

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream> // stringstream

struct ShaderProgramSource
{
    std::string VertexSource;
    std::string FragmentSource;
};

static ShaderProgramSource ParseShader(const std::string& filepath)
{
    std::ifstream stream(filepath);

    enum class ShaderType
    {
        NONE = -1, VERTEX = 0, FRAGMENT = 1
    };

    std::string line;
    std::stringstream ss[2];
    ShaderType type = ShaderType::NONE;
    while (std::getline(stream, line))
    {
        if (line.find("#shader") != std::string::npos)
        {
            if (line.find("vertex") != std::string::npos)
            {
                type = ShaderType::VERTEX;
            }
            else if (line.find("fragment") != std::string::npos)
            {
                type = ShaderType::FRAGMENT;
            }
        }
        else
        {
            // 把 type 作为 string stream 的 index,更清晰
            ss[(int)type] << line << '\n';
        }
    }

    return { ss[0].str(), ss[1].str() };
}

static unsigned int CompileShader(unsigned int type, const std::string& source) // 第二个参数是因为 GLenum 是 unsigned int 的
{
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str(); // 返回的是一个指向 string 内部的指针,因此这个 string 必须存在才有效
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    // Error handling
    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result); // iv: integer, vector
    if (result == GL_FALSE)
    {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);

        // alloca 是C语言给你的一个函数,它让你在堆栈上动态地分配,根据你的判断来使用
        // 因为我们不想在堆上分配,所以调用了这个函数
        char* message = (char*)alloca(length * sizeof(char)); 
        glGetShaderInfoLog(id, length, &length, message);
        std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << " shader!" << std::endl;
        std::cout << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

// 从文件中获取,然后编译、链接,生成buffer id,用于之后的绑定
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
    unsigned int program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);

    // 查阅文档我们知道,验证的状态会被存储为程序对象状态的一部分你可以调用,比如用 glGetProgram 查询实际结果是什么之类的
    glValidateProgram(program);

    // 因为已经被链接到一个程序中了,所以如果你愿意可以删除中间部分
    // 在C++中编译东西的时候会得到诸如 .obj 这样的中间文件,shader 也是如此
    // 还有一些其他的函数,比如 glDetachShader 之类的它会删除源代码
    // 但是cherno不喜欢碰那些函数。
    // 首先清理这些也不是必要的,因为它占用的内存很少,但是保留着色器的源代码对于调试之类的是很重要的
    // 因此很多引擎根本不会去调用 glDetachShader 这些
    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

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 << "Error!" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl;

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

    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW); // position 内存中的data,通过 glBufferData 拷贝到 gpu 的显存中

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

    ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");

    unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
    glUseProgram(shader);

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);

        glDrawArrays(GL_TRIANGLES, 0, 3); // mode, first, count

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

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

    glDeleteProgram(shader);

    glfwTerminate();
    return 0;
}

9. Index Buffers in OpenGL

如果没用 index buffer,那么绘制一个 quad(四方形,quadrangle)或者说绘制一个 square 都需要六个点存在 vertex buffer 中(两个三角形要绘制),有两个点重复。

因此出现了 Index Buffer。

在窗口的循环体之前我们这样写代码:

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

    // 必须用 unsigned
    unsigned int indices[] = {
        0, 1, 2,
        2, 3, 0
    };

    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(float), positions, GL_STATIC_DRAW); // position 内存中的data,通过 glBufferData 拷贝到 gpu 的显存中

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

    unsigned int ibo;
    glGenBuffers(1, &ibo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);

    ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");

    unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
    glUseProgram(shader);

drawcall则从原来的 glDrawArrays(GL_TRIANGLES, 0, 3); 变成了:

// 因为已经 glBindBuffer 绑定了 ibo,所以最后一个参数无需绑定任何,直接填 nullptr 就行
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);

实际上大部分情况下我们使用的都是这样的方式,调用drawcall都是用glDrawElements。

10. Dealing with Errors in OpenGL

OpenGL 的 调试很复杂,这是一个很大的话题其实。

这一节主要讲 glGetError,它几乎在每一个 OpenGL 的版本中都有。

查阅官方文档,可以看到如下说明:

Thus, glGetError should always be called in a loop, until it returns GL_NO_ERROR, if all error flags are to be reset.

于是我们这一节新增这两个函数(清理Error和检查Error):

static void GLClearError()
{
    while (glGetError() != GL_NO_ERROR);
}

static void GLCheckError()
{
    while (GLenum error = glGetError())
    {
        std::cout << "[OpenGL Error] (" << error << ")" << std::endl;
    }
}

然后在 drawcall 那行代码使用:

// 通过我们写的 GLClearError 与 GLCheckError 函数,确保再产生 error 是由于 drawcall 这行代码产生的
GLClearError();
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
GLCheckError();

比如这里把 GL_UNSIGNED_INT 写成了 GL_INT,运行时就会出错:
在这里插入图片描述
实际上输出的是错误码,转为16进制(可以debug加断点然后转化)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
然后在glew.h中查找,如下:
在这里插入图片描述
因此我们要做的是把前面东西给打印出来,才知道是什么错误。

当然这里我们已经知道是哪一行出错了,才把两个函数加在代码上下。我们处理错误还常常使用断言assert。

我们这样写:#define ASSERT(X) if (!(x)) __debugbreak();

这里的 __debugbreak 的前缀 “__” 是 compiler intrinsic,可参考如下链接:
https://docs.microsoft.com/en-us/cpp/intrinsics/compiler-intrinsics?view=msvc-170

这暗示了这是在 MSVC 中,所以它不能在 clang 或者 gcc 下工作。

因此通过这个宏,我们就改写为:

GLClearError();
glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr);
ASSERT(GLLogCall());

但是这样还是很麻烦,于是我们又加一行宏:

#define GLCall(x) GLClearError();\
    x;\
    ASSERT(GLLogCall())

这个宏就直接包装起来了,于是我们就可以直接这样调用:

GLCall(glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr));

cool ! ! !

但是提示错误码还是不太方便,我们进一步修改:

#define GLCall(x) GLClearError();\
    x;\
    ASSERT(GLLogCall(#x, __FILE__, __LINE__))

static bool GLLogCall(const char* function, const char* file, int line)
{
    while (GLenum error = glGetError())
    {
        std::cout << "[OpenGL Error] (" << error << ")" << function <<
            " " << file << ":" << line << std::endl;
        return false;
    }
    return true;
}

#x 变为 字符串(构串操作符),可以参考:https://zhuanlan.zhihu.com/p/147496640

__FILE__、__LINE__并不是compiler intrinsic,各大编译器都有,可以放心用。

这样做报错之后就把 报错的那行代码、文件路径、对应行数都答应出来了:
在这里插入图片描述
有了上面的基础,我们就可以简单地用我们写的 GLCall 去包裹每一个OpenGL函数了。

完整代码:

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream> // stringstream

#define ASSERT(x) if (!(x)) __debugbreak(); // 前面的 __ 是 compiler intrinsic
#define GLCall(x) GLClearError();\
    x;\
    ASSERT(GLLogCall(#x, __FILE__, __LINE__))

static void GLClearError()
{
    while (glGetError() != GL_NO_ERROR);
}

static bool GLLogCall(const char* function, const char* file, int line)
{
    while (GLenum error = glGetError())
    {
        std::cout << "[OpenGL Error] (" << error << ")" << function <<
            " " << file << ":" << line << std::endl;
        return false;
    }
    return true;
}

struct ShaderProgramSource
{
    std::string VertexSource;
    std::string FragmentSource;
};

static ShaderProgramSource ParseShader(const std::string& filepath)
{
    std::ifstream stream(filepath);

    enum class ShaderType
    {
        NONE = -1, VERTEX = 0, FRAGMENT = 1
    };

    std::string line;
    std::stringstream ss[2];
    ShaderType type = ShaderType::NONE;
    while (std::getline(stream, line))
    {
        if (line.find("#shader") != std::string::npos)
        {
            if (line.find("vertex") != std::string::npos)
            {
                type = ShaderType::VERTEX;
            }
            else if (line.find("fragment") != std::string::npos)
            {
                type = ShaderType::FRAGMENT;
            }
        }
        else
        {
            // 把 type 作为 string stream 的 index,更清晰
            ss[(int)type] << line << '\n';
        }
    }

    return { ss[0].str(), ss[1].str() };
}

static unsigned int CompileShader(unsigned int type, const std::string& source) // 第二个参数是因为 GLenum 是 unsigned int 的
{
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str(); // 返回的是一个指向 string 内部的指针,因此这个 string 必须存在才有效
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    // Error handling
    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result); // iv: integer, vector
    if (result == GL_FALSE)
    {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);

        // alloca 是C语言给你的一个函数,它让你在堆栈上动态地分配,根据你的判断来使用
        // 因为我们不想在堆上分配,所以调用了这个函数
        char* message = (char*)alloca(length * sizeof(char)); 
        glGetShaderInfoLog(id, length, &length, message);
        std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << " shader!" << std::endl;
        std::cout << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

// 从文件中获取,然后编译、链接,生成buffer id,用于之后的绑定
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
    unsigned int program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);

    // 查阅文档我们知道,验证的状态会被存储为程序对象状态的一部分你可以调用,比如用 glGetProgram 查询实际结果是什么之类的
    glValidateProgram(program);

    // 因为已经被链接到一个程序中了,所以如果你愿意可以删除中间部分
    // 在C++中编译东西的时候会得到诸如 .obj 这样的中间文件,shader 也是如此
    // 还有一些其他的函数,比如 glDetachShader 之类的它会删除源代码
    // 但是cherno不喜欢碰那些函数。
    // 首先清理这些也不是必要的,因为它占用的内存很少,但是保留着色器的源代码对于调试之类的是很重要的
    // 因此很多引擎根本不会去调用 glDetachShader 这些
    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

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 << "Error!" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl;

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

    // 必须用 unsigned
    unsigned int indices[] = {
        0, 1, 2,
        2, 3, 0
    };

    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(float), positions, GL_STATIC_DRAW); // position 内存中的data,通过 glBufferData 拷贝到 gpu 的显存中

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

    unsigned int ibo;
    glGenBuffers(1, &ibo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);

    ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");

    unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
    glUseProgram(shader);

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);
         
        GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));

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

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

    glDeleteProgram(shader);

    glfwTerminate();
    return 0;
}

11. Uniforms in OpenGL

uniform:全局变量。直接从 CPU 丢到 GPU 的。

fragment shader中,我们这样写:

#version 330 core

layout(location = 0) out vec4 color;

uniform vec4 u_Color;

void main()
{
    color = u_Color;
};

application中,利用第十节写的代码,我们这样写:

GLCall(int location = glGetUniformLocation(shader, "u_Color"));
ASSERT(location != -1)
GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));

我们接着做一些好玩的事:

GLCall(int location = glGetUniformLocation(shader, "u_Color"));
ASSERT(location != -1)
GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));

float r = 0.0f;
float increment = 0.05f;

/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
    /* Render here */
    glClear(GL_COLOR_BUFFER_BIT);
     
    GLCall(glUniform4f(location, r, 0.3f, 0.8f, 1.0f));
    GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));

    if (r > 1.0f)
        increment = -0.05f;
    else if (r < 0.0f)
        increment = 0.05f;

    r += increment;

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

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

但是变换太快了,有闪屏的现象,于是我们加一行代码:
(这里我们在glfwMakeContextCurrent后面添加)

glfwMakeContextCurrent(window);

glfwSwapInterval(5);

就有了一个渐变的效果。

12. Vertex Arrays in OpenGL

Vertex Arrays 有点 OpenGL 特别的,和 dx 不一样。听着容易和 Vertex Buffer 混淆。

我们一般在绘制一个object的时候,我们需要绑定 vertex buffer,绑定 index buffer,那么在绑定 vertex buffer的时候,我们还需要具体指出这个 buffer 的 layout ,在之前我们是这样指定的:

对应第五节内容
glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(float), positions, GL_STATIC_DRAW); // position 内存中的data,通过 glBufferData 拷贝到 gpu 的显存中

glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

这样乍一看没有什么毛病,但是如果我们要绘制多个object,则很可能需要不同的 vertex buffer 以及相应不同的 layout 、ibo 等,那么我们就需要这样写:

在进入窗口的循环体之前,全部解绑
glUseProgram(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

当然也可以用之前的方法全部加上第十节我们写的 GLCall 。

然后在绘制的时候,每个需要绘制的 object 都进行绑定:

/* Render here */
glClear(GL_COLOR_BUFFER_BIT);
 
// 绑定 shader 和 传递 uniform
GLCall(glUseProgram(shader));
GLCall(glUniform4f(location, r, 0.3f, 0.8f, 1.0f));

// 绑定 vertex buffer 和 指定其 layout
GLCall(glBindBuffer(GL_ARRAY_BUFFER, buffer));
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

// 绑定 index buffer
GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo));

// 执行 drawcall
GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));

因为每一次都要指定 vertex buffer 的布局,因为不同的 vertex buffer 的布局是可能不一样的,所以每一次都需要执行 glEnableVertexAttribArray 和 glVertexAttribPointer 两条指令去指示。搞出 Vertex Arrays 其实就是针对这样的情况,Vertex Arrays 里头就包含了这些状态。于是我们就可以针对不同的 drawcall 去写不同的 vertex array object 了。

其本质就是:

原来调用一次drawcall要执行:

  1. 绑定shader
  2. 绑定vertex buffer
  3. 指示其layout(set up the vertex layout)
  4. 绑定index buffer
  5. 调用drawcall(glDrawElements)

现在变为了:

  1. 绑定shader
  2. 绑定vertex array
  3. 调用drawcall
    . .
    因此就是用 vertex array object 去包含了 vertex buffer 和其 layout 这些状态。

注:YouTube的评论区说 index buffer 也存在 VAO 中了。
链接:https://www.youtube.com/watch?v=Bcs56Mm-FJY&list=PLlrATfBNZ98foTJPJ_Ev03o2oq3-GGOS2&index=14

从技术上讲,vertex array object 是强制的(mandatory),哪怕是上面这样的程序他们实际上也是被使用的。也就是说即使我们是用原来的方法,没有创建这样的 vao ,实际上也有一个隐藏的 vao 在背后维持。

这样也能运行的原因就是 OpenGL Compatibility Profile

从OpenGL 3开始,编程模式被分为3种:
1.兼容模式:即传统的方式
2.核心模式:完全新式纯可编程流水线模式,但兼容废弃特性。(注:API不兼容旧的)
3.核心向前兼容模式:完全不兼容废弃特性,其它同核心模式。(主要表现在线宽、线调色板这一类,其它同核心模式)
参考自贴吧中一个网友的回答。
参考https://learnopengl-cn.github.io/01%20Getting%20started/01%20OpenGL/
从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。

在 Compatibility Profile 下实际上为我们默认创建了一个 vao ,这也是之前我们想要解绑的时候写的那个 0 .

但是在 Core-profile 就没有这样做。因此在这种模式下我们就需要自己显示地创建一个 vertex array object 并确保运行时已经绑定了 vao 。

因此现在运行程序我们执行的好好的,但是接下来我们改成 Core-profile 试试,也就是告诉OpenGL上下文我们希望在核心模式下渲染,通过这三行代码:

glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

看意思也可以猜出,这就是告诉 glfw 创建OpenGL上下文的时候的版本,第一行代码说明是OpenGL3,第二行说明是.3,于是一起即 OpenGL3.3 版本,第三行说明 profile 选择为 Core-profile,即核心模式。

因此若是最后一行改为glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);,则仍然能运行的好好的,完整代码如下:

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream> // stringstream

#define ASSERT(x) if (!(x)) __debugbreak(); // 前面的 __ 是 compiler intrinsic
#define GLCall(x) GLClearError();\
    x;\
    ASSERT(GLLogCall(#x, __FILE__, __LINE__))

static void GLClearError()
{
    while (glGetError() != GL_NO_ERROR);
}

static bool GLLogCall(const char* function, const char* file, int line)
{
    while (GLenum error = glGetError())
    {
        std::cout << "[OpenGL Error] (" << error << ")" << function <<
            " " << file << ":" << line << std::endl;
        return false;
    }
    return true;
}

struct ShaderProgramSource
{
    std::string VertexSource;
    std::string FragmentSource;
};

static ShaderProgramSource ParseShader(const std::string& filepath)
{
    std::ifstream stream(filepath);

    enum class ShaderType
    {
        NONE = -1, VERTEX = 0, FRAGMENT = 1
    };

    std::string line;
    std::stringstream ss[2];
    ShaderType type = ShaderType::NONE;
    while (std::getline(stream, line))
    {
        if (line.find("#shader") != std::string::npos)
        {
            if (line.find("vertex") != std::string::npos)
            {
                type = ShaderType::VERTEX;
            }
            else if (line.find("fragment") != std::string::npos)
            {
                type = ShaderType::FRAGMENT;
            }
        }
        else
        {
            // 把 type 作为 string stream 的 index,更清晰
            ss[(int)type] << line << '\n';
        }
    }

    return { ss[0].str(), ss[1].str() };
}

static unsigned int CompileShader(unsigned int type, const std::string& source) // 第二个参数是因为 GLenum 是 unsigned int 的
{
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str(); // 返回的是一个指向 string 内部的指针,因此这个 string 必须存在才有效
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    // Error handling
    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result); // iv: integer, vector
    if (result == GL_FALSE)
    {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);

        // alloca 是C语言给你的一个函数,它让你在堆栈上动态地分配,根据你的判断来使用
        // 因为我们不想在堆上分配,所以调用了这个函数
        char* message = (char*)alloca(length * sizeof(char)); 
        glGetShaderInfoLog(id, length, &length, message);
        std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << " shader!" << std::endl;
        std::cout << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

// 从文件中获取,然后编译、链接,生成buffer id,用于之后的绑定
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
    unsigned int program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);

    // 查阅文档我们知道,验证的状态会被存储为程序对象状态的一部分你可以调用,比如用 glGetProgram 查询实际结果是什么之类的
    glValidateProgram(program);

    // 因为已经被链接到一个程序中了,所以如果你愿意可以删除中间部分
    // 在C++中编译东西的时候会得到诸如 .obj 这样的中间文件,shader 也是如此
    // 还有一些其他的函数,比如 glDetachShader 之类的它会删除源代码
    // 但是cherno不喜欢碰那些函数。
    // 首先清理这些也不是必要的,因为它占用的内存很少,但是保留着色器的源代码对于调试之类的是很重要的
    // 因此很多引擎根本不会去调用 glDetachShader 这些
    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

int main(void)
{
    GLFWwindow* window;

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

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);

    /* 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);

    glfwSwapInterval(5);

    if (glewInit() != GLEW_OK)
        std::cout << "Error!" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl;

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

    // 必须用 unsigned
    unsigned int indices[] = {
        0, 1, 2,
        2, 3, 0
    };

    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(float), positions, GL_STATIC_DRAW); // position 内存中的data,通过 glBufferData 拷贝到 gpu 的显存中

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

    unsigned int ibo;
    glGenBuffers(1, &ibo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);

    ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");

    unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
    glUseProgram(shader);

    GLCall(int location = glGetUniformLocation(shader, "u_Color"));
    ASSERT(location != -1)
    GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));

    GLCall(glUseProgram(0));
    GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));
    GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));

    float r = 0.0f;
    float increment = 0.05f;

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);
         
        // 绑定 shader 和 传递 uniform
        GLCall(glUseProgram(shader));
        GLCall(glUniform4f(location, r, 0.3f, 0.8f, 1.0f));

        // 绑定 vertex buffer 和 指定其 layout
        GLCall(glBindBuffer(GL_ARRAY_BUFFER, buffer));
        GLCall(glEnableVertexAttribArray(0));
        GLCall(glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0));

        // 绑定 index buffer
        GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo));

        // 执行 drawcall
        GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));

        if (r > 1.0f)
            increment = -0.05f;
        else if (r < 0.0f)
            increment = 0.05f;

        r += increment;

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

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

    glDeleteProgram(shader);

    glfwTerminate();
    return 0;
}

但若是改回 CORE ,就会报错:
在这里插入图片描述
在这里插入图片描述

可以参考:https://www.khronos.org/opengl/wiki/Vertex_Specification#Vertex_Array_Object

The compatibility OpenGL profile makes VAO object 0 a default object. The core OpenGL profile makes VAO object 0 not an object at all. So if VAO 0 is bound in the core profile, you should not call any function that modifies VAO state. This includes binding the GL_ELEMENT_ARRAY_BUFFER with glBindBuffer.

再参考:https://docs.gl/gl4/glEnableVertexAttribArray

可以看到:
在这里插入图片描述
因此报错的原因就是没有 vertex array object 被 bound,因此不能用 glEnableVertexAttribArray

因此在 Core-profile 下就要创建这样的 vao:

unsigned int vao;
GLCall(glGenVertexArrays(1, &vao));
GLCall(glBindVertexArray(vao));

这时就已经不报错了,不过事实上就如之前说的,我们没必要再去绑定 vertex buffer 和用glVertexAttribPointer 那样指示 layout 了,只要绑定 vertex array:

GLCall(glBindVertexArray(0));  // 加上这一行
GLCall(glUseProgram(0));
GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));
GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));

... 

// 绑定 vertex buffer 和 指定其 layout
GLCall(glBindBuffer(GL_ARRAY_BUFFER, buffer));

GLCall(glBindVertexArray(vao)); // 循环体中改成绑定 vao

// 绑定 index buffer
// GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo));

但是这样究竟是如何把 vertex buffer,layout,和 vao 绑定起来的呢?其实是通过循环体外的这一句:

glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

当我们一开始绑定 vao 和 vertex buffer 的时候,实际上没有把他们连接起来。真正连接他们的是绑定之后使用上面的指令去指示。

参考:https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
在这里插入图片描述
这里的 EBO(Element Buffer Object)就是我们之前说的 IBO

其实看这张图就能明晰很多概念:
VAO里头存着的是一些指向 VBO 的指针(具体有 GL_MAX_VERTEX_ATTRIBS 个,在Vertex_Specification 有说 Vertex attributes are numbered from 0 to GL_MAX_VERTEX_ATTRIBS - 1. Each attribute can be enabled or disabled for array access. When an attribute’s array access is disabled, any reads of that attribute by the vertex shader will produce a constant value (see below) instead of a value pulled from an array.),因此每一个指针其实是一个状态,看下面“注”中第三点的伪代码实现也可以便于理解,这个状态可以被 enabled 或者 disabled,然后一个 VAO 里头仅有一个 IBO。

注:

  1. 我 debug 发现 vao 在这段程序中的值为1,所以之前的glBindVertexArray(0)确实解绑了,在drawcall的时候还需要glBindVertexArray(vao)
  2. 在前面的参考链接中说:
    A newly-created VAO has array access disabled for all attributes. Array access is enabled by binding the VAO in question and calling:void glEnableVertexAttribArray(GLuint index​);
    这就是说,一个新创建的 VAO ,其内部属性只能通过绑定VAO和调用 glEnableVertexAttribArray 这条指令才能做到。
  3. 推荐这个回答:https://www.zhihu.com/question/39082624/answer/79638826,里面的伪代码很便于理解

Cherno 建议用 VAOs。

完整代码:

#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <fstream>
#include <string>
#include <sstream> // stringstream

#define ASSERT(x) if (!(x)) __debugbreak(); // 前面的 __ 是 compiler intrinsic
#define GLCall(x) GLClearError();\
    x;\
    ASSERT(GLLogCall(#x, __FILE__, __LINE__))

static void GLClearError()
{
    while (glGetError() != GL_NO_ERROR);
}

static bool GLLogCall(const char* function, const char* file, int line)
{
    while (GLenum error = glGetError())
    {
        std::cout << "[OpenGL Error] (" << error << ")" << function <<
            " " << file << ":" << line << std::endl;
        return false;
    }
    return true;
}

struct ShaderProgramSource
{
    std::string VertexSource;
    std::string FragmentSource;
};

static ShaderProgramSource ParseShader(const std::string& filepath)
{
    std::ifstream stream(filepath);

    enum class ShaderType
    {
        NONE = -1, VERTEX = 0, FRAGMENT = 1
    };

    std::string line;
    std::stringstream ss[2];
    ShaderType type = ShaderType::NONE;
    while (std::getline(stream, line))
    {
        if (line.find("#shader") != std::string::npos)
        {
            if (line.find("vertex") != std::string::npos)
            {
                type = ShaderType::VERTEX;
            }
            else if (line.find("fragment") != std::string::npos)
            {
                type = ShaderType::FRAGMENT;
            }
        }
        else
        {
            // 把 type 作为 string stream 的 index,更清晰
            ss[(int)type] << line << '\n';
        }
    }

    return { ss[0].str(), ss[1].str() };
}

static unsigned int CompileShader(unsigned int type, const std::string& source) // 第二个参数是因为 GLenum 是 unsigned int 的
{
    unsigned int id = glCreateShader(type);
    const char* src = source.c_str(); // 返回的是一个指向 string 内部的指针,因此这个 string 必须存在才有效
    glShaderSource(id, 1, &src, nullptr);
    glCompileShader(id);

    // Error handling
    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result); // iv: integer, vector
    if (result == GL_FALSE)
    {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);

        // alloca 是C语言给你的一个函数,它让你在堆栈上动态地分配,根据你的判断来使用
        // 因为我们不想在堆上分配,所以调用了这个函数
        char* message = (char*)alloca(length * sizeof(char)); 
        glGetShaderInfoLog(id, length, &length, message);
        std::cout << "Failed to compile " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << " shader!" << std::endl;
        std::cout << message << std::endl;
        glDeleteShader(id);
        return 0;
    }

    return id;
}

// 从文件中获取,然后编译、链接,生成buffer id,用于之后的绑定
static unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{
    unsigned int program = glCreateProgram();
    unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexShader);
    unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentShader);

    glAttachShader(program, vs);
    glAttachShader(program, fs);
    glLinkProgram(program);

    // 查阅文档我们知道,验证的状态会被存储为程序对象状态的一部分你可以调用,比如用 glGetProgram 查询实际结果是什么之类的
    glValidateProgram(program);

    // 因为已经被链接到一个程序中了,所以如果你愿意可以删除中间部分
    // 在C++中编译东西的时候会得到诸如 .obj 这样的中间文件,shader 也是如此
    // 还有一些其他的函数,比如 glDetachShader 之类的它会删除源代码
    // 但是cherno不喜欢碰那些函数。
    // 首先清理这些也不是必要的,因为它占用的内存很少,但是保留着色器的源代码对于调试之类的是很重要的
    // 因此很多引擎根本不会去调用 glDetachShader 这些
    glDeleteShader(vs);
    glDeleteShader(fs);

    return program;
}

int main(void)
{
    GLFWwindow* window;

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

    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    /* 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);

    glfwSwapInterval(5);

    if (glewInit() != GLEW_OK)
        std::cout << "Error!" << std::endl;

    std::cout << glGetString(GL_VERSION) << std::endl;

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

    // 必须用 unsigned
    unsigned int indices[] = {
        0, 1, 2,
        2, 3, 0
    };

    unsigned int vao;
    GLCall(glGenVertexArrays(1, &vao));
    GLCall(glBindVertexArray(vao));

    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER, buffer);
    glBufferData(GL_ARRAY_BUFFER, 8 * sizeof(float), positions, GL_STATIC_DRAW); // position 内存中的data,通过 glBufferData 拷贝到 gpu 的显存中

    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);

    unsigned int ibo;
    glGenBuffers(1, &ibo);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, 6 * sizeof(unsigned int), indices, GL_STATIC_DRAW);

    ShaderProgramSource source = ParseShader("res/shaders/Basic.shader");

    unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
    glUseProgram(shader);

    GLCall(int location = glGetUniformLocation(shader, "u_Color"));
    ASSERT(location != -1)
    GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));

    // 全部解绑
    GLCall(glBindVertexArray(0));
    GLCall(glUseProgram(0));
    GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));
    GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));

    float r = 0.0f;
    float increment = 0.05f;

    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here */
        glClear(GL_COLOR_BUFFER_BIT);
         
        // 绑定 shader 和 传递 uniform
        GLCall(glUseProgram(shader));
        GLCall(glUniform4f(location, r, 0.3f, 0.8f, 1.0f));

        // 绑定 VAO
        GLCall(glBindVertexArray(vao));

        // 执行 drawcall
        GLCall(glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr));

        if (r > 1.0f)
            increment = -0.05f;
        else if (r < 0.0f)
            increment = 0.05f;

        r += increment;

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

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

    glDeleteProgram(shader);

    glfwTerminate();
    return 0;
}

13. Abstracting OpenGL into Classes

14. Buffer Layout Abstraction in OpenGL

15. Shader Abstraction in OpenGL

16. Writing a Basic Renderer in OpenGL

17. Textures in OpenGL

18. Blending in OpenGL

19. Maths in OpenGL

20. Projection Matrices in OpenGL

21. Model View Projection Matrices in OpenGL

22. ImGui in OpenGL

23. Rendering Multiple Objects in OpenGL

24. Setting up a Test Framework for OpenGL

25. Creating Tests in OpenGL

26. Creating a Texture Test in OpenGL

27. How to make your UNIFORMS FASTER in OpenGL

28. Batch Rendering - An Introduction

29. Batch Rendering - Colors

30. Batch Rendering - Textures

31. Batch Rendering - Dynamic Geometry

32. Batch Rendering - Indices

SP32. Writing a BATCH RENDERER in ONE HOUR!

  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值