@[TOC](OpenGL (二)OpenGL入门理论知识)
OpenGL 学习网址
- http://www.opengl-tutorial.org/
感觉难度适中
- http://ogldev.atspace.co.uk/index.html
(有一些skybox,shadow volume 等实现技巧的教程)
- http://www.scratchapixel.com/
(看起来更倾向于计算机图形学,而不仅仅是一个 OpenGL 的教程)
- https://learnopengl-cn.readthedocs.io/zh/latest/
(这个网站也很基础,内容非常好,水平和第一个类似,并且还有中文。至少看到纹理映射这篇的教程,讲得比第一个链接更翔实。可以和第一个互相参考,强推)
- http://nehe.gamedev.net/
这个是我觉得全世界最知名的OpenGL教程,而且有网友将其中48个教程翻译成了中文http://www.owlei.com/DancingWind/Nehe 此教程最大的特点是提供了针对不同平台、不同编译器、不同语言的各种版本。你不用考虑自己用的是Linux/Windows、VC/BC、C++/Java/C#/VB,甚至D语言,你都能找到对应的版本。除了这些教程,在Nehe Productions你还能下载到各种很cool的Demo,相当多的一部分都提供源代码。http://nehe.gamedev.net/data/downloads/download.asp?letter=0-9
- http://www.ultimategameprogramming.com/
有100个免费的OpenGL教程,内容涉及很多八叉树、BSP、Cg、GLSL、各种纹理映射技术等,还有OpenAL、Ray Tracing的教程。该网站还推出了一本教你制作游戏引擎的书《Ultimate Game Programming with DirectX》,暂时还没有中文翻译版。
- http://www.codesampler.com/
有关于OpenGL、DirectX的很多教程、而且还定时更新一些东西,不过最近该链接我打开不了,不知道暂时出了什么问题。
- http://www.lighthouse3d.com/opengl/
有针对View Frustum、GLSL、Math、Billboarding、Picking、Terrain、Display Lists、GLUT各专题的教程。其中关于GLUT、Terrain部分讲解都非常详细。一般的教程网站都是通过一段代码展示OpenGL的某种渲染效果,如果没有图形学背景很难理解其原理,该网站对所列每项技术的来龙去脉,相关算法都有比较详细的说明,会让你理解更加深刻。
- http://www.gametutorials.com/
我接触最早的一个OpenGL教程网站,有很多有特色的教程,比如一个小的2D RPG教程,可惜现在大部分代码都不能免费下载了。
- http://www.ozone3d.net/tutorials/index.php
有关于OpenGL、GLSL、Direct3D的教程,还有很多其它有用的资料、工具下载,个人觉得很棒的一个网站。
- http://www.swiftless.com/tutorials/opengl/opengltuts.html
OpenGL、GLSL、DirectX、Physics、Math等相关教程,看着很不错。
- http://www.morrowland.com/apron/tut_gl.php
教程的内容包含OpenGL基本变换、纹理映射等,也有一些有趣的Demo,可惜没有提供源码下载。
- http://www.coolgroups.com/
挺有特色的一个网站。
- http://www.videotutorialsrock.com/
我知道的第一个OpenGL视频教程,可惜是全英文的,看视频教程还有一个好处就是不经意间能学到作者编程时的一些细小技巧。
- https://github.com/tomdalling/opengl-series
我学习opengl得到的启示最多的一篇文章,我强烈地建议大家去读一下这位大神的相关系列的文章.这里面的代码包含全面,真正想学习opengl的可以去看看
OpenGL 简介
- 什么是OpenGL
1、OpenGL是一种应用程序编程接口,它是一种可以对图形硬件设备特性进行访问的软件库。
2、OpenGL被设计为一个现代化的、硬件无关的接口,因此我们可以在不考虑计算机操作系统或窗口系统的前提下,在多种不同的图形硬件系统上,或者完全通过软件的方式实现OpenGL接口。
3、OpenGL自身并不包含任何执行窗口任务,或者处理用户输入的函数。
4、OpenGL也没有提供任何用于表达三维物体模型,或者读取图像文件的操作。我们需要通过一系列的几何图元(点,线段,三角形,以及patch)来创建三维空间物体!
5、OpenGL API是过程性的,不是描述性的,即OpenGL不是面向对象的,所以OpenGL无法利用面向对象的特性。使用的时候只需要:程序与OpenGL的实现链接就可以了!
6、OpenGL的实现可以是软件实现,也可以是硬件实现。
软件实现:是对OpengGL函数调用时作出的响应并创建二维或三维图像的函数库。
硬件实现:则是通过设置能够绘制图形或图像的图形卡驱动程序
硬件实现要比软件实现快得多!!
- OpenGL可以用来干什么?
1、OpenGL已经诞生很长时间了,1992年7月,SGI公司发布了OpenGL的1.0版本。
2、应用领域:视频、图形、图片处理,2D/3D游戏引擎开发,科学可视化,医学软件的开发 ,CAD(计算机辅助技术),虚拟实境(AR VR),AI人工智能
- OpenGL ES和OpenGL有什么关系?
1、OpenGL ES是OpenGL的子集,针对手机、PDA和游戏主机嵌入式设备而设计
2、OpenGL ES 是从 OpenGL 裁剪定制而来的,去除了 glBegin/glEnd,四边形(GL_QUADS)、多边形(GL_POLYGONS)等复杂图元等许多非绝对必要的特性,剩下最核心有用的部分。
可以理解成:OpenGL ES是一个在移动平台上能够支持 OpenGL 最基本功能的精简规范。
- OpenGL程序需要执行的主要操作步骤
1、从OpenGL的几何图元中设置数据,用于构建形状
2、使用不同的着色器对输入的图元数据执行计算操作,判断位置,颜色以及其他渲染属性。
3、将输入图元的数学描述 转换为与屏幕位置对应的像素片元,也称(光栅化)。
4、针对光栅化过程产生的每个片元,执行 片元着色器,从而决定这个片元的最终颜色和位置
5、如果有必要 可以对片元执行一些额外操作。
例如:判断片元对应的对象是否可见,或者将片元的颜色与当前屏幕位置的颜色进行融合。
- 开发语言与编程约定
我们以后会见到OpenGL的函数多是以gl开头,因为OpenGL的函数遵循一定的命名约定,它可以告诉我们这个函数来自哪个函数库,并且还可以告诉我们这个函数的参数个数和类型。
OpenGL的函数是采用以下的书写格式:
<函数库前缀> <根命令> <可选的参数数量> <可选的参数类型>
- OpenGL版本比较
早前学OpenGL的时候还是1.x版本,用的都是glVertex,glNormal等固定管线API。后来工作需要接触DirectX9,shader也只是可选项而已,跟固定管线一起混用着。OpenGL ES 2.0版本开始就不再支持固定管线,只支持可编程管线。
OpenGL 基础理论知识
1. 坐标系与变换
1、在开发OpenGL程序时,需要用到两个坐标系。
一个称为对象坐标系 :(物体坐标系)第一个坐标系是我们在开发中使用的坐标系。
另一个称为世界坐标系:(世界坐标系)第二个坐标系又称为窗口坐标系或屏幕坐标系,在这个坐标系中的单位是像素。
2、 在绘制的过程中,OpenGL会自动实现从对象到窗口坐标系的转换,所需要的信息是屏幕中显示窗口的尺寸和用户希望显示对象空间的大小。
3、OpenGL中所需要的坐标系变换由两个矩阵决定,
即:模型视图矩阵和投影矩阵,这些矩阵是OpenGL的状态的一部分。
设置这两种矩阵的典型步骤包括以下三个步骤:
(1) 指定我们希望修改的矩阵。
(2) 将矩阵设为单位矩阵。
(3) 修改当前矩阵为用户期望的矩阵。
2. OpenGL 显示图形流程
OpenGL是使用客户端——服务端的形式实现的。
(客户端):我们编写的程序。
(服务端):计算机图形硬件厂商所提供的OpenGL实现。
在没有OpenGL的时候,CPU与GPU之间传递数据是通过控制器内存之间传递,传递速度非常的慢,内存在复制数据时,CPU和GPU都不能操作这个数据。OpenGL很好的解决这个问题,OpenGL创建了缓存区域,能够连续的管理RAM,保证数据传输不影响GPU处理数据效率。
3. OpenGL 基本概念
- GLEW, GLFW和GLM介绍
- GLEW
The OpenGL Extension Wrangler (GLEW)是用来访问OpenGL 3.2 API函数的。不幸的是你不能简单的使用#include
来访问OpenGL接口,除非你想用旧版本的OpenGL。在现代OpenGL中,API函数是在运行时(run time)确定的,而非编译期(compile time)。GLEW可以在运行时加载OpenGL API。
- GLFW
GLFW允许我们跨平台创建窗口,接受鼠标键盘消息。OpenGL不处理这些窗口创建和输入,所以就需要我们自己动手。我选择GLFW是因为它很小,并且容易理解。
- GLM
OpenGL Mathematics (GLM)是一个数学库,用来处理矢量和矩阵等几乎其它所有东西。旧版本OpenGL提供了类似glRotate, glTranslate和glScale等函数,在现代OpenGL中,这些函数已经不存在了,我们需要自己处理所有的数学运算。GLM能在后续教程里提供很多矢量和矩阵运算上帮助。
- Shaders
Shaders在现代OpenGL中是个很重要的概念。应用程序离不开它,除非你理解了,否则这些代码也没有任何意义。
Shaders是一段GLSL小程序,运行在GPU上而非CPU。它们使用OpenGL Shading Language (GLSL)语言编写,看上去像C或C++,但却是另外一种不同的语言。使用shader就像你写个普通程序一样:写代码,编译,最后链接在一起才生成最终的程序。
Shaders并不是个很好的名字,因为它不仅仅只做着色。只要记得它们是个用不同的语言写的,运行在显卡上的小程序就行。
在旧版本的OpenGL中,shaders是可选的。在现代OpenGL中,为了能在屏幕上显示出物体,shaders是必须的。
为可能近距离了解shaders和图形渲染管线,我推荐Durian Software的相关文章The Graphics Pipeline chapter。
- Vertex Shaders
Vertex shader主要用来将点(x,y,z坐标)变换成不同的点。顶点只是几何形状中的一个点,一个点叫vectex,多个点叫vertices(发音为ver-tuh-seez)。在本教程中,我们的三角形需要三个顶点(vertices)组成。
Vertex Shader的GLSL代码如下:
#version 150
in vec3 vert;
void main() {
// does not alter the vertices at all
gl_Position = vec4(vert, 1);
}
第一行#version 150告诉OpenGL这个shader使用GLSL版本1.50.
第二行in vec3 vert;告诉shader需要那一个顶点作为输入,放入变量vert。
第三行定义函数main,这是shader运行入口。这看上去像C,但GLSL中main不需要带任何参数,并且不用返回void。
第四行gl_Position = vec4(vert, 1);将输入的顶点直接输出,变量gl_Position是OpenGL定义的全局变量,用来存储vertex shader的输出。所有vertex shaders都需要对gl_Position进行赋值。
gl_Position是4D坐标(vec4),但vert是3D坐标(vec3),所以我们需要将vert转换为4D坐标vec4(vert, 1)。第二个的参数1是赋值给第四维坐标。我们会在后续教程中学到更多关于4D坐标的东西。但现在,我们只要知道第四维坐标是1即可,i可以忽略它就把它当做3D坐标来对待。
Vertex Shader在本文中没有做任何事,后续我们会修改它来处理动画,摄像机和其它东西
- Fragment Shaders
Fragment shader的主要功能是计算每个需要绘制的像素点的颜色。
一个”fragment”基本上就是一个像素,所以你可以认为片段着色器(fragment shader)就是像素着色器(pixel shader)。在本文中每个片段都是一像素,但这并不总是这样的。你可以更改某个OpenGL设置,以便得到比像素更小的片段,之后的文章我们会讲到这个。
本文所使用的fragment shader代码如下:
#version 150
out vec4 finalColor;
void main() {
//set every drawn pixel to white
finalColor = vec4(1.0, 1.0, 1.0, 1.0);
}
再次,第一行#version 150告诉OpenGL这个shader使用的是GLSL 1.50。
第二行finalColor = vec4(1.0, 1.0, 1.0, 1.0);将输出变量设为白色。vec4(1.0, 1.0, 1.0, 1.0)是创建一个RGBA颜色,并且红绿蓝和alpha都设为最大值,即白色。
现在,就能用shader在OpenGL中绘制出了纯白色。在之后的文章中,我们还会加入不同颜色和贴图。贴图就是你3D模型上的图像。
- 编译和链接Shaders
在C++中,你需要对你的.cpp文件进行编译,然后链接到一起组成最终的程序。OpenGL的shaders也是这么回事。
在这篇文章中用到了两个可复用的类,是用来处理shaders的编译和链接:tdogl::Shader和tdogl::Program。这两个类代码不多,并且有详细的注释,我建议你阅读源码并且去链接OpenGL是如何工作的。
- 什么是VBO和VAO?
当shaders运行在GPU,其它代码运行在CPU时,你需要有种方式将数据从CPU传给GPU。在本文中,我们传送了一个三角的三个顶点数据,但在更大的工程中3D模型会有成千上万个顶点,颜色,贴图坐标和其它东西。
这就是我们为什么需要Vertex Buffer Objects (VBOs)和Vertex Array Objects (VAOs)。VBO和VAO用来将C++程序的数据传给shaders来渲染。
在旧版本的OpenGL中,是通过glVertex,glTexCoord和glNormal函数把每帧数据发送给GPU的。在现代OpenGL中,所有数据必须通过VBO在渲染之前发送给显卡。当你需要渲染某些数据时,通过设置VAO来描述该获取哪些VBO数据推送给shader变量。
- Vertex Buffer Objects (VBOs)
第一步我们需要从内存里上传三角形的三个顶点到显存中。这就是VBO该干的事。VBO其实就是显存的“缓冲区(buffers)” - 一串包含各种二进制数据的字节区域。你能上传3D坐标,颜色,甚至是你喜欢的音乐和诗歌。VBO不关心这些数据是啥,因为它只是对内存进行复制。
- Vertex Array Objects (VAOs)
第二步我们要用VBO的数据在shaders中渲染三角形。请记住VBO只是一块数据,它不清楚这些数据的类型。而告诉OpenGL这缓冲区里是啥类型数据,这事就归VAO管。
VAO对VBO和shader变量进行了连接。它描述了VBO所包含的数据类型,还有该传递数据给哪个shader变量。在OpenGL所有不准确的技术名词中,“Vertex Array Object”是最烂的一个,因为它根本没有解释VAO该干的事。
你回头看下本文的vertex shader(在文章的前面),你就能发现我们只有一个输入变量vert。在本文中,我们用VAO来说明“hi,OpenGL,这里的VBO有3D顶点,我想要你在vertex shader时,发三个顶点数据给vert变量。”
在后续的文章中,我们会用VAO来说“hi,OpenGL,这里的VBO有3D顶点,颜色,贴图坐标,我想要你在shader时,发顶点数据给vert变量,发颜色数据给vertColor变量,发贴图坐标给vertTexCoord变量。”
给使用上个OpenGL版本的用户的提醒
假如你在旧版本的OpenGL中使用了VBO但没有用到VAO,你可能会不认同VAO的描述。你会争论说“顶点属性”可以用glVertexAttribPointer将VBO和shaders连接起来,而不是用VAO。这取决于你是否认为顶点属性应该是VAO“内置(inside)”的(我是这么认为的),或者说它们是否是VAO外置的一个全局状态。3.2内核和我用的AIT驱动中,VAO不是可选项 - 没有VAO的封装glEnableVertexAttribArray, glVertexAttribPointer和glDrawArrays都会导致GL_INVALID_OPERATION错误。这就是为啥我认为顶点属性应该内置于VAO,而非全局状态的原因。
3.2内核手册也说VAO是必须的,但我只听说ATI驱动会抛错误。下面描述引用自OpenGL 3.2内核手册
所有与顶点处理有关的数据定义都应该封装在VAO里。
一般VAO边界包含所有更改vertex array状态的命令,比如VertexAttribPointer和EnableVertexAttribArray;所有使用vertex array进行绘制的命令,比如DrawArrays和DrawElements;所有对vertex array状态进行查询的命令(见第6章)。
不管怎样,我也知道为啥会有人认为顶点属性应该放在VAO外部。glVertexAttribPointer出现早于VAO,在这段时间里顶点属性一直被认为是全局状态。你应该能看得出VAO是一种改变全局状态的有效方法。我更倾向于认为是这样:假如你没有创建VAO,那OpenGL通过了一个默认的全局VAO。所以当你使用glVertexAttribPointer时,你仍然是在VAO内修改顶点属性,只不过现在从默认的VAO变成你自己创建的VAO。
这里有更多的讨论:http://www.opengl.org/discussion_boards/showthread.php/174577-Questions-on-VAOs
OpenGL 入门实践
- 先来介绍一个大神的开源opengl项目代码:
- https://github.com/tomdalling/opengl-series
这是现代OpenGL教程系列的第一篇。所有代码都是开源的。通过这篇教程,你将会学到如何在Windows下用Visual Studio 2013或Mac下用Xcode搭建OpenGL 3.2工程。该应用包含一个顶点着色器(vertex shader),一个片段着色器(fragment shader)和使用VAO和VBO来绘制的三角形。该工程使用GLEW来访问OpenGL API,用GLFW来处理窗口创建和输入,还有使用GLM进行矩阵/矢量相关的数学运算。所有例子代码的zip打包可以从这里获取:https://github.com/tomdalling/opengl-series/archive/master.zip。
这一系列文章中所使用的代码都存放在:https://github.com/tomdalling/opengl-series。
你可以在页面中下载zip,加入你会git的话,也可以复制该仓库。
- 关于兼容性
本文使用OpenGL 3.2,但我会尝试保持如下兼容:
向后兼容OpenGL 2.1
向前兼容OpenGL 3.X和4.X
兼容Android和iOS的OpenGL ES 2.0
因为OpenGL和GLSL存在许多不同版本,本文代码不一定能做到100%上述兼容。我希望能兼容99%,并且不同版本之间只要轻微修改即可。
想要了解OpenGL和GLSL不同版本间的区别 ,可以参考这个网站:OpenGL, OpenGL ES, WebGL, GLSL, GLSL ES APIs Table
- 环境安装配置
- Visual Studio下安装
代码在Windows 7 32位系统,Visual Studio Express 2013(免费)下创建和测试。你应该可以打开解决方案并成功编译所有工程。
- Xcode下安装
Xcode工程实在OSX 10.10系统,Xcode 6.1下创建并测试的。打开Xcode工程应该可以成功编译所有目标。
- Linux下安装
Linux是基于SpartanJ。我在Ubuntu 12.04下简单测试通过。
安装GLM,GLFW和GLEW:
sudo aptitude install libglm-dev libglew-dev libglfw-dev
进入工程目录:cd platforms/linux/01_project_skeleto
运行makefile:make
运行可执行文件:bin/01_project_skeleton-debug
- 开始编码
打开main.cpp,我们从main()函数开始。
首先,我们初始化GLFW:
glfwSetErrorCallback(OnError);
if(!glfwInit())
throw std::runtime_error("glfwInit failed");
glfwSetErrorCallback(OnError)这一行告诉GLFW当错误发生时调用OnError函数。OnError函数会抛一个包含错误信息的异常,我们能从中发现哪里出错了。
然后我们用GLFW创建一个窗口。
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
gWindow = glfwCreateWindow((int)SCREEN_SIZE.x, (int)SCREEN_SIZE.y, "OpenGL Tutorial", NULL, NULL);
if(!gWindow)
throw std::runtime_error("glfwCreateWindow failed. Can your hardware handle OpenGL 3.2?");
该窗口包含一个向前兼容的OpenGL 3.2内核上下文。假如glfwCreateWindow失败了,你应该降低OpenGL版本。
创建窗口最后一步,我们应该设置一个“当前”OpenGL上下文给刚创建的窗口:
glfwMakeContextCurrent(gWindow);
无论我们调用哪个OpenGL函数,都会影响到“当前上下文”。我们只会用到一个上下文,所以设置完后,就别管它了。理论上来说,我们可以有多个窗口,且每个窗口都可以有自己的上下文。
现在我们窗口有了OpenGL上下文变量,我们需要初始化GLEW以便访问OpenGL接口。
glewExperimental = GL_TRUE; //stops glew crashing on OSX :-/
if(glewInit() != GLEW_OK)
throw std::runtime_error("glewInit failed");
这里的GLEW与OpenGL内核有点小问题,设置glewExperimental就可以修复,但希望再未来永远不要发生。
我们也可以用GLEW再次确认3.2版本是否存在:
if(!GLEW_VERSION_3_2)
throw std::runtime_error("OpenGL 3.2 API is not available.");
在LoadShaders函数中,我们使用本教程提供的tdogl::Shader和tdogl::Program两个类编译和链接了vertex shader和fragment shader。
std::vector shaders;
shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("vertex-shader.txt"), GL_VERTEX_SHADER));
shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("fragment-shader.txt"), GL_FRAGMENT_SHADER));
gProgram = new tdogl::Program(shaders);
在LoadTriangle函数中,我们创建了一个VAO和VBO。这是第一步,创建和绑定新的VAO:
glGenVertexArrays(1, &gVAO);
glBindVertexArray(gVAO);
然后我们创建和绑定新的VBO:
glGenBuffers(1, &gVBO);
glBindBuffer(GL_ARRAY_BUFFER, gVBO);
接着,我们上传一些数据到VBO中。这些数据就是三个顶点,每个顶点包含三个GLfloat。
GLfloat vertexData[] = {
// X Y Z
0.0f, 0.8f, 0.0f,
-0.8f,-0.8f, 0.0f,
0.8f,-0.8f, 0.0f,
};
glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
现在缓冲区包含了三角形的三个顶点,是时候开始设置VAO了。首先,我们应该启用shader程序中的vert变量。这些变量能被开启或关闭,默认情况下是关闭的,所以我们需要开启它。vert变量是一个“属性变量(attribute variable)”,这也是为何OpenGL函数名称中有带“Attrib”。我们可以在后续的文章中看到更多类型。
glEnableVertexAttribArray(gProgram->attrib("vert"));
VAO设置最复杂的部分就是下个函数:glVertexAttribPointer。让我们先调用该函数,等会解释。
glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL);
第一个参数,gProgram->attrib("vert"),这就是那个需要上传数据的shder变量。在这个例子中,我们需要发数据给vertshader变量。
第二个参数,3表明每个顶点需要三个数字。
第三个参数,GL_FLOAT说明三个数字是GLfloat类型。这非常重要,因为GLdouble类型的数据大小跟它是不同的。
第四个参数,GL_FALSE说明我们不需要对浮点数进行“归一化”,假如我们使用了归一化,那这个值会被限定为最小0,最大1。我们不需要对我们的顶点进行限制,所以这个参数为false。
第五个参数,0,该参数可以在顶点之间有间隔时使用,设置参数为0,表示数据之间没有间隔。
第六个参数,NULL,假如我们的数据不是从缓冲区头部开始的话,可以设置这个参数来指定。设置该参数为NULL,表示我们的数据从VBO的第一个字节开始。
现在VBO和VAO都设置完成,我们需要对它们进行解绑定,防止一不小心被哪里给更改了。
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
然后告诉OpenGL我们要开始使用VAO和shader了:
glUseProgram(gProgram->object());
glBindVertexArray(gVAO);
最后,我们绘制出三角形:
glDrawArrays(GL_TRIANGLES, 0, 3);
调用glDrawArrays函数说明我们需要绘制三角形,从第0个顶点开始,有3个顶点被发送到shader。OpenGL会在当前VAO范围内确定该从哪里获取顶点。
顶点将会从VBO中取出并发送到vertex shader。然后三角形内的每个像素会发送给fragment shader。接着fragment shader将每个像素变成白色。欢呼!
现在绘制结束了,为了安全起见,我们需要将shader和VAO进行解绑定:
glBindVertexArray(0);
glUseProgram(0);
最后一件事,在我们看到三角形之前需要切换帧缓冲:
glfwSwapBuffers(gWindow);
参考教程:https://www.e-learn.cn/content/qita/641129
http://huangwei.pro/2015-05/modern-opengl1/