glew 库可以自动管理 OpenGL 扩展,每个 OpenGL 程序都应该在程序开始的地方引入这个库。OpenGL 提供基础图元的绘制方法,包括点、线段、三角形、四边形,任何复杂的图元都可以转换成三角形进行绘制。OpenGL 基于状态,每个 GL Object 都有自己的一套状态,全局环境 GL Context 也有一套状态,对象绘制前先绑定到上下文。
使用 glew
glew 全称是 OpenGL Extension Wrangler Library
,它能够帮忙解决 OpenGL 不断扩展的问题。初始化 glew 之后,它将查询系统上所有可用的扩展功能并自动加载它们,然后提供一个头文件作为接口,我们直接通过头文件就可以使用这些扩展功能。
下载 glew,下载之后进行解压,得到以下目录结构。
1 | |-glew-2.1.0 |
接下来开始配置环境,配置的方式有两种,第一种方式跟之前配置 glut 一样,系统级的配置,配置之后所有的 OpenGL 项目都不需要配置。
- 第一步,把 include 目录下的头文件放在
%VISUAL_STUDIO%\VC\include\gl
目录下; - 第二步,把 lib 目录下的 lib 文件放在
%VISUAL_STUDIO%\VC\lib
目录下,然后在项目属性的 链接器 –> 输入 添加相应库的引用;如果要创建 64 位程序,则要把 lib 文件放在
%VISUAL_STUDIO%\VC\lib\amd64
目录下。 - 第三步,把 bin 目录下的 dll 文件放在系统目录下。
32 位系统直接放在
System32
目录下;64 位系统如果要创建 64 位程序也放在System32
目录下,如果要创建 32 位程序则要放在SysWOW64
目录下。
这是系统级的配置,一次配置对所有项目都生效,这种方式虽然能够一劳永逸,但过多地把库文件拷到系统路径也不好,因此还有第二种方式,即给每个创建的项目单独配置环境,
- 把头文件放在
$PROJECT_ROOT%\include\GL
目录下,然后在项目属性的 C/C++ –> 附加包含目录 中添加.\include
; - 把静态库 lib 文件放在
$PROJECT_ROOT%\lib
目录下,然后在项目属性的 链接器 –> 常规 –> 附加库目录 中添加.\lib
,在 链接器 –> 输入 中添加相应库的引用;如果要同时编译 32 位程序和 64 位程序,可以在项目下建立
.\lib\win32
和.\lib\x64
两个目录,然后配置 32 位程序链接第一个目录,配置 64 位程序链接第二个目录。 - 把动态库 dll 文件放在可执行程序 exe 同级目录下。
如果一个项目想同时编译 32 位和 64 位,则可以分别把 32 位的 lib 文件和 64 位的 lib 文件放在 .\lib\win32 和 .\lib\x64 目录下,然后分别修改附加库目录,再把相应的 dll 文件拷到编译后的 32 位程序和 64 位程序目录下。
关于 VS 配置的详细内容,可参考 Visual Studio 环境配置。
GL context
在开始绘制图形之前,我们必须先了解 GL context 和 GL objects 这两个重要概念,详细的可参考 官方文档。
上一篇文章讲到 OpenGL 渲染是基于状态(state)的。OpenGL context 是一个重要的概念,只有创建了 context,OpenGL 才存在,context 一旦被销毁了,OpenGL 就不存在了。context 存储了一个 OpenGL 实例的所有状态,类似于一个程序开辟的所有内存空间。context 可以看作进程在操作系统中的一个执行过程,一个进程可以创建多个 context,每一个 context 代表一个可视面,就像一个应用程序的一个界面一样。
简单来讲,OpenGL 上下文保存了一个 OpenGL 实例的所有状态,在使用 OpenGL 之前必须先创建一个 OpenGL context。
1 | int main(int argc, char **argv) |
这里是想获取一些系统信息,但得到的结果却全是空,这里因为此时还没有创建 context,OpenGL 相当于不存在。
1 | int main(int argc, char** argv) |
这样就可以正常获取信息了,因为 glutCreateWindow
创建一个窗口的同时就已经创建了一个 OpenGL context。
GL objects
再重申一遍,OpenGL 基于状态来渲染,可以说 OpenGL 被定义成“状态机”,所有的 API 都是修改状态、查询状态或者使用状态来渲染。GL Object 是一些状态的集合,这点看起来跟 GL Context 有点像,也可以这样类比,但要知道 object 和 context 的 state 是相互独立的,context 有一套状态,每个 object 也会有自己的一些状态。只有把 object 绑定到 context 上,它的状态都会映射到 context 上,绑定之后修改 context 的状态,object 也会受影响;相反基于 context 状态的函数也可以使用 object 的状态。
对象可以分成两大类,regular objects 和 container objects。
- regular object 包括 Buffer Objects,Query Objects,Renderbuffer Objects,Sampler Objects,Texture Objects。
- container objects 包括 Vertex Array Objects,Framebuffer Objects,Program Pipeline Objects,Transform Feedback Objects。
对象创建
使用 glGen*
函数给对象生成一个名字,就是创建对象了。
1 | void glGen*(GLsizei n, GLuint *objects); |
@param n
表示要创建的对象个数;@param objects
表示对象的地址,如果只创建一个对象,则传这个对象的地址,如果创建多个,则传数组的首地址。
对象创建成功会给对象分配一个名字,也就是对象的唯一 ID,这是一个 32 位无符号整型,用于标识这个对象。另外,整数 0 这个名字用于空对象,给对象分配名字时是从 1 开始的。
对象销毁
使用 glDelete*
可以销毁一个或多个对象,参数同创建对象一样。
1 | void glDelete*(GLsizei n, const GLuint *objects); |
关于对象销毁有几点要注意的:
- 如果对象已经绑定到 GL Context ,则对象销毁后会自动解绑;但如果对象附加到另一个对象上,则这种附加关系不会解除;
- 对象被 delete 之后并不会立即删除,它的名字还可以使用,但请不要用。
对象绑定
1 | void glBind*(GLenum target, GLuint object); |
这个函数用于把对象绑定到 GL Context。OpenGL 是无法直接操作 GL Object 的,只能操作 GL Context,所以必须先把对象绑定到上下文,绑定的时候会指定一个类型,有些对象可以绑定为多个类型,比如一个 buffer obejct
可以绑定为 array buffer,index buffer,pixel buffer,transform buffer
或者其它。
@param target
指明了对象要绑定的类型;@param object
要绑定的对象。
VAO && VBO
我们知道了 OpenGL 是一个状态机,使用 object 来保存数据和状态,通过绑定到 context,将 object 的状态和 context 的状态关联起来,然后使用 context 的状态进行渲染。绘制图形的时候需要两个最基础的对象 VBO
和 VAO
。
VBO
,即 Vertex Buffer Object
,顶点缓冲对象,用于在缓存区保存顶点数据。VBO
对象创建后要给其填充数据。
1 | glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); |
VAO
即 Vertex Array Object
,顶点数组对象(亦或叫顶点属性对象),它不保存顶点数据,而是保存了顶点数据的格式和所需 buffer 对象的引用。
顶点的每一个状态属性都是可以开启和关闭的,只有开启的顶点属性在绘制的时候才会生效。
1 | void glEnableVertexAttribArray(GLuint index); |
为了让 VAO
能使用 VBO
的数据,我们需要告诉 OpenGL 编号为 index 的顶点属性使用当前绑定 VBO
的哪个数据。
1 | glVertexAttribPointer(index, size, type, normalized, stride, pointer); |
index
是第几个属性,像顶点的第 0 个属性就是位置;size
指定构成属性的分量个数,像顶点位置由x,y,z
三个分量组成,所以size = 3
;type
指定属性值的类型,像顶点位置为GL_FLOAT
;normalized
指属性在管线中使用之前是否需要被规范化;stride
指两个相同属性值之间间隔的字节数,只有一个属性时可以直接填 0,但实际上步长不会为 0 的;pointer
指存储数据的偏移值,只有一个属性时偏移值为 0。
绘制点
直接先上代码。
1 | void init() |
初始化
- 创建顶点缓冲对象 VBO,把 VBO 绑定到 GL Context,给顶点缓冲对象填充顶点数据。
- 创建顶点数组对象 VAO,把 VAO 绑定到 GL Context,链接顶点属性,即指定顶点哪个属性用 VBO 中的哪个数值,最后顶点属性必须开启才能生效。
这里省略 VAO 的创建和绑定,使用了全局的默认 VAO 对象,详细的后面再讲。
glPointSize
设置顶点大小,也就是一个点占用几个像素。
绘制
绘制的时候是基于 GL Context 状态,初始化的时候已经将 VBO 和 VAO 绑定到 GL Context 了,所以在绘制函数中直接绘制即可。
glDrawArrays
绘制基础图元:
- 参数一指定要绘制的图元类型,
GL_POINTS
就是绘制点; - 参数二指定从第几个顶点数据开始使用;
- 参数三指定要使用几个顶点的数据,在这里也就是绘制几个点。
这里我们从第 0 个点开始绘制,只绘制一个点,结果如下。
绘制线段
1 | void init() |
首先使用 glLineWidth
设置线段宽度。
绘制线段只需要把图元类型设置为线段即可,0
仍表示从第 0 个顶点开始绘制,8
表示使用 8 个顶点,注意不是指绘制 8 条线,无论绘制什么图元,这个参数的意思都是使用的顶点个数。
不同于点的单一,线段是由多个顶点确定的图元,那顶点之间的组合方式就有多种,GL_LINES, GL_LINES_ADJACENCY, GL_LINE_STRIP, GL_LINE_STRIP_ADJACENCY, GL_LINE_LOOP
都表示线段。下图很好地解释了各种图元是怎么绘制的。
GL_LINES
1 | glDrawArrays(GL_LINES, 0, 8); |
GL_LINES
按照顺序两点确定一条线段,0~1 为线段一,2~3 为线段二,依次类推。
两个顶点确定一条线段,这里使用了 8 个顶点,所以绘制了 4 条线段。OpenGL 会尽可能地满足我们的绘制要求,如果满足不了就向下兼容。比如我们要求使用 4 个顶点,但 VBO 中只有 3 个顶点,那 OpenGL 就只会绘制一条线段。再比如我们要求使用 5 个顶点,OpenGL 也只会绘制两条线段。
GL_LINES_ADJACENCY
1 | glDrawArrays(GL_LINES_ADJACENCY, 0, 8); |
adjacency 为“邻接”的意思,GL_LINES_ADJACENCY
给每条线段两端留一个邻接点,也就是线段两端各跳过一个点不使用。具体的就是 1~2 为线段一,0,3 这两个顶点跳过;5~6 为线段二,4,7 这两个点跳过。
GL_LINE_STRIP
1 | glDrawArrays(GL_LINE_STRIP, 0, 8); |
strip
是长条的意思,也就是一个挨着一个,GL_LINE_STRIP
绘制连续的线段,下一条线段的起点为上一条线段的终点。具体的就是 0~1 为线段一,1~2 为线段二,2~3 为线段三,依次类推。
GL_LINE_STRIP_ADJACENCY
1 | glDrawArrays(GL_LINE_STRIP_ADJACENCY, 0, 8); |
adjacency
要跳过一个点,而 strip
则重复一个点,两个矛盾体结合的结果是 strip
占主导地位,最终的结果是只跳过首尾两个点,中间仍是线段条的形式。具体的就是 1~2 为线段一,2~3 为线段二,5~6 为最后一条线段,0,7 这两个点跳过。
GL_LINE_LOOP
1 | glDrawArrays(GL_LINE_LOOP, 0, 8); |
GL_LINE_LOOP
在 GL_LINE_STRIP
的基础上把首尾两个点连接起来,形成一个循环。
绘制三角形
三角形是组成其它复杂图元的基础图元,一个三角形就是一个网格,其它图元,无论是矩形,圆形,还是复杂的三维模型,都可以拆分成一个个三角形。
同顶点一样,三角形中顶点的组合方式也有多种,GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_TRIANGLE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY
都表示三角形。
GL_TRIANGLES
1 | glDrawArrays(GL_TRIANGLES, 0, 8); |
这是基础的三角形,按顺序三个点确定一个三角形,0~2 为三角形一,3~5 为三角形二,6,7 只有两点不足以形成三角形,舍去。
GL_TRIANGLE_STRIP
1 | glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); |
“三角形条”每次都保留上个三角形的最后两个点,再增加一个点组成新的三角形。0,1,2 为三角形一,1,2,3 为三角形二。
为了更容易看出效果,使用 glPolygonMode
设置多边形的显示模式为线条。
GL_TRIANGLE_FAN
1 | glDrawArrays(GL_TRIANGLE_FAN, 0, 4); |
与“三角形条”不同的是,“三角形扇”每次保留的是首个点和上个三角形的最后一个点。0,1,2 为三角形一,0,2,3 为三角形二。
“三角形条” 和 “三角形扇”的用途非常广泛,合理利用能节省很多顶点空间。特别是GL_TRIANGLE_FAN
,上面的例子就是使用 4 个顶点来绘制一个矩形,如果使用 GL_TRIANGLES
来绘制,则需要 6 个顶点。GL_TRIANGLE_FAN
还可以用来绘制圆形,看下面例子。
1 | GLfloat vertices[114] = { 0,0,0 }; |
以 38 个点绘制 36 个三角形,组成一个比较粗糙的圆形。
GL_TRIANGLE_ADJACENCY
和 GL_TRIANGLE_STRIP_ADJACENCY
应用相对来讲不是很广,也比较复杂,这里就不演示了,绘制方法可以参考上面的图。
绘制四边形
虽然三角形能组成任意图形,OpenGL 还是提供了直接绘制四边形的方法。
1 | glDrawArrays(GL_QUADS, 0, 4); |
对比这个四边形和上面的四边形,上面的四边形是由两个三角形组成的,中间有一条斜,而这个四边形直接由四个顶点组成。