OpenGL ES 高级进阶:VBO和IBO

7,168 阅读6分钟

大家好,这是我的OpenGL ES 高级进阶系列文章,在我的github上有一个与本系列文章对应的项目,欢迎关注,链接:github.com/kenneycode/…

今天给大家介绍VBO(Vertex Buffer Object)IBO(Index Buffer Object),让我们先从一段代码开始,逐步介绍它们:

// 将三角形顶点数据放入buffer中
// Put the triangle vertex data into the vertexDataBuffer
vertexDataBuffer = ByteBuffer.allocateDirect(vertexData.size * java.lang.Float.SIZE / 8)
    .order(ByteOrder.nativeOrder())
    .asFloatBuffer()
vertexDataBuffer.put(vertexData)
vertexDataBuffer.position(0)

// 启动对应位置的参数,这里直接使用LOCATION_ATTRIBUTE_POSITION,而无需像OpenGL 2.0那样需要先获取参数的location
// Enable the parameter of the location. Here we can simply use LOCATION_ATTRIBUTE_POSITION, while in OpenGL 2.0 we have to query the location of the parameter
GLES30.glEnableVertexAttribArray(LOCATION_ATTRIBUTE_POSITION)

// 指定a_position所使用的顶点数据
// Specify the data of a_position
GLES30.glVertexAttribPointer(LOCATION_ATTRIBUTE_POSITION, VERTEX_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertexDataBuffer)

这段代码大家应该很熟悉了,它的作用就是将顶点数据指定给vertex shader中的attribute变量,这里有个细节之前没提到过,就是glVertexAttribPointer()这个方法指定了顶点数据,但它不会将顶点数据一直存储在显存中,而我们渲染的时候,所有要访问的数据必需在显存中,因此,每次渲染时,OpenGL会有一个将这些顶点数据从内存复制到显存的操作,这样会带来一些问题:

  • 一个是因此每次渲染都要复制一次,因此内存中的顶点数据要一直留着,不然复制的时候就没有数据来复制了。

  • 另一个是如果顶点数据量大的时候,每次渲染都做这样的一次复制,性能上会有问题,我们的例子中,顶点算是非常少的,那什么时候顶点会多呢?例如做一些形变效果时,往往会划分网格,一般来说划分得越多,效果越细腻,这时顶点就会很多,另外,在做3D渲染时,通常顶点也很多,单个3D模型甚至可以有上万个顶点。

那有什么办法来避免复制?这时就要用到VBO,它可以和顶点数据绑定,绑定后的顶点数据是一直存储在显存中的,当需要用这些顶点数据的时候,直接绑定这个VBO就行了,不会有复制过程。

IBO又是什么呢?它和VBO作用很类似,VBO是为了避免顶点数据的复制,IBO是则是为了避免顶点索引数据的复制,什么是顶点索引呢?我们先来看一份顶点数据:

// 三角形顶点数据
// The vertex data of a triangle
private val vertexData = floatArrayOf(
                            -1f, -1f,   // 左下角
                            -1f, 1f,    // 左上角
                            1f, 1f,     // 右上角
                            -1f, -1f,   // 左下角
                            1f, 1f,     // 右上角
                            1f, -1f     // 右下角
                        )                       
// 顶点索引数据
// The vertex data of triangles
private val indexData = intArrayOf(0, 1, 2, 0, 2, 3)

这是一份很普通的顶点数据,在我们的教程中多次用到过,它用2个三角形组成了一个矩形,对应用GL_TRIANGLES的绘制模式进行渲染(绘制模式可参考我的一篇文章《Android OpenGL ES 2.0 手把手教学(5)- 绘制模式》),我们可以很容易地看到,这6个顶点是有重复的,一个矩形只需要4个顶点就行了,有些点是不同三角形之间共用的,那么如何让不同三角形之间共用?这就要用到顶点索引,它能让我们用索引的方法告诉OpenGL我们的顶点,而不是每个点都用坐标的方式给出,这样可以减少我们的顶点数据量,这在面片数量较大时很有用。

那么下面我们来看看具体如何使用VBOIBO,先来看看顶点数据和顶点索引数据:

// 三角形顶点、纹理数据
// The vertex data and texture coordinate data of triangles
private val vertexData = floatArrayOf(
                            -1f, -1f,   0f, 1f,     // x, y, u, v
                            -1f, 1f,    0f, 0f,
                            1f, 1f,     1f, 0f,
                            1f, -1f,    1f, 1f
                        )

这里我们将顶点和纹理坐标组合越来,这也是配合VBOIBO的常规优化用法,它的好处让顶点和纹理坐标在存储上靠近,利于OpenGL取数据,提高性能,特别是在3D渲染时,数据一般都是这样组织的。

接下来用glGenBuffers创建VBOIBO:

// 创建VBO和IBO
// Create VBO and IBO
val buffers = IntArray(2)
GLES30.glGenBuffers(buffers.size, buffers, 0)
vbo = buffers[0]
ibo = buffers[1]

我们可以看到,在创建的时候其实并没有VBOIBO的区别,都是叫buffer,它们是在真正用的时候才能体现出区别。

下面将顶点的纹理数据加载到VBO中:

// 将顶点数据载入VBO
// Load vertex data into VBO
val vertexDataBuffer = ByteBuffer.allocateDirect(vertexData.size * java.lang.Float.SIZE / 8)
                                    .order(ByteOrder.nativeOrder())
                                    .asFloatBuffer()
vertexDataBuffer.put(vertexData)
vertexDataBuffer.position(0)
GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo)
GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, vertexDataBuffer.capacity() * java.lang.Float.SIZE / 8, vertexDataBuffer, GLES30.GL_STATIC_DRAW)
GLES30.glEnableVertexAttribArray(LOCATION_ATTRIBUTE_POSITION)
GLES30.glEnableVertexAttribArray(LOCATION_ATTRIBUTE_TEXTURE_COORDINATE)
GLES30.glVertexAttribPointer(LOCATION_ATTRIBUTE_POSITION, 2, GLES30.GL_FLOAT, false, 16, 0)
GLES30.glVertexAttribPointer(LOCATION_ATTRIBUTE_TEXTURE_COORDINATE, 2, GLES30.GL_FLOAT, false, 16, 8)

主要的关键点是先用glBindBuffer绑定我们在操作的buffer,这一点和操作textureframe buffer时很像,操作前都先绑定。接着用glBufferData给它喂数据,这里最后一个参数用于提示OpenGL以便于它做一些优化,例如这里我们传了GL_STATIC_DRAW,即我们的数据是不会变的,还有一些其它的可以设置,如GL_DYNAMIC_DRAW则提示OpenGL这个buffer的数据是会变的。

glVertexAttribPointer指定顶点和纹理数据时,我们不再像之前那样,直接把数据的传进来,因为这时我们的数据已经在VBO中了,这里不需要再传,这里重点关注最后两个参数,倒数第二个参数是指定stride,即OpenGL去取一份数据时的跨度,这里因为我们把顶点和纹理数据组合在了一起,因此一份数据是2个顶点和2个纹理坐标,即4个float,16个字节。倒数第一个参数是指定这个数据在一份数据中的开始位置,因为我们在一份数据中是先放顶点再放纹理坐标,因此对于顶点,开始位置是0,对于纹理坐标,开始位置是第8个字节。

这样我们就设置好了VBOIBO,在渲染的时候,直接绑定VBOIBO就可以使用对应的顶点和纹理数据了:

override fun onDrawFrame(gl: GL10?) {

    // 设置清屏颜色
    // Set the color which the screen will be cleared to
    GLES30.glClearColor(0.9f, 0.9f, 0.9f, 1f)

    // 清屏
    // Clear the screen
    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)

    // 设置视口,这里设置为整个GLSurfaceView区域
    // Set the viewport to the full GLSurfaceView
    GLES30.glViewport(0, 0, glSurfaceViewWidth, glSurfaceViewHeight)

    // 设置好状态,准备渲染
    // Set the status before rendering
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo)
    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, ibo)
    GLES30.glActiveTexture(GLES30.GL_TEXTURE0)
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, imageTexture)

    // 调用draw方法用TRIANGLES的方式执行渲染,顶点数量为3个
    // Call the draw method with GL_TRIANGLES to render 3 vertices
    GLES30.glDrawElements(GLES30.GL_TRIANGLES, indexData.size, GLES30.GL_UNSIGNED_INT, 0)

}

来看看效果,就是渲染出一张图来,从效果上看不出什么区别哈:

代码在我githubOpenGLESPro项目中,本文对应的是SampleVBOAndIBO,项目链接:github.com/kenneycode/…

感谢阅读!