opengl是一种规范,而不是库或者引擎。

显卡驱动程序中会有自己的对opengl的实现,不同的显卡可能会有所不同。

opengl不是开源的。

使用现代的opengl

我们需要在linker的input选项里添加opengl32.lib。我们最终用的头文件,特别是用于隐藏函数的头文件,事实上,我们用的一个是glfw,它提供了转动调整窗口的效果,另一个是现在的windows。

windows已经有了一个图像api,它叫DirectX或者是Direct3D。它是一个专用一点的,在windows上,windows也鼓励使用directx这个api,但是记住,这不是windows能够决定的事,这取决于我们的gpu的供应商和来自英伟达,amd,英特尔的gpu驱动程序,这是我们应该使用opengl的地方,它们都支持opengl作为一个渲染api。

windows的opengl头文件很古老,需要我们自己去获得opengl的新功能。于是我们获取opengl函数,但是他不是要你真正地去下载某样东西,它(opengl函数)事实上在你的图像驱动里。opengl函数在你的gpu驱动里,为了去用任意比opengl1.1新的函数,我们需要进入到那些驱动盒里面,获取函数。
我们需要一些win32 api调用或者外来的windows加载库,并且加载函数指针

我们引入了glew库,具体请参考cherno的视频

#include <iostream>
#include <GL/glew.h>
#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);//需要生成渲染模块才能初始化glew

    if (glewInit() != GLEW_OK)
        std::cout << "Error!" << std::endl;
    /* Loop until the user closes the window */
    while (!glfwWindowShouldClose(window))
    {
        /* Render here 调用glClear()函数清空颜色缓冲区,准备绘制新的帧。*/
        glClear(GL_COLOR_BUFFER_BIT);

        glBegin(GL_TRIANGLES);
        glVertex2f(-0.5f, -0.5);
        glVertex2f(0.5f, 0.5);
        glVertex2f(-0.5f, 0.5);
        glEnd();//5行代码生成了一个三角形

        /* Swap front and back buffers 调用glfwSwapBuffers()函数交换前后缓冲区,这样刚才绘制的内容才会在屏幕上显示出来。*/
        glfwSwapBuffers(window);

        /* Poll for and process events 调用glfwPollEvents()函数处理窗口事件,比如鼠标、键盘等输入事件。*/
        glfwPollEvents();
    }

    glfwTerminate();
    return 0;
}

这行代码使我们生成了一个三角形:

顶点缓冲区,他基本上就是去掉vertex这个单词,他只是一个内存缓冲区,一个内存字节数组。但是顶点缓冲区和cpp中像字符数组的内存缓冲区不一样,区别在于它是opengl的内存缓冲区,这意味着它实际上在显卡GPU上,在我们的VRAM(video ram)中。

在OpenGL中,缓冲区对象用于存储和管理图形数据。这些数据可以是顶点数据,纹理数据或其他任何类型的数据,而缓冲区对象是将数据发送到GPU进行渲染的关键。

这里的基本想法是,我要定义一些数据来表示三角形,放入显卡的vram中,发出drawcall指令,这是一个绘制指令,就是说,嘿你的显存中有一堆数据,读取它,并且把它画在屏幕上。事实上我们还要告诉gpu如何读取和解释这些数据,以及如何把它放到我们的屏幕上。当我们在cpu这边做了所有的事情,我们还要用某种方法告诉显卡:好了,一旦你在显卡端获得了这些数据,我要你像这样把它摆出来,我希望你把它画出来在屏幕上给我显示一个三角形。所以我们需要告诉显卡怎么做,需要对显卡编程,这就是着色器,着色器只是一个运行在显卡上的程序,它是一堆我们可以编写的在显卡上以一种非常特殊的方式运行的代码。

还有重要的一点,opengl是一个状态机,你所需要做的事设置一系列的状态。当你做了一些事的时候,比如:给我画一个三角形,这是与上下文非常相关的。我想让你用这个缓冲区,这个着色器,给我画一个三角形,然后根据你选择的缓冲区和着色器,决定画什么样子的三角形,画在哪里等等。

在计算机科学中,状态机(state machine)是一个用于表示系统状态的数学模型,通常由一组状态、一组输入和一组输出组成,表示在输入的作用下,系统从一种状态转移到另一种状态的过程,产生相应的输出。在计算机图形学中,OpenGL被称为一个状态机,它管理着许多状态,包括渲染状态、矩阵状态、着色器状态、纹理状态等等。

在OpenGL中,状态通常表示为一组全局变量,每个变量存储着某个状态的值。例如,glEnable函数可以用于启用或禁用某个状态,而glClearColor函数可以用于设置背景颜色状态。这些状态值在OpenGL中的使用方式与其他的C++ API不同,它们被存储在OpenGL上下文中,并且可以在任何时候被查询和更改。

在OpenGL中,通过更改状态的值来控制渲染的结果。例如,可以通过设置矩阵状态来指定几何变换矩阵,然后将模型的顶点位置指定为顶点着色器的输入,从而更改渲染结果。OpenGL的状态机模型具有一定的优点,例如可重用状态、便于调试和更改状态时避免重复计算等等。但是,过多的状态转换和状态查询也可能会降低OpenGL的性能,因此需要合理地管理状态机。

然后让我们写一些代码来创建顶点缓冲区。我们现在想用现代opengl来创建这个三角形。
我们现在想把glVertex2f里的数据都放进一个缓冲区,传到OpenGL的VRAM,然后发出一个DrawCall指令,说:嘿,请根据缓冲区画出图形。
要做到这点,需要这行代码:

glBegin(GL_TRIANGLES);
glVertex2f(-0.5f, -0.5);
glVertex2f(0.5f, 0.5);
glVertex2f(-0.5f, 0.5);
glEnd();//5行代码生成了一个三角形

之后我们需要创建自己的缓冲区,这个过程非常简单,你只需要输入glGenBuffers()。

unsigned int buffer
glGenBuffer(1 , &buffer)

opengl可以一次生成一堆的缓冲区,我们这里只需要一个缓冲区,第一个参数指定需要几个缓冲区;第二个参数指定返回整数的内存地址,这也是生成的缓冲区的 id。记住 OpenGL 是作为一个状态机工作,这意味着你可以生成一切,而 OpenGL 中生成所有东西都分配了一个唯一的标识符,它只是一个整数,也是你实际对象的 id,当你想要使用这个对象的时候就用这个数字。

因为我要渲染我的三角形,需要说明用哪个缓冲区来渲染三角形,只需要传递这个整数即可。现在我们有了这个 id,一旦创建缓冲区后,我们现在就要选择那个缓冲区。选择(Selecting)在 OpenGL 中被称为绑定(Binding)

glBindBuffer(GL_ARRAY_BUFFER , buffer);

下一步就要将数据存入这个缓冲区。

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

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

通常我们还会需要创建一个索引缓冲区,但是这是后话了。
单单这些代码还是不够的,我们还需要告诉opengl怎么布局这些数据,因为从着色器的角度,我们希望能够以恰当的方式读取这些数据。我们可以使用glVertexAttribPointer()函数来实现。这又是和着色器紧密联系在一起的东西。

我们可以使用立即模式画一个三角形。我实际上会为这个缓冲区发出一个drawcall指令。与两种方法实现:
glDrawArrays(),这是一个没有索引缓冲区的时候可以使用的方法。
glDrawElements(),这是有索引缓冲区的时候使用的函数。

OpenGL中顶点的属性和布局

opengl的概念一个顶点不只是位置,顶点只是一个在几何体上的点,人们在视觉上对它们的看法显然是由它的位置决定的。顶点可以包含比一个位置更多的东西。

布局这些缓冲区的数据需要用到glVertexAttribPointer()
文档:glVertexAttribPointer - OpenGL 4 - docs.gl

glVertexAttribPointer — define an array of generic vertex attribute data
它需要一些参数,有索引、大小、类型、法线、步幅、指针。

index

基本上,我们着色器读取这些所有东西,都要通过一个索引。一般来说如果我们有一个位置在索引 0 处,我们需要把它作为索引 0 来引用;而当我们有三种属性,我想让我的位置在下标 0,纹理坐标在索引 1,法线在索引 2.所以当我开始从着色器和显卡读取数据时,然后进入那个缓冲区,我可以简单地引用它们。这就是索引,它只是缓冲区实际属性的索引。

size

Specifies the number of components per generic vertex attribute. Must be 1, 2, 3, 4. Additionally, the symbolic constant GL_BGRA is accepted by glVertexAttribPointer. The initial value is 4.这里的 size 可能有点误导人,它是每个通用顶点属性的组件数,只能是 1,2,3,4。所以这个 size 和字节没有关系,和它们实际占用了多少内存也没关系。在本例中每个顶点的坐标有 x 和 y 两组分量,所以 size 为 2。

type

Specifies the data type of each component in the array. The symbolic constants GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, and GL_UNSIGNED_INT are accepted by glVertexAttribPointer and glVertexAttribIPointer. Additionally GL_HALF_FLOAT, GL_FLOAT, GL_DOUBLE, GL_FIXED, GL_INT_2_10_10_10_REV, GL_UNSIGNED_INT_2_10_10_10_REV and GL_UNSIGNED_INT_10F_11F_11F_REV are accepted by glVertexAttribPointer. GL_DOUBLE is also accepted by glVertexAttribLPointer and is the only token accepted by the type parameter for that function. The initial value is GL_FLOAT.

normalized

标准化其实不用太担心,如果我们处理的是浮点数,因为它们已经被规范化了。假设我们要指定一个颜色字节在 0 到 255 之间,它在我们的实际着色器作为一个浮点数需要被规范化到 0 到 1 之间,这不是一个你可以在 CPU 上做的事情,但你可以让 OpenGL 替你做。

stride

Specifies the byte offset between consecutive generic vertex attributes. If stride is 0, the generic vertex attributes are understood to be tightly packed in the array. The initial value is 0.

stride 指针会让很多人感到困惑,如文档所示它就是连续通用顶点属性之间的字节偏移量,也可以理解为每个顶点之间的字节数。举个例子我们有位置 vec3、纹理坐标 vec2 和法线 vec3,那么我们的 stride 就是 3 * 4 + 2 * 4 + 3 * 4 = 32 字节,它是每个顶点的字节大小。

如果我们想从一个顶点跳到下一个顶点,我需要在缓冲区中加上 32 个字节。所以如果我们有一个指针指向缓冲区的开始,然后经过缓冲区的 32 个字节,我应该在下一个顶点的起点,这就是 stride。

你可以用宏来代替这些数据

pointer

Specifies a offset of the first component of the first generic vertex attribute in the array in the data store of the buffer currently bound to the GL_ARRAY_BUFFER target. The initial value is 0.

pointer 文档的表述上第一个组件的一个偏移量,它是指向实际属性的指针。不要管有多少个顶点,聚焦于一个顶点,里面包含位置、纹理坐标和法线。对于位置偏移量为 0,因为它是缓冲区的第一个字节;然后我们前进 12 个字节到达纹理坐标,所以对于我的纹理坐标属性这个值(pointer)是 12;最后再前进 8 字节得到顶点的法线,所以对于顶点法线属性 20 是这个 pointer 的值。

Cherno OpenGL 教程_Yousazoe的博客-CSDN博客

需要一个enable函数

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

这两段代码告诉 OpenGL 缓冲区的布局是什么,理论上如果有一个着色器就可以看到在屏幕上看到三角形了。

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

using namespace std;

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;

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

    unsigned int buffer;
    glGenBuffers(1, &buffer);
    glBindBuffer(GL_ARRAY_BUFFER , buffer);//GL_ARRAY_BUFFER是OpenGL的一个缓冲区类型,它用于存储顶点数据。当我们需要在渲染过程中向顶点着色器提供顶点数据时,我们可以使用GL_ARRAY_BUFFER类型的缓冲区对象来存储这些数据。
    glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW);

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

    glBindBuffer(GL_ARRAY_BUFFER, 0);

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

        glDrawArrays(GL_TRIANGLES, 0, 3);

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

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

    glfwTerminate();
    return 0;
}

opengl的shader

如果我们运行之前的代码,屏幕上会打印一个白色三角形,这是因为gpu驱动会向你提供默认的着色器,如果你没有提供自己的着色器的话。
但是这事实上是基于你电脑的驱动的。

chatgpt:

在OpenGL中,着色器是一种运行在GPU上的小程序,用于处理图形渲染过程中的各种计算。它们可以用来实现顶点转换、颜色计算、纹理采样和像素输出等功能。着色器使用一种专门的语言编写,例如OpenGL Shader Language(GLSL),然后编译并链接到OpenGL应用程序中。

着色器的主要作用是控制图形渲染过程中的各个阶段,例如顶点着色器(Vertex Shader)负责将顶点从局部空间转换为屏幕空间,片段着色器(Fragment Shader)则负责计算像素的颜色和深度值等。着色器可以根据具体的应用需求进行定制,从而实现各种各样的效果,例如光照、阴影、纹理映射等。


当我们发出一个绘制调用,顶点着色器会获得调用,片段着色器会get_cold,然后我们就会在屏幕上看到结果。(简单起见中间有很多部分都跳过去了)

顶点着色器
那么顶点着色器是做什么的?

它会被我们渲染的每个顶点调用,在这个例子中我们有一个三角形三个顶点,这意味着顶点着色器会被调用三次,每个顶点调用一次。顶点着色器的主要目的是告诉 OpenGL 你希望这个顶点在屏幕空间的什么位置。再强调一次,顶点着色器的主要目的是提供那些顶点的位置,如果有必要我们需要能够提供一些变换以便 OpenGL 能把这些数字转化成屏幕坐标,这样我们就能在窗口中看到我们的图形在对的位置。

片段着色器
一旦顶点着色器运行结束,我们就进入了管道的下一个阶段:片段着色器或者像素着色器。

虽然片段和像素在术语上有点小差别,但现在你可以把像素当成片段或者把片段想象成像素,因为片段着色器会为每个需要光栅化的像素运行一次。我们的窗口基本上是由像素组成的,我们指定的那三个顶点组成我们的三角形现在需要用实际的像素填充,这就是光栅化阶段所做的。

片段着色器或像素着色器就是对三角形中需要填充的每个像素调用一次,主要决定这个像素是什么颜色,这就是它的作用,它决定了像素的输出颜色,这样像素就可以用正确的颜色着色。形象一点可以把它想象成一本涂色本,当你只有东西的轮廓时需要给它上色,这就是片段着色器的职责。

相比于顶点着色器,片段着色器里面的东西代价要高得多,因为它会为每个像素运行。

话虽如此,有些东西显然需要按像素计算例如光源。如果你在计算光源,每个像素都有一个颜色值,这个值是由很多东西决定:光源、环境、纹理、提供给表面的材质…所有这些一起来确定一个特定像素的正确颜色。显然这取决于一些输入,例如相机的位置在哪里,而这些所有的东西结束后你在片段着色器中的决定仅仅是单个像素的颜色,这就是片段着色器的作用。
————————————————
版权声明:本文为CSDN博主「Yousazoe」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43454101/article/details/125019293

写一个shader

第一步我们需要定义一个静态函数,因为我不希望它链接到其他的编译单元或者cpp文件。他会返回一个整数。

和创建缓冲区一致,我们需要返回一个整型作为标识符,当我们想绑定的时候可以绑定那个缓冲区 id。

所以我们要做的第一件事就是创建一个程序,基本上我们只需要输入 glCreateProgram(),该函数不需要传入整数引用之类的东西,它会返回一个无符号的整数(顺带一提,这里和后面不使用 OpenGL 自带类型的原因是个人处理多种类型的图形 API,更倾向于 C++ 类型)

static unsigned int CompileShader(unsigned int type , const std::string source)
{//由于代码的复用性,我们可以写一个函数来生成着色器对象
    unsigned int id = glCreateShader(GL_VERTEX_SHADER);
    const char* src = source.c_str();//初始化函数,查找字符串的第一个字符然后返回它的内存地址
    glShaderSource(id, 1, &src, nullptr);
    /*
    这行代码是将指定ID的着色器对象的源代码设置为传递给函数的源代码字符串。具体来说:

id是用于指定要修改的着色器对象的ID。
1指定要修改的源代码字符串数量。在这种情况下,我们只传递了一个源代码字符串。
&src是指向源代码字符串的指针,该源代码字符串已存储在内存中,src是一个指向此字符串的指针。
nullptr指定字符串长度数组的指针,表示每个源代码字符串的长度。因为我们使用了一个NULL指针,OpenGL会自动假定我们使用的字符串是以NULL结束的,这样它可以自己计算字符串长度。
    glCompileShader(id);
    */
    
    return 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);
    glValidateProgram(program);//这个过程很像 C++编译中将两个不同的文件链接在一起,以便可以同时使用它们:

    glDeleteShader(vs);
    glDeleteShader(fs);//连接程序完成,可以删除shader
    
    return program;
}

错误处理:

//TODO: Error handing
    int result;
    glGetShaderiv(id, GL_COMPILE_STATUS, &result);
    if (result == GL_FALSE)
    {
        int length;
        glGetShaderiv(id, GL_INFO_LOG_LENGTH , &length);
		//获取着色器的编译状态
        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;
    }

chatgpt

glShaderSource(id, 1, &src, nullptr);是什么意思

这行代码是将指定ID的着色器对象的源代码设置为传递给函数的源代码字符串。具体来说:

  • id是用于指定要修改的着色器对象的ID。
  • 1指定要修改的源代码字符串数量。在这种情况下,我们只传递了一个源代码字符串。
  • &src是指向源代码字符串的指针,该源代码字符串已存储在内存中,src是一个指向此字符串的指针。
  • nullptr指定字符串长度数组的指针,表示每个源代码字符串的长度。因为我们使用了一个NULL指针,OpenGL会自动假定我们使用的字符串是以NULL结束的,这样它可以自己计算字符串长度。

chatgpt

glGetShaderInfoLog() 函数是用于获取着色器编译或连接期间的错误消息或警告信息的函数。在这段代码中,该函数被用于获取着色器编译期间的错误消息。

具体来说,id 是用于指定要查询信息的着色器对象的 ID。length 是用于指定接收消息的缓冲区大小的最大值,它的值在前面调用 glGetShaderiv() 获取着色器编译状态时已经获取了。&length 是返回实际消息长度的变量的指针,message 是指向接收错误消息的缓冲区的指针。

在函数调用后,message 缓冲区中将包含编译错误消息或警告信息。如果着色器编译成功,则消息缓冲区可能为空或包含一些提示性信息。如果编译失败,消息缓冲区将包含有关编译错误的详细信息,包括出错行数和错误描述等。


接下来我们快速写一个着色器:

首先 #version 330 core 意味着我们将使用 GLSL(OpenGL 的着色器),其次指定位置和颜色:

std::string vertexShader = R"(
        #version 330 core
        
        layout(location = 0) in vec4 position;

        void main()
		{
			gl_Position = position;
		}
    )";//这意味着他不让我们使用任何弃用函数或者类似的东西
	std::string fragmentShader = R"(
        #version 330 core
        
        layout(location = 0) in vec4 color;

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

	unsigned int shader = CreateShader(vertexShader, fragmentShader);
	glUseProgram(shader);
#include <iostream>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <vector>

static unsigned int CompileShader(unsigned int type, const std::string& source)
{
	unsigned int id = glCreateShader(type);
	const char* src = source.c_str();
	glShaderSource(id, 1, &src, nullptr);
	glCompileShader(id);

	//TODO: Error handing
	int result;
	glGetShaderiv(id, GL_COMPILE_STATUS, &result);
	if (result == GL_FALSE)
	{
		int length;
		glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
		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;
}

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);
	glValidateProgram(program);//这个过程很像 C++编译中将两个不同的文件链接在一起,以便可以同时使用它们:

	glDeleteShader(vs);
	glDeleteShader(fs);//连接程序完成,可以删除shader

	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;

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

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

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

	std::string vertexShader = R"(
        #version 330 core

        layout(location = 0) in vec4 position;

        void main()
		{
			gl_Position = position;
		}
    )";//这意味着他不让我们使用任何弃用函数或者类似的东西
	std::string fragmentShader = R"(
        #version 330 core

        layout(location = 0) out vec4 color;

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

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

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

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

	glfwTerminate();
	return 0;
}

shader处理

创建一个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 , 1.0 , 0.0 , 1.0);
}

如果你在studio外面运行可执行文件,默认的工作目录将会是包含可执行文件的目录,所以一切都会相对于那个。但是如果我们通过visual studio调试运行这个,工作目录实际上是由visul studio调试类属性设置的。properties - debugging - working directory

写一个读写文件的函数:

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 (getline(stream, line))//一行一行地浏览文件
	{
		if (line.find("#shader") != std::string::npos)
		{
			if (line.find("vertex") != std::string::npos)
			{//set mode to vertex
				type = Shadertype::VERTEX;
			}
			else if (line.find("fragment") != std::string::npos)
			{//set mode to fragment
				type = Shadertype::FRAGMENT;
			}
		}
		else
		{
			ss[(int)type] << line << '\n';
		}
	}
	return { ss[0].str() , ss[1].str() };
}

修改main函数

ShaderProgramSource source = ParseShader("res/shaders/Shader.shader");
unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
glUseProgram(shader);

源码如下:

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

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 (getline(stream, line))//一行一行地浏览文件
	{
		if (line.find("#shader") != std::string::npos)
		{
			if (line.find("vertex") != std::string::npos)
			{//set mode to vertex
				type = Shadertype::VERTEX;
			}
			else if (line.find("fragment") != std::string::npos)
			{//set mode to fragment
				type = Shadertype::FRAGMENT;
			}
		}
		else
		{
			ss[(int)type] << line << '\n';
		}
	}
	return { ss[0].str() , ss[1].str() };
}

static unsigned int CompileShader(unsigned int type, const std::string& source)
{
	unsigned int id = glCreateShader(type);
	const char* src = source.c_str();
	glShaderSource(id, 1, &src, nullptr);
	glCompileShader(id);

	//TODO: Error handing
	int result;
	glGetShaderiv(id, GL_COMPILE_STATUS, &result);
	if (result == GL_FALSE)
	{
		int length;
		glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
		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;
}

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);
	glValidateProgram(program);//这个过程很像 C++编译中将两个不同的文件链接在一起,以便可以同时使用它们:

	glDeleteShader(vs);
	glDeleteShader(fs);//连接程序完成,可以删除shader

	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.5,
		 0.5f, 0.5,
		-0.5f, 0.5
	};

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

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

	ShaderProgramSource source = ParseShader("res/shaders/Shader.shader");
	unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);
	glUseProgram(shader);

	/* Loop until the user closes the window */
	while (!glfwWindowShouldClose(window))
	{
		/* Render here 调用glClear()函数清空颜色缓冲区,准备绘制新的帧。*/
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		glDrawArrays(GL_TRIANGLES, 0, 3);

		/* Swap front and back buffers 调用glfwSwapBuffers()函数交换前后缓冲区,这样刚才绘制的内容才会在屏幕上显示出来。*/
		glfwSwapBuffers(window);

		/* Poll for and process events 调用glfwPollEvents()函数处理窗口事件,比如鼠标、键盘等输入事件。*/
		glfwPollEvents();
	}

	glDeleteProgram(shader);

	glfwTerminate();
	return 0;
}

索引缓冲区

如果我们想要使用opengl画一个矩形,我们可以尝试这样修改代码:

float positions[12] = {
	-0.5f,-0.5,
	 0.5f, 0.5,
	-0.5f, 0.5,

	-0.5f, -0.5,
	 0.5f,-0.5,
  	 0.5f, 0.5
};//更改储存坐标的数组

修改缓冲区设置

glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);//更改缓冲区的布局

更改draw

glDrawArrays(GL_TRIANGLES, 0, 6);

用这种方式,我们创建了一个矩形,它的原理是通过生成两个三角形来生成一个矩形。但是它的缺点是会生成一些重复的顶点,性能浪费明显。
索引缓冲区
所以我们能做的就是使用一个叫做索引缓冲区的东西,这允许我们重用现有的顶点。对于矩形或者正方形而言可能还好,它看起来可能并不浪费,因为它没有太多的东西。然而当它换成游戏中的 3D 模型如宇宙飞船,每一个组成那个飞船的独立三角形会被连接到另一个三角形,这意味着你已经立马重复了至少两个顶点,每个顶点再包含法线、切线、纹理坐标的数据,那么你不得不复制整个缓冲区,它一次又一次地构成了那个实际的顶点,那是完全不现实的。

让我们来转换一下这种顶点缓冲,添加一个索引缓冲区并删除那些重复的冗余内存。

float positions[12] = {
    -0.5f,-0.5,//0
    0.5f, 0.5,//1
    -0.5f, 0.5,//2
    0.5f,-0.5,//3
};

unsigned int indices[] = {
    0 , 1 , 2,
    1 , 3 , 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);

更改drawcall:

while (!glfwWindowShouldClose(window))
	{
		/* Render here 调用glClear()函数清空颜色缓冲区,准备绘制新的帧。*/
		glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
		glClear(GL_COLOR_BUFFER_BIT);

		//glDrawArrays(GL_TRIANGLES, 0, 6);
		glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);

		/* Swap front and back buffers 调用glfwSwapBuffers()函数交换前后缓冲区,这样刚才绘制的内容才会在屏幕上显示出来。*/
		glfwSwapBuffers(window);

		/* Poll for and process events 调用glfwPollEvents()函数处理窗口事件,比如鼠标、键盘等输入事件。*/
		glfwPollEvents();
	}

opengl处理错误

在opengl我们一般有两种方式来处理错误:

一个是glGetError会返回一个整数型错误标志(错误代码)。
我修改一下之前的代码,使其不能正常运行

glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr);//回到上次渲染正方形的代码,我们可以更改索引缓冲区类型导致错误:

之后我们会无法正常渲染矩形了。

glDebugMessageCallback

在最近的 OpenGL 4.3 中添加了一个新的函数 glDebugMessageCallback()
它允许我们指定一个指向OpenGL的函数指针,当错误发生的时候opengl就会调用我们使用的那个函数。
唯一的问题在于兼容性,它只在 4.3 及以上版本,所以你不能再早期版本中使用它。优点也很明显,它不会仅仅给你一个错误码,会提供更详细的信息。根据我的经验, glDebugMessageCallback() 总体上非常好,比 glGetError() 好得多。但今天我们只讨论 glGetError()。
你可以创建一个循环调用的报错函数

OpenGL的API调用在出现错误时通常会将错误码存储在一个全局的错误状态中,而glGetError函数则是用来获取这个错误状态的值。由于在OpenGL的世界中,一个错误可能会触发多个错误码的设置,因此在某个操作之后立即调用glGetError函数并不能保证能够获取到所有的错误码。

因此,通常建议将glGetError函数放在一个循环中,反复调用直到获取到GL_NO_ERROR为止,以便获取所有的错误码。

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

接下来创建一个打印出实际错误的函数GLCheckError()

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

现在我们可以调用刚才的函数:

GLClearError();
glDrawElements(GL_TRIANGLES , 6 , GL_INT , nullptr);
GLCheckError();

首先排除其他的错误,相当于调试的断言,通过这样的方式我们可以确保所有的错误实际上都是来自这个函数。

可以看到错误代码是 1280。在源码中搜索 1280 找不到任何东西,因为 OpenGL 采用的是十六进制表示错误码。所以我们可以换为十六进制:0x0500。再返回 <glew.h> 文件检索:

#define GL_INVALID_ENUM 0x0500

500 意味着无效的枚举,而 GL_INT确实是我们实际传递的无效枚举,它应该是无符号整型。

实际上 glClearError() 和 glCheckError() 还是比较笨重,并且让扩展变得更加困难。但我们实际上可以做的就是得到实际的调试器,暂时执行并在导致错误的代码行上中断。我们可以通过使用断言来实现这一点,如果那个条件是 false,你通常要么将消息写入控制台,要么只是停止程序的执行并且在那行中断。

为此我需要修改 GLCheckError() 变为 GLLogCall()

#define ASSERT(x) if(!(x)) __debugbreak();//一个msbc函数,这个下划线让我们知道它是编译器内部的函数
//如果这是假的我就会调用一个函数它会在代码中插入断点并且打断调试器
//这是编译器的内在属性,这意味着你在这里调用的函数对于你使用的每一个编译器都是不同的
#define GLCall(x) GLClearError();\
	x;\
	ASSERT(GLLogCall(#x, __FILE__ , __LINE__))
//#x代表传入一个字符串

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

这是修改后的代码,之后我们在所有的opengl函数嵌套一个GLCall()就可以获得opengl的错误日志

opengl的统一变量

今天我们要讨论的是统一变量。

那么首先统一变量是一个非常单一的概念,它对于我们而言实际上是一种从 CPU 端获取数据的方式。在本例中是从 C++ 到我们的着色器,所以我们实际上把它当一个变量使用。

颜色变量

回到着色器我们创建一个 u_Color 并赋值:

#shader fragment

#version 330 core

layout(location = 0) out vec4 color;

uniform vec4 u_Color;

void main()
{
	color = u_Color;
}

每个统一变量都有一个 id,这样我们就可以引用它了。而我们查找 id 的方式通常是通过它的名称,所以我们基本上就是问我们的着色器 u_Color 变量的位置。

在更现代的 OpenGL 版本,你实际上可以设置和索引。所以从 4.3 开始你可以指定一个明确的统一变量位置,这是一种非常现代的新功能。

...
ShaderProgramSource source = ParseShader("res/shaders/Shader.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));

最后结果是生成了一个蓝色的矩形
现在我们可以干一些更加有趣的事情,比如生成一个颜色会发生变化的矩形

float r = 0.0f;
float increment = 0.05f;
/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
	/* Render here 调用glClear()函数清空颜色缓冲区,准备绘制新的帧。*/
	GLCall(glClearColor(0.2f, 0.3f, 0.3f, 1.0f));
	GLCall(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()函数交换前后缓冲区,这样刚才绘制的内容才会在屏幕上显示出来。*/
	glfwSwapBuffers(window);

	/* Poll for and process events 调用glfwPollEvents()函数处理窗口事件,比如鼠标、键盘等输入事件。*/
	glfwPollEvents();
}

opengl的顶点数组

顶点数组

我们讲了很多 OpenGL 的基本概念甚至一般的图形编程,但 OpenGL 实际上有一个顶点数组。乍一看你可能会说顶点数组、顶点缓冲区它们之间的区别是什么,它们听起来非常相似。确实如此,并且这并不是 DirectX 等其他渲染接口中真正存在的东西,它是 OpenGL 独有的,也可以说是 OpenGL 的一个原始接口。它们基本上是一种通过特定的规范绑定顶点缓冲区的方式,用于实际顶点缓冲区的布局。

在我们的代码中,我们创建了 buffer 包含所有的顶点数据,然后创建缓冲区之后也做了绑定,启用了顶点属性指定实际数据的布局。现在一个顶点数组对象允许我们通过 glVertexAttribArray() 绑定指定的顶点规范到实际的顶点缓冲区,可能对于 OpenGL 的初学者比较难以理解,如果屏幕上有多个对象、多个网格、多个顶点缓冲区,需要我们绑定顶点和索引缓冲区,然后绘制实际的对象。

但我们绑定顶点缓冲区之后,我们实际也需要指定布局,让我们看看解绑一切会发生什么:

ASSERT(location != -1);
	GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));

	float r = 0.0f;
	float increment = 0.05f;
	GLCall(glUseProgram(0));//+
	GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));//+
	GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));//+解绑所有的

	GLCall(glPolygonMode(GL_FRONT_AND_BACK, GL_LINE));//线框模式

	/* Loop until the user closes the window */
	while (!glfwWindowShouldClose(window))
	{
		/* Render here*/
		GLCall(glClearColor(0.2f, 0.3f, 0.3f, 1.0f));
		GLCall(glClear(GL_COLOR_BUFFER_BIT));

		GLCall(glUseProgram(shader));
		GLCall(glUniform4f(location, r, 0.3f, 0.8f, 1.0f));

		GLCall(glBindBuffer(GL_ARRAY_BUFFER, buffer));//+
		GLCall(glEnableVertexAttribArray(0));//+
		GLCall(glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0));//+

		GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo));//+

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

这里我基本上解绑了所有的东西,到了绘制的时候我们需要实际上绑定我们需要的所有东西,让 DrawCall 工作以此正确渲染所有东西。

我们绑定着色器,设置统一变量,绑定顶点缓冲区,设置顶点缓冲区的布局,最后绑定索引缓冲区调用 glDrawElements()。运行这段代码会得到了和之前一样的结果,完美。这里值得商榷的是这里:

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

我们是否每次都要这样做?答案是肯定的,因为如果我们用不同的布局绘制另一个对象,它们可能已经改变了。所以顶点数组对象实际上就是包含这种状态的对象,因此如果我们正确地利用顶点数组对象例如为几何体的每个部分创建不同的顶点数组对象,然后只需要绑定顶点数组对象就完事儿了,因为顶点数组对象将包含顶点缓冲区之间的绑定、布局。

因此,我们的绘制方式从绑定我们的着色器、绑定我们的顶点缓冲区、设置顶点布局、绑定我们的索引缓冲区、然后发出实际的 DrawCall 指令变为了绑定我们的着色器、绑定顶点数组、绑定索引缓冲区、最终发出实际的 DrawCall 指令。所以绑定顶点缓冲区并设置其布局变为了绑定顶点数组对象,因为它包含了我们实际需要的所有状态。

我需要在这里提一件事情,从技术上讲顶点数组对象是必须的,它们现在正在被使用,这就是为什么我说即使我们没有创建它们这个状态仍由顶点数组对象保持。这个东西是 OpenGL 兼容性配置文件,默认情况下兼容性配置文件实际上为我们创建了一个顶点数组对象。

然而,核心配置文件没有。所以我们实际上需要自己显式地创建一个 OpenGL 顶点数组对象,绑定它确保一切正常。如果我们正在使用核心配置文件,需要手动处理

int main(void)
{
	GLFWwindow* window;

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

	glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//+
	glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//+前两行确定opengl主次版本为3.3
	glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//+
	//后一行则设置我的 OpenGL 配置为核心配置文件 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;
	}

但是我们这样设置了之后程序又突然无法运行了。

当我们尝试启用这个 vertexAttribArray 的时候没有绑定顶点数组对象,因此我们无法指定顶点属性类型的规范或者 enableVertexAttribArray。那么我们需要做的就是在核心配置文件中实际创建那个 VAO,也就是顶点数组对象:

unsigned int indices[] = {
	0, 1, 2,
	2, 3, 0
};

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

unsigned int buffer;

再次运行程序不再报错,这就是我们显式地创建一个 vao。有意思的是我们甚至可以删掉部分绑定的代码,程序依然可运行:

GLCall(glBindVertexArray(0));//+
GLCall(glUseProgram(0));
GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));
GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));

GLCall(glPolygonMode(GL_FRONT_AND_BACK, GL_LINE));//线框模式

/* Loop until the user closes the window */
while (!glfwWindowShouldClose(window))
{
	/* Render here */
	GLCall(glClearColor(0.2f, 0.3f, 0.3f, 1.0f));
	GLCall(glClear(GL_COLOR_BUFFER_BIT));

	GLCall(glUseProgram(shader));
	GLCall(glUniform4f(location, r, 0.3f, 0.8f, 1.0f));

	GLCall(glBindVertexArray(vao));//+

	GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo));

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

cpp与抽象

抽象顶点和索引缓冲区为类

顶点缓冲区.h

#pragma once

class VertexBuffer
{
private:
	unsigned int m_RendererID;

public:
	VertexBuffer(const void* data, unsigned int size);
	~VertexBuffer();

	void Bind() const;
	void Unbind() const;
};

cpp文件:

#include "VertexBuffer.h"
#include "Renderer.h"

VertexBuffer::VertexBuffer(const void* data, unsigned size)
{
	GLCall(glGenBuffers(1, &m_RendererID));
	GLCall(glBindBuffer(GL_ARRAY_BUFFER, m_RendererID));
	GLCall(glBufferData(GL_ARRAY_BUFFER, size, data, GL_STATIC_DRAW));
}

VertexBuffer::~VertexBuffer()
{
	GLCall(glDeleteBuffers(1, &m_RendererID));
}

void VertexBuffer::Bind() const
{
	GLCall(glBindBuffer(GL_ARRAY_BUFFER, m_RendererID));
}
void VertexBuffer::Unbind() const
{
	GLCall(glBindBuffer(GL_ARRAY_BUFFER, 0));
}

索引缓冲区.h

#pragma once

class IndexBuffer
{
private:
	unsigned int m_RendererID;
	unsigned int m_Count;
public:
	IndexBuffer(const unsigned int* data, unsigned int count);
	~IndexBuffer();

	void Bind() const;
	void Unbind() const;

	inline unsigned int GetCount() const { return m_Count; }
};

cpp文件:

#include "IndexBuffer.h"

#include "Renderer.h"

IndexBuffer::IndexBuffer(const unsigned int* data, unsigned int count) 
	: m_Count(count)
{
	ASSERT(sizeof(unsigned int) == sizeof(GLuint))//与跨平台有关

	GLCall(glGenBuffers(1, &m_RendererID));
	GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID));
	GLCall(glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(unsigned int), data, GL_STATIC_DRAW));
}

IndexBuffer::~IndexBuffer()
{
	GLCall(glDeleteBuffers(1, &m_RendererID));
}

void IndexBuffer::Bind() const
{
	GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_RendererID));
}
void IndexBuffer::Unbind() const
{
	GLCall(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
}

抽象顶点数组

首先我们抽象顶点数组的目的是什么?

对我们来说,顶点数组需要做的是将顶点缓冲区与某种布局联系在一起,所以顶点缓冲区就是有数据的缓冲区,它们没有实际的概念比如前三个浮点数是位置,没有类型或者大小之类的概念,它只是实际数据的普通缓冲区。每个字节是什么、这些顶点有多大等等才是顶点数组真正代表的,它应该把缓冲区和实际布局联系在一起。

顶点数组对象是 OpenGL 存储那种状态的方式,那么当我们考虑创建这个接口时,我们需要做的是需要一些东西来创建一个顶点数组。

下面是头文件VertexArray.h

#pragma once

#include "VertexBuffer.h"
#include "VertexBufferLayout.h"

class VertexArray
{
private:
	unsigned int m_RendererID;
public:
	VertexArray();
	~VertexArray();

	void Bind() const;
	void Unbind() const;

	void AddBuffer(const VertexBuffer& vb, const VertexBufferLayout& layout);
};

cpp文件

#pragma once

#include "VertexArray.h"
#include "Renderer.h"

VertexArray::VertexArray()
{
	GLCall(glGenVertexArrays(1, &m_RendererID));
}

VertexArray::~VertexArray()
{
	GLCall(glDeleteVertexArrays(1, &m_RendererID));
}

void VertexArray::Bind() const
{
	GLCall(glBindVertexArray(m_RendererID));
}

void VertexArray::Unbind() const
{
	GLCall(glBindVertexArray(0));
}

void VertexArray::AddBuffer(const VertexBuffer& vb, const VertexBufferLayout& layout)
{
	Bind();
	vb.Bind();

	unsigned int offset = 0;
	const auto& elements = layout.GetElements();
	for (unsigned int i = 0; i < elements.size(); i++)
	{
		const auto& element = elements[i];
		
		GLCall(glEnableVertexAttribArray(i));
		GLCall(glVertexAttribPointer(i, element.count, element.type, element.normalized, layout.GetStride(), (const void*)offset));

		offset += element.count * VertexBufferElement::GetSizeOfType(element.type);
	}
}

layout.h

#pragma once

#include <vector>
#include <GL/glew.h>

#include "Renderer.h"

struct VertexBufferElement
{
	unsigned int type;
	unsigned int count;
	unsigned int normalized;

	static unsigned int GetSizeOfType(unsigned int type)
	{
		switch (type)
		{
		case GL_FLOAT: return 4;
		case GL_UNSIGNED_INT: return 4;
		case GL_UNSIGNED_BYTE: return 1;
		}

		//ASSERT(false);
		return 0;
	}
};

class VertexBufferLayout
{
private:
	unsigned int m_Stride;
	std::vector<VertexBufferElement> m_Elements;

public:
	VertexBufferLayout() : m_Stride(0) {}

	template<typename T>
	void Push(unsigned int count)
	{
		// static_assert(false);
	}
	template<>
	void Push<float>(unsigned int count)
	{
		m_Elements.push_back({ GL_FLOAT , count , GL_FALSE });
		m_Stride += count * VertexBufferElement::GetSizeOfType(GL_FLOAT);
	}
	template<>
	void Push<unsigned int>(unsigned int count)
	{
		m_Elements.push_back({ GL_UNSIGNED_INT , count , GL_FALSE });
		m_Stride += count * VertexBufferElement::GetSizeOfType(GL_UNSIGNED_INT);
	}
	template<>
	void Push<unsigned char>(unsigned int count)
	{
		m_Elements.push_back({ GL_UNSIGNED_BYTE , count , GL_TRUE });
		m_Stride += count * VertexBufferElement::GetSizeOfType(GL_UNSIGNED_BYTE);
	}

	inline unsigned int GetStride() const { return m_Stride; }
	inline std::vector<VertexBufferElement> GetElements() const { return m_Elements; }
};

抽象着色器

头文件:

#pragma once

#include <string>
#include <unordered_map>

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

class Shader
{
private:
	std::string m_FilePath;
	unsigned int m_RendererID;
	std::unordered_map<std::string, int> m_UniformLocationCache;
public:
	Shader(const std::string& filepath);
	~Shader();

	void Bind() const;
	void Unbind() const;

	void SetUniform4f(const std::string& name, float v0, float v1, float v4, float v3) const;

private:
	unsigned int GetUniformLocation(const std::string& name) const;
	ShaderProgramSource ParseShader(const std::string& filepath) const;
	unsigned int CompileShader(unsigned int type, const std::string& source); 
	unsigned int CreateShader(const std::string& vertexShader, const std::string& fragmentShader);
};

cpp文件:

#pragma once

#include<iostream>
#include<fstream>
#include<sstream>

#include "Shader.h"
#include "Renderer.h"

Shader::Shader(const std::string& filepath) : m_FilePath(filepath) , m_RendererID(0)
{
	const ShaderProgramSource source = ParseShader(filepath);
	m_RendererID = CreateShader(source.VertexSource, source.FragmentSource);
}

Shader::~Shader()
{
	GLCall(glDeleteProgram(m_RendererID));
}

void Shader::Bind() const
{
	GLCall(glUseProgram(m_RendererID));
}

void Shader::Unbind() const
{
	GLCall(glUseProgram(0));
}

void Shader::SetUniform4f(const std::string& name, float v0, float v1, float v2, float v3) const
{
	GLCall(glUniform4f(GetUniformLocation(name), v0, v1, v2, v3));
}

unsigned int Shader::GetUniformLocation(const std::string& name) const
{
	auto it = m_UniformLocationCache.find(name);
	if (it != m_UniformLocationCache.end())
		return it -> second;

	GLCall(const int location = glGetUniformLocation(m_RendererID, name.c_str()));
	if (location == -1)
	{
		std::cout << "Warning:uniform '" << name << "' doesn't exist!" << std::endl;
	}
	return location;
}

ShaderProgramSource Shader::ParseShader(const std::string& filepath) const
{
	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 (getline(stream, line))//一行一行地浏览文件
	{
		if (line.find("#shader") != std::string::npos)
		{
			if (line.find("vertex") != std::string::npos)
			{//set mode to vertex
				type = Shadertype::VERTEX;
			}
			else if (line.find("fragment") != std::string::npos)
			{//set mode to fragment
				type = Shadertype::FRAGMENT;
			}
		}
		else
		{
			ss[(int)type] << line << '\n';
		}
	}
	return { ss[0].str() , ss[1].str() };
}

unsigned int Shader::CompileShader(unsigned int type, const std::string& source)
{
	unsigned int id = glCreateShader(type);
	const char* src = source.c_str();
	glShaderSource(id, 1, &src, nullptr);
	glCompileShader(id);

	int result;
	glGetShaderiv(id, GL_COMPILE_STATUS, &result);
	if (result == GL_FALSE)
	{
		int length;
		glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
		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;
}

unsigned int Shader::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);
	glValidateProgram(program);

	glDeleteShader(vs);
	glDeleteShader(fs);

	return program;
}

抽象渲染器

renderer.h

#pragma once

#include <GL/glew.h>

#include "VertexArray.h"
#include "IndexBuffer.h"
#include "Shader.h"

#define  ASSERT(x) if (!(x))   __debugbreak();

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

void GLClearError();
bool GLLogCall(const char* function, const char* file, int line);

class VertexArray;
class IndexBuffer;
class Shader;

class Renderer
{
public:
	void Clear() const;
	void Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader ,const float& timeValue) const;
};

cpp文件:

#include "Renderer.h"
#include <iostream>//处理类抽象

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

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

void Renderer::Clear() const
{
	GLCall(glClear(GL_COLOR_BUFFER_BIT));
}

void Renderer::Draw(const VertexArray& va, const IndexBuffer& ib, const Shader& shader , const float& timeValue) const
{
	shader.Bind(); 
	float dv1 = sin(timeValue) / 2.0f + 0.5f;
	float dv2 = sin(timeValue + 1.57) / 2.0f + 0.5f;
	float dv3 = sin(timeValue + 3.14) / 2.0f + 0.5f;
	std::cout << dv1 << std::endl;
	shader.SetUniform4f("dy", dv1, dv2, dv3, 1.0f);

	va.Bind();
	ib.Bind();

	GLCall(glDrawElements(GL_TRIANGLES, ib.GetCount(), GL_UNSIGNED_INT, nullptr));
}

应用这些类

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

#include "Renderer.h"
#include "VertexBuffer.h"
#include "IndexBuffer.h"
#include "VertexArray.h"
#include "VertexBufferLayout.h"
#include "Shader.h"

int main(void)
{
	GLFWwindow* window;

	if (!glfwInit())
		return -1;

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

	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.5, 0.0 ,   0.75 , 0.0 , 0.0  , //  1.0f , 1.0f , //0
		 0.5f, 0.5, 0.0 ,   0.25 , 0.5 , 0.0  ,  // 1.0f , 0.0f ,//1
		-0.5f, 0.5, 0.0 ,   0.0  , 0.5 , 0.25 , //  0.0f , 0.0f ,//2
		 0.5f,-0.5, 0.0 ,   0.0  , 0.0 , 0.75 ,  // 0.0f , 1.0f //3
	};

	unsigned int indices[] = {
		0 , 1 , 2,
		1 , 3 , 0
	};

	{

		VertexArray va;
		VertexBuffer vb(positions, 4 * 6 * sizeof(float));

		VertexBufferLayout layout;
		layout.Push<float>(3);
		va.AddBuffer(vb, layout);
		layout.Push<float>(3);
		va.AddBuffer(vb, layout);

		IndexBuffer ib(indices, 6);

		Shader shader("res/shader/Shader.shader");
		shader.Bind();

		Renderer renderer;

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

		va.Bind();
		shader.Unbind();
		vb.Unbind();

		while (!glfwWindowShouldClose(window))
		{
			GLCall(glClearColor(0.2f, 0.3f, 0.3f, 1.0f));

			renderer.Clear();

			renderer.Draw(va, ib, shader, static_cast<float>(glfwGetTime()));

			glfwSwapBuffers(window);

			glfwPollEvents();
		}
	}
	glfwTerminate();
	return 0;
}

代码简洁了很多!

纹理

slot 就是绑定纹理的插槽。在 OpenGL 我们有各种各样的插槽可以绑定纹理,Windows 上经典的现代显卡会有 32 个纹理插槽,而在诸如安卓等移动设备上可能有八个插槽,这取决于你们的实际显卡以及它们的 OpenGL 实现。