D2b476def29e3b0774e73b3ac6ce7060
Learn OpenGL 2-3. “优雅地”地画一个三角形和一个长方形

预备知识

Vertex Array Object

Vertex Array Object: 顶点数组对象,VAO

第一眼看到这个命名,可能会跟 Vertex Array 联系起来。然而它实际上和顶点数组完全没有关系,它也不是 Buffer Object,所以它并不做数据存储。它的工作是状态记录,这样说可能会有点抽象。我们可以从下面的例子一步步了解到 VAO 存在的意义。

动手画画

假设现在有这样一个需求,我需要在将一个三角形和一个长方形交替渲染(如下面的视频展示)。

<p align="center">
<video id="video" controls="" preload="auto" poster="https://images.xiaozhuanlan.com/photo/2019/afc9fa64a34bd0f02085036016786e90.png">
<source id="mp4" src="https://images.xiaozhuanlan.com/photo/2019/4defcfb92b12a74c4685f1fd5e2af67c.mp4" type="video/mp4"></video>
</p>

实际上做法有很多,例如可以把顶点定义在同一个顶点数组里,然后通过 glDrawArrays 交替选择不同的顶点数据进行渲染。这里为了讲解 VAO,我们采用的是定义两个顶点数组和索引数组分别表示三角形和长方形。

不使用 VAO 的情况

定义顶点数组以及索引数组

private let rectangleVertices: [L2Vertex] = [
    // 长方形
    L2Vertex(position: (0.5, 0, 0)),
    L2Vertex(position: (-0.5, 0, 0)),
    L2Vertex(position: (-0.5, -0.5, 0)),
    L2Vertex(position: (0.5, -0.5, 0)),
]

// 索引数组
private let rectangleIndices: [GLubyte] = [
    0, 1, 2,
    2, 3, 0
]

private let triganleVertices: [L2Vertex] = [
    // 三角形
    L2Vertex(position: (1, 0, 0)),
    L2Vertex(position: (0, 0.5, 0)),
    L2Vertex(position: (-1, 0, 0)),
]

private let triganleIndices: [GLubyte] = [
    0, 1, 2,
]

将顶点数组和索引数组输入缓冲区

由于我们采用了两个顶点数组以及索引数组,所以我们创建了两个 VBO 和 EBO 来存储对应的数据

// vertex buffer object
private var VBO1: GLuint = 0
// element buffer object
private var EBO1: GLuint = 0

// vertex buffer object
private var VBO2: GLuint = 0
// element buffer object
private var EBO2: GLuint = 0

private func setupBuffer() {

    glGenBuffers(1, &VBO1)
    glBindBuffer(GLenum(GL_ARRAY_BUFFER), VBO1)
    glBufferData(
        GLenum(GL_ARRAY_BUFFER),
        rectangleVertices.elementsSize,
        rectangleVertices,
        GLenum(GL_STATIC_DRAW)
    )

    glGenBuffers(1, &EBO1)
    glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), EBO1)
    glBufferData(
        GLenum(GL_ELEMENT_ARRAY_BUFFER),
        rectangleIndices.elementsSize,
        rectangleIndices,
        GLenum(GL_STATIC_DRAW)
    )

    glGenBuffers(1, &VBO2)
    glBindBuffer(GLenum(GL_ARRAY_BUFFER), VBO2)
    glBufferData(
        GLenum(GL_ARRAY_BUFFER),
        triganleVertices.elementsSize,
        triganleVertices,
        GLenum(GL_STATIC_DRAW)
    )

    glGenBuffers(1, &EBO2)
    glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), EBO2)
    glBufferData(
        GLenum(GL_ELEMENT_ARRAY_BUFFER),
        triganleIndices.elementsSize,
        triganleIndices,
        GLenum(GL_STATIC_DRAW)
    )
}

渲染

提示:请随时提醒自己 OpenGL 实际是一个巨大的状态机。
在渲染过程中,我们可以通过不断切换当前缓冲区来实现交替渲染不同数据的效果。记住,需要同时切换顶点缓冲对象和索引缓冲对象,以及重新传输顶点数据。

override func glkView(_ view: GLKView, drawIn rect: CGRect) {

    glClearColor(0, 0, 0, 1)
    glClear(GLenum(GL_COLOR_BUFFER_BIT))

    // 调用执行的着色器
    effect.prepareToDraw()

    // 切换缓冲区
    if count % 2 == 0 {
        curIndices = rectangleIndices
        glBindBuffer(GLenum(GL_ARRAY_BUFFER), VBO1)
        glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), EBO1)
    } else {
        curIndices = triganleIndices
        glBindBuffer(GLenum(GL_ARRAY_BUFFER), VBO2)
        glBindBuffer(GLenum(GL_ELEMENT_ARRAY_BUFFER), EBO2)
    }

    count += 1

    // 重传数据
    setupVertexAttributes()

    // 绘制
    glDrawElements(
        GLenum(GL_TRIANGLES),
        GLsizei(curIndices.count),
        GLenum(GL_UNSIGNED_BYTE),
        nil
    )
}

从上面代码可以看到,如果需要在不同缓冲区间切换,会有大量的模板代码存在。这时候,我们就可以使用 VAO 来存储状态信息,在渲染过程中需要切换不同 VAO 就可以达到切换缓冲数据的目的。

使用 VAO 的情况

顶点/索引数据的声明都跟上面一致,不同的地方是绑定缓冲区以及渲染时候执行的代码。

将顶点数组和索引数组输入缓冲区

// vertex array object
private var VAO1: GLuint = 0

// vertex array object
private var VAO2: GLuint = 0

private func simpleSetupBuffer() {

    glGenVertexArraysOES(1, &VAO1)
    glBindVertexArrayOES(VAO1)

    glGenBuffers(1, &VBO1)
    ...
    setupVertexAttributes()

    glBindVertexArrayOES(0)

    glGenVertexArraysOES(1, &VAO2)
    glBindVertexArrayOES(VAO2)

    glGenBuffers(1, &VBO2)
    ...

    setupVertexAttributes()

    glBindVertexArrayOES(0)
}

可以看到,我们新增的代码实际上只有 glGenVertexArraysOES 和 glBindVertexArrayOES,这和创建缓冲区的代码是类似的,然后将 VBO 和 EBO 绑定的代码“包在” VAO 中。
我们可以这样理解:绑定 VAO 后,下面的绑定 VBO、EBO 的操作代码会“绑定”到当前 VAO 中,然后在渲染的时候,使用对应的 VAO,就相当于“重放”你绑定 VBO、EBO 的逻辑。

1. glGenVertexArraysOES(GLsizei n, GLuint *arrays)
与 glGenBuffers 类似
2. glBindVertexArrayOES(GLuint array)
与 glBindBuffer 类似,不同点在于不需要传输存储的类型

glBindVertexArrayOES 传 0 代表解绑当前 VAO。

渲染

这时候渲染的代码就变得简单得多了,我们只需要切换不同 VAO 就可以达到我们的目的,而不需要重复指定 VBO、EBO和重传数据的逻辑

override func glkView(_ view: GLKView, drawIn rect: CGRect) {
    ...

    // 切换缓冲区
    if count % 2 == 0 {
        curIndices = rectangleIndices
        glBindVertexArrayOES(VAO1)
    } else {
        curIndices = triganleIndices
        glBindVertexArrayOES(VAO2)
    }

    count += 1

    // 绘制
    glDrawElements(
        GLenum(GL_TRIANGLES),
        GLsizei(curIndices.count),
        GLenum(GL_UNSIGNED_BYTE),
        nil
    )

    glBindVertexArrayOES(0)
}

如果我们的代码一切都正常,那我们就看到开头视频中的效果。
上面的代码可以在 https://github.com/xurunkang/learnopengl 中找到。

小结

  • 了解到如何使用 VAO 来达到简化代码的目的。

延伸阅读

如果你对 VBO、EBO 和 VAO 的概念还不太熟悉,可以看一下这篇文章 VAO 与 VBO 的前世今生

© 著作权归作者所有
这个作品真棒,我要支持一下!
🧀 专栏介绍 - 记录日常开发和学习中遇到的知识/问题 🚀 专栏方向 目前专注于 - OpenGL - ...
0条评论
top Created with Sketch.