引言

这是 TheCherno 的 OpenGL 系列教程。关于这个系列,我在 GitHub 和码云上自建了源码仓库,如果有需要的朋友可以去看看。

为了更好的阅读体验,请访问:Cherno OpenGL 教程

欢迎来到OpenGL

在这个视频中,主要包括 OpenGL 实际是什么、怎么使用它以及它能为我们做什么和它不是什么等等,这个系列也是如此。

什么是OpenGL

首先如果你听过 OpenGL 但不确定它是什么,你可能想到与图形有关的,那就是 OpenGL。OpenGL 是一种图形接口,现在 API 代表应用程序接口,基本上就是一大堆我们能够调用的函数去做一些事情。在这种情况下,由于 OpenGL 是一种图形 API,它允许我们做一些与图形相关的事情,特别的是它允许我们访问 GPU 也就是显卡,图形处理单元(Graphics Processing Unit)。

调用显卡也更好地绘制图形,所以实际上为了利用电脑或其他设备(比如手机)中强大的图形处理器,需要调用一些 API 访问固件。OpenGL 正好是允许访问和操作 GPU 的许多接口中的一种,当然我们也有一些其他的接口,比如 Direct3D、Vulcan 和 Metal 等等,所以某种角度来说 OpenGL 允许我们控制显卡。

关于OpenGL误解

让我们来澄清一些人关于 OpenGL 的误解。

首先,许多人称它为一个类库或一种引擎或一些其他的框架,但这些都不是。OpenGL 核心本身只是一种规范,和 CPP 规范差不多。实际上它没有确定任何代码和类似的事情本身就是规范,比如“嘿,这个函数应该存在,需要这些参数并且返回这个值”,它只是一种你能利用这种 API 做什么规范,没有任何具体的实现,这意味它绝不是一个类库,因为 OpenGL 本身没有代码,它只是一种规范。

去哪儿下载 OpenGL 是另一个常见的问题。

然而,答案是你不需要真的去下载 OpenGL,它本身只是一种规范。那么,谁去实现它呢,谁去为你调用的 OpenGL 函数写代码呢?这个答案是 GPU 制造商,所以如果你使用的是 NVIDIA 显卡,那么你的显卡驱动(也就是 NVIDIA 驱动)实际上包含了 OpenGL 的实现,每个显卡制造商比如 AMD、Intel 等都会有它们自己的实现,每家关于 OpenGL 的实现都会有些不同,这也就是有些游戏能在 NVIDIA 驱动的显卡上运行但在一个 AMD 电视或者其他显卡设备上运行有些区别、甚至会出现问题的原因。

但不管怎么说,关键在于你的显卡制造商实现的 OpenGL,这又可能导致下一个有关 OpenGL 常见的误解:它是开源的。我不知道人们为什么会这么觉得,也许是名字中带着“Open”,但是它根本不是开源的,你看不到 OpenGL 的源码,因为首先它是由 GPU 制造商实现的,它们肯定不会发布它们的驱动源码。

OpenGL 提供了什么能激发和导致许多人不幸地说 OpenGL 是无与伦比的?原因在于它是跨平台的,所以你的 OpenGL 代码可以在 Windows、Mac、Linux 和 Android 上正常执行,以至于人们立马就会意识到 OpenGL 比 Direct3D 更优越,因为它能在所有平台上运行,但是请不要这么说。

从我 EA 技术中心并处理过许多引擎中图形接口的经验来看,因为 OpenGL 是跨平台的 API,而制作一款游戏不会只涉及实现一个独立的图形 API。如果游戏引擎是跨平台的,那意味着它不仅实现了 Xbox,也包括其他一些平台,它们不得不实现大量其他的图形接口。

因此,我们面临的问题是图形 API 是为特定平台设计的。例如 Direct3D 是微软为 Windows 设计的,它在 Windows 上的表现要比跨平台的 API 好些。现在请记住,实际编写这些代码的人不是微软,即使微软的确为了更好的代码质量和 GPU 制造商合作过。所以关于 API 的比较是没有任何意义的,因为通常平台原生的东西会更健壮更友好。

就 OpenGL 的复杂性而言,它可以说是现在可以学习的最简单的 API 了,所以 OpenGL 是绝对值得学习的。Vulkan 是另一个跨平台的 API,但它更底层更严谨,不适合初学者直接入门,老实说我并不想使用 Vulkan 去开发游戏,OpenGL 更加稳定。

传统与现代OpenGL

另外本系列学习的主要是现代 OpenGL。OpenGL 于 90 年代发布,那时的情况和现在大不相同,那时的 GPU 是可编程的,十分灵活,制造商给了程序员和开发者更多的控制权。

传统 OpenGL 和现代 OpenGL 之间最大的区别就是着色器。如果你对图形学感兴趣的话可能听说过着色器,它可能有点像 shadow 这个单词或者是光源,有些人将它和光源或其他比较。抛开这些,着色器是程序,它是在你 GPU 上运行的代码,这就是着色器,它是在你 GPU 上运行的一段程序。。

那么如果你用 C++、Java 或 C# 或不管什么语言写代码,这段代码都会运行在你的 CPU 上。但当我们开始处理图形的大部分时间里,我们想要更为精确的控制显卡运行,可能要将大部分代码从 CPU 转到 GPU 上,因为它在 GPU 上运行更快,这就是着色器存在的意义:允许我们在 GPU 上运行代码。所以可编程的着色器是最大的区别。

设置OpenGL和C++中创建一个窗口

上一节我们只说了 OpenGL 到底是什么以及这个系列实际会讲什么。今天我们的任务是让我的操作系统为我创造一个窗口,在未来我们可能会在窗口内绘制图形。

我们会使用一个向我提供窗口创建和窗口管理的实际代码类库,不管 Windows、Mac 还是 Linux。GLFW 就是满足上述条件的类库,我喜欢这个类库的原因是它确实是一个轻量级类库,它虽然不如 SDL 那么全面(实际上它就是个渲染器),但依然可以创建窗口、OpenGL context 以及给我们访问一些类似输入之类的基础东西。

下载类库

由于我使用的是 Mac,所以使用 brew 下载:

brew install glew
brew install glfw

下载后默认的路径为 /usr/local/Cellar/

配置CMake文件

我将项目名称设置为 OpenGL,在 CLion 中配置:

cmake_minimum_required(VERSION 3.16)
project(OpenGL)set(CMAKE_CXX_STANDARD 17)# Add head file
set(GLEW_H /usr/local/Cellar/glew/2.2.0_1/include/GL)
set(GLFW_H /usr/local/Cellar/glfw/3.3.7/include/GLFW)
include_directories(${GLEW_H} ${GLFW_H})# Add target link
set(GLEW_LINK /usr/local/Cellar/glew/2.2.0_1/lib/libGLEW.2.2.dylib)
set(GLFW_LINK /usr/local/Cellar/glfw/3.3.7/lib/libglfw.3.dylib)
link_libraries(${OPENGL} ${GLEW_LINK} ${GLFW_LINK})set(SOURCE_FILES main.cpp)
add_executable(OpenGL ${SOURCE_FILES})if (APPLE)target_link_libraries(OpenGL "-framework OpenGL")target_link_libraries(OpenGL "-framework GLUT")
endif()

编译项目

复制下面内容到 main.cpp

#include <glew.h>
#include <glfw3.h>
#include <iostream>
using namespace std;int main()
{/* Initialize the library */if(!glfwInit())return -1;/* Create a Windowed mode and its OpenGL context */GLFWwindow* 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);/* Swap front and back buffers */glfwSwapBuffers(window);/* Poll for and events */glfwPollEvents();}glfwTerminate();return 0;
}

运行代码,我们会得到一个黑色的窗口。

画一个三角形

下面我们用传统 OpenGL 设定三个顶点,绘制一个三角形:

#include <glew.h>
#include <glfw3.h>
#include <iostream>
using namespace std;int main()
{/* Initialize the library */if(!glfwInit())return -1;/* Create a Windowed mode and its OpenGL context */GLFWwindow* 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 events */glfwPollEvents();}glfwTerminate();return 0;
}

在C++中使用现代OpenGL

顶点缓冲区和在现代OpenGL中画一个三角形

现代 OpenGL 比那种传统 OpenGL 更具可编程性,它的扩展性更好也更加强大,你可以用它做许多事情,但代价是在绘制一个简单三角形前,我们实际上需要做很多设置。而前面我们已经用传统方法简单绘制了三角形,非常简单并且不需要什么设置。

让我们聊聊需要什么东西才可以绘制一个三角形。对于现代 OpenGL 而言,首先我们需要能够创建一个顶点缓冲区,然后还要创建一个着色器(之后可能会单独讲一下图形渲染管线是如何运行的,特别是 OpenGL)。让我们快速地说一下这两个东西是什么吧。

顶点缓冲区与着色器

顶点缓冲区基本上就是去掉 vertex,它只是一个内存缓冲区,一个内存字节数组,从字面上讲就是一块用来存字节的内存。但是顶点缓冲区又和 C++ 中像字符数组的内存缓冲区不太一样,它是 OpenG 中的内存缓冲区,这意味着它实际上在显卡显存(Video RAM)上。

所以这里的基本思路就是我要定义一些数据来表示三角形,我要把它放入显卡的 VRAM 中,然后还需要发出 DrawCall 绘制指令。实际上我们还需要告诉显卡如何读取和解释这些数据,以及如何把它放到我们屏幕上,一旦我们发出 DrawCall 指令,我们需要告诉显卡:好了,一旦你在显卡端获得了这些数据,我要你像这样把它摆出来,我希望你把它画出来在屏幕上给我显示一个三角形。所以我们需要告诉显卡怎么做,需要对显卡编程,这就是着色器,着色器只是一个运行在显卡上的程序,它是一堆我们可以编写的在显卡上以一种非常特殊的方式运行的代码。

听起来很复杂,但是不要想太多。简而言之我们有一个可以指定的内存,还有一些我可以指定的数据,告诉显卡:嘿,这是数据。然后从显卡那边说:好了,现在读一下这些数据并解释一下(例如屏幕上的位置),可能的话把它们连成一个三角形。这就是整件事的原理,也是 OpenGL 渲染的流程。

要注意 OpenGL 是作为一个状态机来运行的,这意味着你不必把它当作一个对象或任何类似的东西来对待,你所做的是设置一系列的状态,然后当你说一些事比如给我画一个三角形,这是非常 contextual 的。我的意思是,我不只是说,嘿,给我画一个三角形,然后传递 OpenGL 需要绘制三角形的所有东西。实际上它已经知道画三角形需要什么了,因为那是状态的一部分。

代码实践

让我们来创建这个顶点缓冲区来看看能做些什么。

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

这是之前使用传统 OpenGL 绘制三角形的代码。现代 OpenGL 需要创建顶点缓冲区,发送到 OpenGL 显存,然后发出一个 DrawCall 指令说:嘿,请画出我的缓冲区。

首先我们要创建自己的缓冲区,这个过程非常简单,只需要调用 glGenBuffers()

unsigned int buffer;
glGenBuffers(1, &buffer);

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

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

glBindBuffer(GL_ARRAY_BUFFER, buffer);

下一步是指定数据。一个简单的方式是在声明数据的时候直接把顶点数据填充进去:

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

接着调用 glBufferData() ,查阅文档:


glBufferData(GL_ARRAY_BUFFER, 6 * sizeof(float), positions, GL_STATIC_DRAW);

很好,现在我们为 OpenGL 指定了 positions,通常还需要创建一个索引缓冲区,也都是后话了。不过我们还没有着色器,去解释如何用这些数据进行绘制,我们将在下一期讨论那个问题。今天我们要做的只是想看看能不能把它绘制出来。

在没有索引缓冲区的情况下我们可以调用 glDrawArrays() 绘制指定图元:

/* 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();+glDrawArrays(GL_TRIANGLES, 0, 3);

我喜欢用 Photoshop 打比方。如果我选择一个图层,然后在该图层上用画笔绘制一些东西,它只会影响这一图层。然而如果我没有选择任何东西或者选错了东西,它不会影响到我想绘制的那个图层。OpenGL 也是一样,在使用它之前你需要选择或绑定所有东西,因为这就是它的运行原理,它是上下文相关的,它是一个状态机。

OpenGL中顶点的属性和布局

上一节我们讨论了顶点缓冲区,但实际上有一些部分我们忽略了:顶点属性和着色器。今天我们将学习顶点属性。

顶点属性

所以什么是顶点属性?OpenGL 渲染管线的工作原理是我们为我们的显卡提供数据,我们在显卡上存储一些内存,它包含了我们想要绘制的所有数据;然后我们使用一个着色器在显卡上读取数据,并且完全显示在屏幕上。

通常我们绘制几何图元的方式就是使用一个叫顶点缓冲区的东西,也就是一个存储在显卡上的内存缓冲区,所以当对着色器编程时实际上是从读取顶点缓冲区开始的,它需要知道缓冲区的布局,这个缓冲区包含的浮点数指定了每个顶点的位置、纹理坐标、法线之类的。

代码实践

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

在我继续之前想先定义顶点是什么意思,因为人们在错误的使用这个词。

顶点与位置无关,顶点就是几何图形上的一个点。大多数人在视觉上对它们的明显印象是通过它的位置,所以如果我给你画一个三角形,你会看到三个顶点。但顶点不是位置,一个顶点可以不仅仅包含一个位置,顶点可以包含位置更多的数据例如纹理坐标、法线、颜色等等,它们可能都在一个顶点。所以我们需要告诉 OpenGL 外面的数据布局,调用 glVertexAttribPointer() 函数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZAH98jP2-1653724618203)(https://cdn.jsdelivr.net/gh/Yousazoe/picgo-repo/img/image-20220516230107887.png)]

  • index:Specifies the index of the generic vertex attribute to be modified.

    基本上我们的着色器读取所有这些的方式是通过索引。一般来说如果我们有一个位置在索引 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.

    这里是我们指定数据类型 type,在本例中则是位置的浮点类型 GL_FLOAT

  • normalized:For glVertexAttribPointer, specifies whether fixed-point data values should be normalized (GL_TRUE) or converted directly as fixed-point values (GL_FALSE) when they are accessed.

    标准化其实不用太担心,如果我们处理的是浮点数,因为它们已经被规范化了。假设我们要指定一个颜色字节在 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 的值。

最后别忘了调用 glEnableVertexAttribArray() 去启用 glVertexAttribPointer():

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

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

#include <glew.h>
#include <glfw3.h>
#include <iostream>
using namespace std;int main()
{/* Initialize the library */if(!glfwInit())return -1;/* Create a Windowed mode and its OpenGL context */GLFWwindow* 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);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 events */glfwPollEvents();}glfwTerminate();return 0;
}

OpenGL中着色器的原理

今天我们讨论的都是关于着色器。

着色器

每个新手脑海里的第一个问题是什么是着色器?

着色器就是一个运行在显卡上的程序代码,它是我们可以在计算机上以文本或者字符串形式编写的代码,然后我们可以把它给 OpenGL 发到显卡上像其他程序一样编译链接,最后运行它,不同的是它是在我们的显卡上运行而不是 C++ 那样在我们的 CPU 上。那么为什么我们需要程序完全在显卡上运行呢?

究其原因是我们希望能够告诉显卡该做些什么,显卡处理图形的速度要快得多,我们想要利用显卡的能力在屏幕上绘制图形。这并不意味着所有的工作必须在显卡上完成,CPU 有自己擅长的部分,也许之后我们可以将结果数据发送给显卡同时仍然在 CPU 上进行处理。

着色器类型

对于大多数图形编程,我们会把重点放在两种着色器:顶点着色器和片段着色器(又称像素着色器)。

虽然还没有正式讲过标准图形渲染管线,但是我们应该对它大致是如何工作的应该了解:我们在 CPU 上写了一堆数据,向显卡发送这些数据并且发出一个叫做 DrawCall 指令的东西,也绑定了某些状态,最后我们进入了着色器阶段,GPU 实际处理 DrawCall 指令并在屏幕上绘制一些东西。这个特定的过程基本上就是渲染管道,我们如何在屏幕上从数据到结果的。

现在当显卡开始绘制三角形时,着色器就派上用场了。顶点着色器和片段着色器是渲染管线两种不同的着色器类型,所以当我们真正发出 DrawCall 指令时,顶点着色器会被调用,然后片段着色器会被调用,最后我们会在屏幕上看到结果。

顶点着色器

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

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

片段着色器

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

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

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

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

话虽如此,有些东西显然需要按像素计算例如光源。如果你在计算光源,每个像素都有一个颜色值,这个值是由很多东西决定:光源、环境、纹理、提供给表面的材质…所有这些一起来确定一个特定像素的正确颜色。显然这取决于一些输入,例如相机的位置在哪里,而这些所有的东西结束后你在片段着色器中的决定仅仅是单个像素的颜色,这就是片段着色器的作用。

OpenGL中写一个着色器

让我们开始写一些着色器代码吧。

准备工作

首先创建一个 CreateShader() 创建着色器函数,传入两个着色器字符串,这些字符串都是实际的代码:

static int CreateShader(const std::string& vertexShader, const std::string& fragmentShader)
{}

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

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

unsigned int program = glCreateProgram();

接下来我们需要做的是创建我们的两个着色器对象:

unsigned int vs = glCreateShader(GL_VERTEX_SHADER);

考虑到这个工作在今后的复用性,可以另创建一个函数 CompileShader()

static unsigned int CompileShader(unsigned int type, const std::string& source)
{unsigned int id = glCreateShader(type);
}

然后可以加一个初始化函数,查找字符串中的第一个字符然后返回它的内存地址:

// const char* src = &source[0];
const char* src = source.c_str();

最后就是调用 glShaderSource()glCompileShader()

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

回到 CreateShader()

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

这个过程很像 C++编译中将两个不同的文件链接在一起,以便可以同时使用它们:

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

最后我们实际上现在可以删了我们的着色器,因为它们已经被链接到一个程序中,所以我们可以删除这些中间文件:

glDeleteShader(vs);
glDeleteShader(fs);

最终我们的函数源码:

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);glDeleteShader(vs);glDeleteShader(fs);return program;
}

错误处理

让我们快速做一下这个错误处理。

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

编译着色器实际上不会返回任何东西,所以如果任何东西出错我们无法检索找出问题。但我们可以通过调用 glGetShaderiv() 实现:

int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);

判断 result,检索错误信息的长度并输出信息:

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 handlingint 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 complie " << (type == GL_VERTEX_SHADER ? "vertex" : "fragment") << "shader!" << std::endl;std::cout << message << std::endl;glDeleteShader(id);return 0;}return id;
}

着色器编写

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

    std::string vertexShader = R"(#version 330 corelayout(location = 0) in vec4 position;void main(){gl_Position = position;})";std::string fragmentShader = R"(#version 330 corelayout(location = 0) out vec4 color;void main(){color = vec4(1.0, 0.0, 0.0, 1.0);})";

完整代码如下:

#include <GL/glew.h>
#include <GLFW/glfw3.h>#include <iostream>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);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);glDeleteShader(vs);glDeleteShader(fs);return program;
}int main(void)
{GLFWwindow* window;if (!glfwInit())return -1;window = glfwCreateWindow(640, 480, "Window", NULL, NULL);if (!window){glfwTerminate();return -1;}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);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""   glPosition = position;\n""}\n";std::string fragmentShader ="#version 450 core\n""\n""layout(location = 0) out vec4 colour;\n""\n""void main()\n""{\n""   colour = vec4(1.0, 0.0, 0.0, 1.0);\n""}\n";unsigned int shader = CreateShader(vertexShader, fragmentShader);glUseProgram(shader);while (!glfwWindowShouldClose(window)){glClear(GL_COLOR_BUFFER_BIT);glDrawArrays(GL_TRIANGLES, 0, 3);glfwSwapBuffers(window);glfwPollEvents();}glfwTerminate();return 0;
}

整理错误

Failed to compile vertex shader!
ERROR: 0:1: '' :  version '330' is not supported
ERROR: 0:1: '' : syntax error: #version
ERROR: 0:3: 'layout' : syntax error: syntax errorFailed to compile fragment shader!
ERROR: 0:1: '' :  version '450' is not supported
ERROR: 0:1: '' : syntax error: #version
ERROR: 0:3: 'layout' : syntax error: syntax error

在 stackoverflow 有人遇到了相同的问题,解决方法如下:

  1. add these line of code in front of glCreateWindow
  2. add VAO as @Ali ASkari memtion above (Modern OpenGL requires a VAO be defined and bound if you are using the core profile.)
#include <iostream>// GLEW
#include <glew.h>// GLFW
#include <glfw3.h>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 handlingint 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 complie " << (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);glDeleteShader(vs);glDeleteShader(fs);return program;
}int main()
{/* Initialize the library */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);
+   #ifdef __APPLE__
+       std::cout << "I'm apple machine" << std::endl;
+       glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
+   #endifunsigned int width = 800;unsigned int height = 600;/* Create a Windowed mode and its OpenGL context */window = glfwCreateWindow(width, height, "Hello World", NULL, NULL);if(!window){std::cout << " can't create window!!!" << std::endl;glfwTerminate();return -1;}unsigned int major = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MAJOR);unsigned int minor = glfwGetWindowAttrib(window, GLFW_CONTEXT_VERSION_MINOR);std::cout << "opengl shader version: " << major << "." << minor << std::endl;/* 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 VBO, VAO;
+   glGenBuffers(1, &VBO);
+   glGenVertexArrays(1, &VAO);
+   glBindVertexArray(VAO);glBindBuffer(GL_ARRAY_BUFFER, VBO);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 corelayout(location = 0) in vec4 position;void main(){gl_Position = position;})";std::string fragmentShader = R"(#version 330 corelayout(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 events */glfwPollEvents();}+   glDeleteVertexArrays(1, &VAO);
+   glDeleteBuffers(1, &VBO);glDeleteProgram(shader);glfwTerminate();return 0;
}

OpenGL中如何处理着色器

个人习惯将两个着色器合并到一个只有顶点和片段着色器的文件中。不管怎样,让我们进入代码展示这一切的原理。

着色器合并

首先我们实际上要做的是创建一个包含这两个着色器的文件,以便我们了解是如何处理它们的。

在项目目录中创建一个放置资源的 res 文件夹,在该文件夹下创建专门放置着色器的 shaders 文件夹,在之后我们可能会有像纹理之类的其他资源。最后在 shaders/ 下新建文件 Basic.shader

现在我们有了一个着色器文本,你可以看到它没有显示 C++ 图标,这非常重要因为我们不想把它编译成 C++ 代码。将之前的着色器代码复制粘贴到文件中:

#shader vertex
#version 330 corelayout(location = 0) in vec4 position;void main()
{gl_Position = position;
}#shader fragment
#version 330 corelayout(location = 0) out vec4 color;void main()
{color = vec4(1.0, 0.0, 0.0, 1.0);
}

这样我们就无需在两个不同的着色器文件之间切换,并且很简单干净有条理。

着色器读取与使用

下面我们要做的就是读取这个文件,然后把它分成两块字符串,一个是我们的片段着色器,而另一个就是我们的顶点着色器源码。引入头文件 #include<fstream>,打开文件:

static void ParseShader(const std::string& filepath)
{std::fstream stream(filepath);
}

所以现在我们需要做的就是一行一行地浏览那个文件,然后只去检查是否是指定的着色器类型。

std::string line;
while(getline(stream, line))
{if (line.find("#shader") != std::string::npos){if (line.find("vertex") != std::string::npos)// set mode to vertexelse if (line.find("fragment") != std::string::npos)// set mode to fragment}
}

添加着色器类型并在分支设定正确的类型:

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)type = ShaderType::VERTEX;else if (line.find("fragment") != std::string::npos)type = ShaderType::FRAGMENT;}
}
else
{ss[(int)type] << line << '\n';
}

最后源码如下:

#include <iostream>
#include <fstream>
#include <string>
#include <sstream>#include <GL/glew.h>
#include <GLFW/glfw3.h>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)type = ShaderType::VERTEX;else if (line.find("fragment") != std::string::npos)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 handlingint result;glGetShaderiv(id, GL_COMPILE_STATUS, &result);if (result == GL_FALSE){int length;glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);char* message = (char*)malloc(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);glDeleteShader(vs);glDeleteShader(fs);return program;
}int main()
{/* Initialize the library */if (!glfwInit())return -1;/* Create a Windowed mode and its OpenGL context */GLFWwindow* 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);glEnableVertexAttribArray(0);glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 2, 0);ShaderProgramSource source = ParseShader("res/shader/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);/* Swap front and back buffers */glfwSwapBuffers(window);/* Poll for and events */glfwPollEvents();}glDeleteProgram(shader);glfwTerminate();return 0;
}

由于 Mac 出现的各种错误太多了,相同代码跑不通太浪费时间了,所以还是转到了 Windows 上:

OpenGL中的索引缓冲区

今天我们会讨论索引缓冲区。

绘制四边形

在我们讨论索引缓冲区为什么用它以及它到底是什么之前,让我们先考虑一个图形编程的基础的问题:去画一个正方形。

显然我们可以通过拼接两个三角形实现,先画出来第一个三角形:

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

回到我们的代码增加另一个三角形的三个顶点(其中两个顶点与第一个三角形重合):

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

同时更改一下缓冲区的设置:

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

由于我们绘制的是六个顶点,所以这里也要改:

glDrawArrays(GL_TRIANGLES, 0, 6);

这里不是正方形的原因是因为窗口原因。

很酷!我们已经画出了一个正方形,它并不太难,但是绘制这个正方形的方式有些东西不太理想:我们的两个顶点是完全一样的,也就是在复制我们的内存,我们在显存中存储相同顶点的相同字节,存储了多次。因为显存并不是无限的,而我们想要降低它的内存使用。

索引缓冲区

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

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

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

接着创建一个无符号整型数组 indices

float positions[] = {-0.5f, -0.5f,  // 00.5f, -0.5f,    // 10.5f,  0.5f,    // 2-0.5f,  0.5f    // 3
};unsigned int indices[] = {0, 1, 2,2, 3, 0
}

这实际就是一个索引缓冲区,我们需要说明 OpenGL 如何去渲染这个三角形或者正方形,而不是给它提供冗余或重复的顶点位置。在这个例子中我们只有位置,但实际应用中可能会有更多的数据。

现在我们需要把它们发送到显卡上,并且告诉 OpenGL 用它们去渲染。而我们实现的方式非常类似于创建顶点缓冲区:

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

ibo 代表索引缓冲区对象,表示这个特定的索引缓冲区。这里唯一的区别是把 GL_ARRAY_BUFFER 换为 GL_ELEMENT_ARRAY_BUFFERpositions 替换为之前的 indices

需要说明一下我们会在所有这些例子中使用 unsigned int,因为在这种情况下不会有任何的性能差异,这里的关键是必须使用无符号类型。

最后需要改变的是我们的 DrawCall:

-glDrawArrays(GL_TRIANGLES, 0, 3);
+glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);

count 就是我们需要绘制的 6 个索引,绘制索引的数量而非我们绘制顶点的数量;type 就是在索引缓冲区中的数据类型,在本例中是 GL_UNSIGNED_INT;最后是指向那个索引缓冲区的指针,而前面我们已经绑定了 ibo,所以这里可以填 nullptr。这就是我们绘制三角形的实际 DrawCall 指令。

运行程序,你可以看到我们得到了一个漂亮的矩形。

我们已经删除了任何重复的顶点,在顶点缓冲区中得到了完全唯一的顶点,之后创建了一个索引以便多次绘制顶点;然后我们用 ibo 绑定代码把索引缓冲区发送给显卡;最终我们使用 glDrawElements() 绘制图形。

#include <iostream>
#include <fstream>
#include <string>
#include <sstream>#include <GL/glew.h>
#include <GLFW/glfw3.h>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)type = ShaderType::VERTEX;else if (line.find("fragment") != std::string::npos)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 handlingint result;glGetShaderiv(id, GL_COMPILE_STATUS, &result);if (result == GL_FALSE){int length;glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);char* message = (char*)malloc(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);glDeleteShader(vs);glDeleteShader(fs);return program;
}int main()
{/* Initialize the library */if (!glfwInit())return -1;/* Create a Windowed mode and its OpenGL context */GLFWwindow* 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,    // 00.5f, -0.5f,    // 10.5f,  0.5f,    // 2-0.5f,  0.5f        // 3};unsigned int indices[] = {0, 1, 2,2, 3, 0};unsigned int buffer;glGenBuffers(1, &buffer);glBindBuffer(GL_ARRAY_BUFFER, buffer);glBufferData(GL_ARRAY_BUFFER, 6 * 2 * sizeof(float), positions, GL_STATIC_DRAW);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/shader/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);glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);/* Swap front and back buffers */glfwSwapBuffers(window);/* Poll for and events */glfwPollEvents();}glDeleteProgram(shader);glfwTerminate();return 0;
}

OpenGL中处理错误

今天我们要讨论的都是错误,我们怎么知道我们做错了什么以及如何尽快地调试并修复它。

glGetError

我们有两种主要的方式来检查 OpenGL 中的错误,其中一个一个叫做 glGetError(),它是一个我们可以调用的 OpenGL 内置函数。它能够兼容所有版本并且原理相对简单:在我们调用 OpenGL 时如果发生错误,内存中有一个标志会被内部设置,其会说明发生了什么类型的错误,并且当我们调用 glGetError() 时它会返回一个标志(或者说错误码)。如果我们继续调用 glGetError() 它会把所有标志返回给我们,因为我们可能会产生多个类型的错误。

回到上次渲染正方形的代码,我们可以更改索引缓冲区类型导致错误:

-glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, nullptr);
+glDrawElements(GL_TRIANGLES, 6, GL_INT, nullptr);

可以看到小小的改动得到的就是黑屏,我们的矩形根本没有渲染,这太糟糕了。

glDebugMessageCallback

在最近的 OpenGL 4.3 中添加了一个新的函数 glDebugMessageCallback()

查阅文档,可以发现它允许我们指定一个指向 OpenGL 的函数指针,当错误发生时 OpenGL 会调用我们的那个函数。

唯一的问题在于兼容性,它只在 4.3 及以上版本,所以你不能再早期版本中使用它。优点也很明显,它不会仅仅给你一个错误码,会提供更详细的信息。根据我的经验, glDebugMessageCallback() 总体上非常好,比 glGetError() 好得多。但今天我们只讨论 glGetError()

可以创建一个循环调用的报错函数 GLClearError()

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

接下来创建另一个打印出实际错误的函数 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()

static bool GLLogCall()
{while (GLenum error = glGetError()){std::cout << "[OpenGL Error] (" << error << ")" << std::endl;return false;}return true;
}

接着在头文件下面定义宏断言:

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

这是 MSVC 特有的,在 clang、gcc 或者其他编译器中都不起作用。最后插入断言:

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

为了方便起见定义宏 GLCall()

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

更改调用:

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

最后加一些调试信息:

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

OpenGL中的统一变量

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

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

颜色变量

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

#shader fragment
#version 330 corelayout(location = 0) out vec4 color;+uniform vec4 u_Color;void main()
{
-   color = vec4(0.0, 0.2, 0.8, 1.0);
+   color = u_Color;
}

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

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

 ShaderProgramSource source = ParseShader("res/shader/basic.shader");unsigned int shader = CreateShader(source.VertexSource, source.FragmentSource);GLCall(glUseProgram(shader));+  GLCall(int location = glGetUniformLocation(shader, "u_Color"));
+  ASSERT(location != -1);
+  GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));/* Loop until the user closes the window */while (!glfwWindowShouldClose(window)){......

总结一下,我通过使用实际的着色器 id 编写 glUseProgram() 绑定着色器,获取这个颜色变量的实际位置。而当我获取那个位置的时候,就调用 glUniform4f() 在着色器中设置我的数据。如果一切顺利,我应该将这个颜色值写入我的矩形每个像素的实际输出颜色:

可以看到我们得到了与之前完全相同的结果,为了区分明显一点修改颜色:

-GLCall(glUniform4f(location, 0.2f, 0.3f, 0.8f, 1.0f));
+GLCall(glUniform4f(location, 0.8f, 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 */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(window);/* Poll for and events */glfwPollEvents();}GLCall(glDeleteProgram(shader));glfwTerminate();return 0;
}

运行程序会发现颜色变化比较快,因为我们没有限制帧率:

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

该设置可以同步我们主频的帧率,得到更平滑的动画。

OpenGL中的顶点数组

今天我们会讲 OpenGL 的顶点数组。

…非常抱歉,虽然文章早就写好了,但是因为我用的自己图床迁移比较麻烦,所以就不再 CSDN 上更新了,喜欢的朋友可以到置顶的原站上继续学习。

抽象顶点索引缓冲区成类

OpenGL中的缓冲区和布局的抽象

OpenG中抽象着色器

OpenGL写一个基础的渲染器类

OpenGL中的纹理

OpenGL中的混合

OpenGL中的数学

OpenGL中的投影矩阵

OpenGL中的模型视图投影矩阵

OpenGL中的ImGui

批量渲染对象

为OpenGL建立一个测试框架

创建测试

创建一个纹理测试

如何让统一变量更快

批渲染

简介

颜色

纹理

动态几何

索引

挑战一小时完成批渲染器

Cherno OpenGL 教程相关推荐

  1. WhyGL:一套学习OpenGL的框架,及翻写Nehe的OpenGL教程

    最近在重学OpenGL,之所以说重学是因为上次接触OpenGL还是在学校里,工作之后就一直在搞D3D,一转眼已经毕业6年了.OpenGL这门手艺早就完全荒废了,现在只能是重学.学习程序最有效的办法是动 ...

  2. NeHe OpenGL教程 第四十课:绳子的模拟

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  3. NeHe OpenGL教程 第十课:3D世界

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  4. NeHe OpenGL教程 第二十三课:球面映射

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  5. NeHe OpenGL教程 第四十七课:CG顶点脚本

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  6. NeHe OpenGL教程 第三十六课:从渲染到纹理

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  7. NeHe OpenGL教程 第二十一课:线的游戏

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  8. NeHe OpenGL教程 第三十七课:卡通映射

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  9. NeHe OpenGL教程 第三十课:碰撞检测

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

  10. NeHe OpenGL教程 第四课:旋转

    转自[翻译]NeHe OpenGL 教程 前言 声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改.对NeHe的OpenGL管线 ...

最新文章

  1. 基于CNN的图像缺陷分类
  2. postgres 入门
  3. python安装模块方法_Python安装模块的几种方法
  4. Android 心跳呼吸动画
  5. 用Java描述数据结构之栈和队列,以及栈和队列的常用方法
  6. Magento:Paypal付款不成功返回后不要清空购物车产品的解决方案
  7. 重置密码解决MySQL for Linux错误 ERROR 1045 (28000):
  8. C++ STL 迭代器在string类中的使用方法
  9. 使用mustache js模板引擎
  10. WebView 简单使用方法
  11. MATLAB设置使用语言为中文
  12. 国内docker镜像网站
  13. 重装服务器系统鼠标键盘用不了,win7重装系统后鼠标键盘不能用怎么办
  14. 虚拟光驱传文件到服务器,虚拟光驱服务器
  15. MVC简介——一篇非常简单易懂的介绍
  16. UIImageView 显示不规则图片,类似微信聊天气泡图片展示效果
  17. Linux | CentOS系统时钟校正
  18. 唐宇迪学习笔记20:聚类算法——DBSCAN
  19. 3D查找表(3D LUT)说明
  20. 重磅!中国最赚钱的公司,要上市了!

热门文章

  1. CZTZ-JavaEE Web开发基础平台系列培训(一)-数据库设计规范
  2. navicat中文破解版,navicat for mysql10.0.11简体中文破解版
  3. python解析excel函数
  4. python人脸识别门禁系统_一种基于python的人脸识别开源系统
  5. 联想小新打印机M7268W固件升级步骤
  6. SQLyog详细安装教程
  7. QCC512x/302x笔记(3)-- 只改一行代码,实现串口输出调试log
  8. laravel下载安装
  9. 华三交换机如何进入配置_h3c交换机配置telnet配置教程
  10. 信号与系统实验之信号的时域分析(Python+matplotlib)