OpenGL学习笔记 (二)- 缓冲与绘制

前一篇文章( OpenGL学习笔记 (一)- 综述、渲染管线 )提到过,现代OpenGL不再推荐使用显示列表或者更古老的glVertex了。这篇笔记将详细探讨这个话题,并介绍几何图形的绘制方式。

几何图元

OpenGL中有若干几何图元,但是最终这些图元都会被转化为点、线和三角形。通过组合三角形,OpenGL还额外提供了条带和扇面。

在OpenGL中,多边形区分正面与背面。默认情况下,两面的绘制方式相同。但是可以通过glPolygonMode来变更为点集、轮廓线和填充模式(默认)。

绘制多边形时,我们除了需要给出顶点坐标之外,还需要指定顶点之间的连接方式。OpenGL采用了数学中“正向”的概念,也就是说对于(凸)多边形的正面,从屏幕上观察,它的顶点是以逆时针排列的。

顶点(vertex)实际上就是坐标,是几何图元的组成部分。在OpenGL中,使用四个分量(齐次坐标)来描述一个位置。不过,一个顶点还可以同时具备其他的数据,比如顶点处的法向量、对应的纹理坐标等等。

OpenGL缓冲

现代OpenGL广泛应用缓冲。通过缓冲,我们可以把诸如顶点数据等等的数据放置在图形硬件的高速存储器(又叫显存)中,供后续绘制等操作使用。因此OpenGL中有若干不同类型的缓冲,缓冲管理也有一个通用的接口。使用glGenBuffers(新版本中还提供了glCreateBuffers)可以创建一个缓冲对象,之后必须glBindBuffer来绑定这个缓冲对象。

创建缓冲对象时,并不需要给出缓冲对象的大小。实际上,glGenBuffers的作用是返回缓冲对象名称。

绑定对象时,我们把一个缓冲对象绑定到一个确定的目标上。目标可以是GL_ARRAY_BUFFER代表的顶点数据类型等等( 详表参考 )。这里要注意,我们实际上不是给缓冲类型,而是把缓冲绑定在一个目标上。因此,glBindBuffer是一个状态函数,一个目标也只能绑定一个缓冲。

之后我们就需要使用glBufferData来真正的传输数据了。glBufferData的对象并不是缓冲对象名称,而是一个目标。比如对GL_ARRAY_BUFFER写入,那实际上当前绑定GL_ARRAY_BUFFER的缓冲对象才是真正的目标缓冲。这样写入缓冲的确有点不太直观,因此在新版OpenGL中提供了glNamedBufferStorage和相关函数来直接对缓冲对象名称写入数据,不过由于实在是太新了,因此这个函数的兼容性并不好。

顶点缓冲对象

顶点缓冲对象(Vertex Buffer Object,后略VBO)是用来存储顶点数据的缓冲对象。它的创建就如之前提到的:

  1. glGenBuffers:创建缓冲对象名称
  2. glBindBuffer:绑定缓冲,类型为GL_ARRAY_BUFFER
  3. glBufferData:传输数据

结合一段实际创建的代码可以更好的理解这个过程:

// Create VBO, VAO
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

不难看出,VBO仅仅是将所有二进制顶点数据储存的缓冲对象。VBO之内的顶点数据实际上没有语义,只是以二进制的形式缓存,故VBO不可以直接进行绘制。

顶点数组对象

顶点数组对象(Vertex Array Object,VAO)就是存储顶点数据的数组,显然其中的数据已经有其组织形式了,所以VAO可以直接用于绘制指令。VAO的创建类似于VBO,可以通过glGenVertexArrays创建VAO,并通过glBindVertexArray进行绑定。

由于现代OpenGL的顶点数据都存在于缓冲对象中(曾经可以使用glVertexPointer函数),所以现在VAO已经不自带数据了,因此它需要绑定一个VBO。同时,我们需要给出解释VBO数据的方法,也就是顶点属性指针(vertex attribute pointer)。通过glVertexAttribPointer函数,我们可以向当前的VAO中添加顶点属性指针。glVertexAttribPointer定义顶点属性的方法是通过长度、步长和偏移。比如对于若干个紧密排列的三维空间的坐标(格式:x1 y1 z1 x2 y2 z2 …,参考下图),显然他们的长度是3,步长是3个数据的长度,偏移是0。

顶点属性指针例(图源Reference)

需要注意的是,glVertexAttribPointer的第一个参数接受一个index。由于一个顶点可以有若干属性,比如位置、纹理坐标等等,因此这个index就是用来区别不同顶点属性的。最后通过glEnableVertexAttribArray就可以启用这个配置了。VAO、VBO和顶点属性指针的关系可以参考下图。

VAO、VBO与顶点属性指针(图源Reference)

可以看到,真正绑定VBO的并不是VAO,而是相应的顶点属性指针。而VAO可以绑定多个顶点属性指针(只要index不同),一个VAO事实上可以同时“绑定”多个VBO。这个连接建立的时机是glVertexAttribPointer函数的调用。另外,由于VAO和VBO没有直接的关系,因此VAO、VBO绑定的先后顺序并不重要,只要都先于glVertexAttribPointer即可。

数据布局

了解了VAO、VBO与顶点属性指针的内容之后,就可以处理不同样式的数据布局了。我们假设现在有三种顶点属性:位置(3分量,用P表示)、颜色(3分量,用C表示)、纹理坐标(2分量,用T表示)。

最暴力的一种方式就是把三个数据分开存放在三个VBO中,大致可以表示为:

[P P P …] … [C C C …] … [T T T …]

虽然很直观,不过这样存储的话代码量较大(因为有三个数组),而且由于数据的分次传递,其效率也不太高。因此,我们可以将这些数据相邻放置,并存储在一个VBO里。

P P P … C C C … T T T …

为了进一步提高效率,我们还可以对数据块进行对齐(在三个块之前)。不过这样带来的问题是,在制定顶点属性指针时我们就需要预先知道数据的长度以计算偏移。这会使我们的代码丧失一定的灵活性。因此,我们还可以将数据交叉存储。

P C T   P C T   P C T …

交叉存储下,一个顶点的各个属性数据都是连贯的。因此我们就不需要知道数据长度来计算偏移了,我们可以通过下图来了解这个结构。

交叉存储的顶点数据(图源Reference)

不过交叉存储也是有缺点的。在对齐的时候,交叉存储会消耗更多空间。此外,交叉存储是否能提升效率还有待数据验证。

绘制指令

OpenGL中以glDraw开头的就是绘制指令。虽然glDraw开头的函数众多,不过它们大致可以分为以glDrawArrays和glDrawElements为首的两族。所有绘制指令的对象都是VAO,因此在绘制前程序需要绑定一个正确的VAO。同时,绘制时需要传入一个模式以确定如何组装顶点为图元,可被接受的就是“几何图元”节中提到的。

glDrawArrays一族直接对缓冲内的数据进行绘制。因为直接使用缓冲内的数据,因此只需要给出首个顶点偏移与所用顶点数即可。一个使用glDrawArrays进行绘制的完整例子如下。

// 准备数据
glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);
 
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 
glBindVertexArray(VAO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
 
// 绘制时
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3); // 绘制一个三角形

glDrawElements一族是索引形式的绘制指令,其通过索引访问缓冲内数据进行绘制。因此我们还需要传入索引的数据类型(如无符号整数GL_UNSIGNED_INT)。另外,在使用glDrawElements之前,还需要给VAO绑定一个索引,在下一节中将会进行详细说明。

使用索引进行绘制的意义在于减少重复数据。在绘制中,经常会遇到两个顶点相同的情况(比如正方体的顶点),使用索引可以减少重复数据点,节省存储空间。

索引缓冲对象

索引缓冲对象(Element Buffer Object,后略EBO;或Index Buffer Object,IBO)就是存放绘制需要的索引所需要的。由于EBO本质上还是一个缓冲对象,因此它的创建和VBO几乎完全相同:

  1. glGenBuffers:创建缓冲对象名称
  2. glBindBuffer:绑定缓冲,类型为GL_ELEMENT_ARRAY_BUFFER
  3. glBufferData:传输数据

唯一不同的是,EBO的格式固定,每一位就代表了一个索引,因此不需要给出数据格式。EBO可以理解为阅读VAO顶点数据的顺序,因此需要绑定给VAO,绑定的过程是在glBindBuffer发生的。一个使用了EBO的绘制示例如下。

// 准备数据
glGenBuffers(1, &VBO);
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &EBO);
 
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 
glBindVertexArray(VAO);
 
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
 
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
 
// 绘制时
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 绘制两个三角形

状态对象

状态对象实际上是我硬造的词,用来描述OpenGL中一些仅存储状态的对象。VAO就是状态对象。通常来讲,VAO存储了:

  1. 顶点属性指针的调用参数,可以看成VBO的引用+读取格式
  2. glEnableVertexAttribArray和glDisableVertexAttribArray的调用,也就是顶点属性指针的启用与否
  3. 当前GL_ELEMENT_ARRAY_BUFFER目标的缓冲对象,也就是EBO的引用

VAO关系图示(图源Reference)

了解了VAO的细节之后,应该能更好的对数据进行管理,并充分复用现有的数据了。

Reference

  1. OpenGL编程指南(原书第9版)
  2. OpenGL Vertex Buffer Object( http://www.songho.ca/opengl/gl_vbo.html
  3. LearnOpenGL CN( https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
  4. VAO & binding multiple VBOS( https://community.khronos.org/t/vao-binding-multiple-vbos/77291
  5. Vertex Specification – OpenGL Wiki( https://www.khronos.org/opengl/wiki/Vertex_Specification#Vertex_Array_Object
我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章