文章目录
- 前言
- 1. Welcome to OpenGL
- 2. Setting up OpenGL and Creating a Window in C++
- 3. Using Modern OpenGL in C++
- 4. Vertex Buffers and Drawing a Triangle in OpenGL
- 5. Vertex Attributes and Layouts in OpenGL
- 6. How Shaders Work in OpenGL
- 7. Writing a Shader in OpenGL
- 8. How I Deal with Shaders in OpenGL
- 9. Index Buffers in OpenGL
- 10. Dealing with Errors in OpenGL
- 11. Uniforms in OpenGL
- 12. Vertex Arrays in OpenGL
- 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!
前言
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有一大堆函数。但是有两个问题:
- 不跨平台。
到达驱动然后从里面拉出函数,需要用一些win32 api调用外来的Windows加载库,并且加载函数指针,所有的这些都只适用于Windows。 - 函数这么多,需要手动检索它们然后为它们写代码,很麻烦!
所以我们要做的是使用另外一个库。因此我们知道,这个库实际上很简单,基本上做的就是提供OpenGL api的规范,函数的声明,符号的声明,常量,所有的东西在一个头文件里面。然后那些幕后文件,C++文件在里面,找到你在使用的图像驱动,找到相应的dll文件,然后加载所有的函数指针,那就是它做的所有。其实做的是苦力活。
要使用的这个库叫 gluw 或者 glew 或者是 opengl extension wrangler,也有另一个库叫 glad,是opengl有点特殊的一个扩展。我们在这里使用 glew 库。
glew库链接:http://glew.sourceforge.net/
查阅文档我们知道:
- 不能从glew里面使用opengl函数,直到你调用了glewInit()
- 在调用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 这些
运行应该是一个红色的三角形。
这里还有一些要注意的地方:
- 写顶点着色器的时候,是vec4而实际我们只传了两个浮点数,这是因为 gl_Position 必须是 vec4 的,不然在main里头也需要强制转换
- layout(location = 0) 就和 glVertexAttribPointer 对应了,表示这个实际属性位于索引 0 处
- 在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要执行:
- 绑定shader
- 绑定vertex buffer
- 指示其layout(set up the vertex layout)
- 绑定index buffer
- 调用drawcall(glDrawElements)
现在变为了:
- 绑定shader
- 绑定vertex array
- 调用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。
注:
- 我 debug 发现 vao 在这段程序中的值为1,所以之前的
glBindVertexArray(0)
确实解绑了,在drawcall的时候还需要glBindVertexArray(vao)
- 在前面的参考链接中说:
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 这条指令才能做到。- 推荐这个回答: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;
}