【OpenGL】绘制基础图元

glew 库可以自动管理 OpenGL 扩展,每个 OpenGL 程序都应该在程序开始的地方引入这个库。OpenGL 提供基础图元的绘制方法,包括点、线段、三角形、四边形,任何复杂的图元都可以转换成三角形进行绘制。OpenGL 基于状态,每个 GL Object 都有自己的一套状态,全局环境 GL Context 也有一套状态,对象绘制前先绑定到上下文。

使用 glew

glew 全称是 OpenGL Extension Wrangler Library,它能够帮忙解决 OpenGL 不断扩展的问题。初始化 glew 之后,它将查询系统上所有可用的扩展功能并自动加载它们,然后提供一个头文件作为接口,我们直接通过头文件就可以使用这些扩展功能。

下载 glew,下载之后进行解压,得到以下目录结构。

1
2
3
4
5
|-glew-2.1.0
|-bin
|-lib
|-include
|-doc

接下来开始配置环境,配置的方式有两种,第一种方式跟之前配置 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
2
3
4
5
6
7
int main(int argc, char **argv)
{
char *GL_version=(char *)glGetString(GL_VERSION);
char *GL_vendor=(char *)glGetString(GL_VENDOR);
char *GL_renderer=(char *)glGetString(GL_RENDERER);
return 0;
}

这里是想获取一些系统信息,但得到的结果却全是空,这里因为此时还没有创建 context,OpenGL 相当于不存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main(int argc, char** argv)
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA);

glutInitWindowSize(400, 400);
glutInitWindowPosition(100, 100);
glutCreateWindow("Create Dot");

//获取 OpenGL 版本号
char *GL_version = (char *)glGetString(GL_VERSION);
//获取本机提供 GL 支持的公司
char *GL_vendor = (char *)glGetString(GL_VENDOR);
//获取渲染器的名称
char *GL_renderer = (char *)glGetString(GL_RENDERER);
//获取着色器的版本号
char* GL_shader_version = (char*)glGetString(GL_SHADING_LANGUAGE_VERSION);
//获取本机硬件支持的最大顶点属性数
GLint max_vertex_attrib;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &max_vertex_attrib);

return 0;
}

这样就可以正常获取信息了,因为 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 objectscontainer 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 的状态进行渲染。绘制图形的时候需要两个最基础的对象 VBOVAO

VBO,即 Vertex Buffer Object,顶点缓冲对象,用于在缓存区保存顶点数据。VBO 对象创建后要给其填充数据。

1
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

VAOVertex Array Object,顶点数组对象(亦或叫顶点属性对象),它不保存顶点数据,而是保存了顶点数据的格式和所需 buffer 对象的引用。

顶点的每一个状态属性都是可以开启和关闭的,只有开启的顶点属性在绘制的时候才会生效。

1
2
void glEnableVertexAttribArray(GLuint index);
void glDisableVertexAttribArray(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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void init()
{
//定义数据
GLfloat vertices[] = {
0,0,0,
0, 0.5f, 0,
0.1f, 0.5f, 0,
0.1f, 0, 0,
0.2f, 0, 0,
0.2f, 0.5f, 0,
0.3f, 0.5f, 0,
0.3f, 0, 0
};

//创建 buffer 对象
GLuint VBO;
glGenBuffers(1, &VBO);
//绑定 buffer 对象
glBindBuffer(GL_ARRAY_BUFFER, VBO);
//填充 buffer 的值
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

//开启顶点属性
glEnableVertexAttribArray(0);
//指定属性使用的 buffer
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

// 设置顶点大小
glPointSize(15);
}

void display()
{
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_POINTS, 0, 1);
glutSwapBuffers();
}

初始化

  • 创建顶点缓冲对象 VBO,把 VBO 绑定到 GL Context,给顶点缓冲对象填充顶点数据。
  • 创建顶点数组对象 VAO,把 VAO 绑定到 GL Context,链接顶点属性,即指定顶点哪个属性用 VBO 中的哪个数值,最后顶点属性必须开启才能生效。

    这里省略 VAO 的创建和绑定,使用了全局的默认 VAO 对象,详细的后面再讲。

  • glPointSize 设置顶点大小,也就是一个点占用几个像素。

绘制

绘制的时候是基于 GL Context 状态,初始化的时候已经将 VBO 和 VAO 绑定到 GL Context 了,所以在绘制函数中直接绘制即可。

glDrawArrays 绘制基础图元:

  • 参数一指定要绘制的图元类型,GL_POINTS 就是绘制点;
  • 参数二指定从第几个顶点数据开始使用;
  • 参数三指定要使用几个顶点的数据,在这里也就是绘制几个点。

这里我们从第 0 个点开始绘制,只绘制一个点,结果如下。

绘制一个点

绘制线段

1
2
3
4
5
6
7
8
9
void init()
{
glLineWidth(2);
}

void display()
{
glDrawArrays(GL_LINES, 0, 8);
}

首先使用 glLineWidth 设置线段宽度。

绘制线段只需要把图元类型设置为线段即可,0 仍表示从第 0 个顶点开始绘制,8 表示使用 8 个顶点,注意不是指绘制 8 条线,无论绘制什么图元,这个参数的意思都是使用的顶点个数。

不同于点的单一,线段是由多个顶点确定的图元,那顶点之间的组合方式就有多种,GL_LINES, GL_LINES_ADJACENCY, GL_LINE_STRIP, GL_LINE_STRIP_ADJACENCY, GL_LINE_LOOP 都表示线段。下图很好地解释了各种图元是怎么绘制的。

primitive

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_LOOPGL_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
2
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

“三角形条”每次都保留上个三角形的最后两个点,再增加一个点组成新的三角形。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
2
3
4
5
6
7
8
9
GLfloat vertices[114] = { 0,0,0 };
for (int i = 0; i <= 36; i++ )
{
vertices[(i + 1) * 3] = cos(i * 10 * 3.14 / 180) * 0.8;
vertices[(i + 1) * 3 + 1] = sin(i * 10 * 3.14 / 180) * 0.8;
vertices[(i + 1) * 3 + 2] = 0;
}

glDrawArrays(GL_TRIANGLE_FAN, 0, 38);

以 38 个点绘制 36 个三角形,组成一个比较粗糙的圆形。

圆形

GL_TRIANGLE_ADJACENCYGL_TRIANGLE_STRIP_ADJACENCY 应用相对来讲不是很广,也比较复杂,这里就不演示了,绘制方法可以参考上面的图。

绘制四边形

虽然三角形能组成任意图形,OpenGL 还是提供了直接绘制四边形的方法。

1
glDrawArrays(GL_QUADS, 0, 4);

四边形

对比这个四边形和上面的四边形,上面的四边形是由两个三角形组成的,中间有一条斜,而这个四边形直接由四个顶点组成。