• Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序


    OpenGL 学习教程
    Android OpenGL ES 学习(一) – 基本概念
    Android OpenGL ES 学习(二) – 图形渲染管线和GLSL
    Android OpenGL ES 学习(三) – 绘制平面图形
    Android OpenGL ES 学习(四) – 正交投屏
    Android OpenGL ES 学习(五) – 渐变色
    Android OpenGL ES 学习(六) – 使用 VBO、VAO 和 EBO/IBO 优化程序
    Android OpenGL ES 学习(七) – 纹理
    代码工程地址: https://github.com/LillteZheng/OpenGLDemo.git

    上一篇 Android OpenGL ES 学习(五) – 渐变色 ,我们已经完成了 三角形的渐变色处理。

    这一章,我们学习 GL3.0 特有的 OpenGL 缓存对象,VBO ,VAO 和 EBO。这一章稍微有点吃理解,建议多看多想。

    一. VBO

    顶点缓冲对象:Vertex Buffer Object,VBO
    为什么有这个对象?从Android OpenGL ES 学习(二) – 图形渲染管线和GLSL 这章知道,图形渲染管线的顶点数据是在 CPU 上的,我们需要调用 GL 的指令,把这些数据加载到 GPU 中,每绘制一个顶点,都需要加载一次。
    当数据小的时候可以忽略不计,但如果有非常大的数据,频繁的在 CPU 和 GPU 之间传递呢,比如渲染图片,视频。

    再比如,我们的渐变色三角形,就18个顶点数据,我们会把它作为输入发送给图形渲染管线的顶点着色器,它会在 CPU 上创建内存,并存储这些顶点数据,然后再把它传给 CPU 。
    但如果不止 18 点呢,这里有上万点呢,这无疑是很耗性能的。所以,GL在 3.0 版本,引入 VBO 这个顶点缓存对象,它会在 GPU 内存(显存) 中存储大量顶点,然后我们可以一次性把大批数据发送给显存,而不是每个顶点发送一次;

    从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。

    了解原理,看看怎么使用。

    1.1 使用 VBO

    VBO 的使用非常简单,步骤如下:

    1. 创建缓存区:glGenBuffers
    2. 绑定缓存区到上下文: glBindBuffer
    3. 将顶点数据存在缓冲区: glBindData
    4. 指定如何解析顶点属性数组:glVertexAttribPointer
    5. 绘制:glDrawArrays

    创建:

    //创建缓存区
    val vbo = IntArray(1)
    GLES30.glGenBuffers(1,vbo,0)
    //绑定缓存区到上下文
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,vbo[0])
    //将顶点数据存在缓冲区
    GLES30.glBufferData(
        GLES30.GL_ARRAY_BUFFER,
        vertexData.capacity() * 4,
        vertexData,
        GLES30.GL_STATIC_DRAW)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    主要看 glBufferData 这个方法,它是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数,参数如下:

    1. buffer 对象类型: 有GL_ARRAY_BUFFER,GL_ELEMENT_ARRAY_BUFFER,GL_SHADER_STORAGE_BUFFER 等等。
    2. 传输数据大小((以字节为单位)): 填写 buffer 大小,由于是 float类型,乘以4
    3. 实际数据
    4. 告诉显卡如果管理给定的数据,它有三种形式
      • GL_STATIC_DRAW :数据不会或几乎不会改变
      • GL_DYNAMIC_DRAW:数据会被改变很多。
      • GL_STREAM_DRAW :数据每次绘制时都会改变。

    因为三角形的数据不会改变,每次都一样,所以使用 GL_STATIC_DRAW。

    使用数据

    //绘制位置,注意这里,我们不再填入 vertexData,而是填入数据偏移地址
    GLES30.glVertexAttribPointer(
        0, 3, GLES30.GL_FLOAT,
        false, 24, 0
    )
    GLES30.glEnableVertexAttribArray(0)
    
    //绘制颜色,颜色地址偏移量从3开始,前面3个为位置
    vertexData.position(3)
    GLES30.glVertexAttribPointer(
        1, 3, GLES30.GL_FLOAT,
        false, 24, 12 //需要指定颜色的地址 3 * 4
    )
    GLES30.glEnableVertexAttribArray(1)
    
    //解绑数据,因为我们不需要动态更新
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    注意绘制索引时 glVertexAttribPointer,我们不再填入顶点内存数据,因为我们已经把数据关联到 VBO 上了,所以填入数据偏移地址。
    由于位置是首位,所以偏移地址是0,颜色是 3 * 4.

    绘制
    绘制也比较简单,直接使用 vbo 就可以了。

    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,vbo[0])
    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP,0,3)
    
    • 1
    • 2

    效果如下,熟悉的三角形:
    在这里插入图片描述

    二. VAO

    顶点数组对象:Vertex Array Object,VAO
    前面说道 VBO 会把顶点数据存到 GPU 显存中,但怎么使用呢?并没有人管理,GL 并不知道怎么去拿这些数据。
    比如,我们有这个一个场景,要画两个三角形,构成一个矩形,按照我们上面的做法,也需要两个 VBO 。

    顶点数据:

    //第一个三角形
     private val POINT_COLOR_DATA = floatArrayOf(
         // positions         // colors
         0.5f, 0.5f, 0.0f,   1.0f, 0.5f, 0.5f,// 右上角
         0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 1.0f,// 右下角
         -0.5f, 0.5f, 0.0f,  0.0f, 0.5f, 1.0f,// 左上角
     )
     //第二个三角形
     private val POINT_COLOR_DATA2 = floatArrayOf(
         // positions         // colors
         0.5f, -0.5f, 0.0f,  1.0f, 0.5f, 0.5f,// 右下角
         -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f,// 左下角
         -0.5f, 0.5f, 0.0f,   0.0f, 0.5f, 1.0f,// 左上角
     )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    创建两个vbo

      val vbo = IntArray(2)
    private fun useVaoVbo(){
       
        //绑定第一个 vbo
        val vertexData = BufferUtil.createFloatBuffer(POINT_COLOR_DATA)
        GLES30.glGenBuffers(2,vbo,0)
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,vbo[0])
        GLES30.glBufferData(
            GLES30.GL_ARRAY_BUFFER,
            vertexData.capacity() * 4,
            vertexData,
            GLES30.GL_STATIC_DRAW)
        //绘制位置
        GLES30.glVertexAttribPointer(
            0, 3, GLES30.GL_FLOAT,
            false, 24, 0
        )
        GLES30.glEnableVertexAttribArray(0)
    
        //绘制颜色,颜色地址偏移量从3开始,前面3个为位置
        vertexData.position(3)
        GLES30.glVertexAttribPointer(
            1, 3, GLES30.GL_FLOAT,
            false, 24, 12 //需要指定颜色的地址 3 * 4
        )
        GLES30.glEnableVertexAttribArray(1)
        //解绑数据
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0)
       
    
    
        //绑定第二个 vbo
        val vertexData2 = BufferUtil.createFloatBuffer(POINT_COLOR_DATA2)
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,vbo[1])
        GLES30.glBufferData(
            GLES30.GL_ARRAY_BUFFER,
            vertexData2.capacity() * 4,
            vertexData2,
            GLES30.GL_STATIC_DRAW)
        //绘制位置
        GLES30.glVertexAttribPointer(
            0, 3, GLES30.GL_FLOAT,
            false, 24, 0
        )
        GLES30.glEnableVertexAttribArray(0)
    
        //绘制颜色,颜色地址偏移量从3开始,前面3个为位置
        vertexData2.position(3)
        GLES30.glVertexAttribPointer(
            1, 3, GLES30.GL_FLOAT,
            false, 24, 12 //需要指定颜色的地址 3 * 4
        )
        GLES30.glEnableVertexAttribArray(1)
    
        //解绑数据
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,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
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58

    绘制:

    //绘制第一个三角形,右上角
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,vbo[0])
    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP,0,3)
    
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述
    咦,我们的第一个顶点数据的数据为:

    //第一个三角形
    private val POINT_COLOR_DATA = floatArrayOf(
        // positions         // colors
        0.5f, 0.5f, 0.0f,   1.0f, 0.5f, 0.5f,// 右上角
        0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 1.0f,// 右下角
        -0.5f, 0.5f, 0.0f,  0.0f, 0.5f, 1.0f,// 左上角
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    应该是下图才对,
    在这里插入图片描述
    怎么变成第二个顶点的三角形形状了呢?
    因为前面我们创建了两个 VBO ,使用 glBufferData 后,VBO[0] 就发送给显卡内存了,同理 VBO[1] 也发给过去,但是GPU 在拿这些数据的时候,并不知道这些数据的地址,就拿了最新的,所以才会显示 VBO[1] 的数据。
    怎么处理呢?

    回到刚才的 VAO ,它会记录 Buffer Object 中缓存顶点属性的缓存对象的配置信息,相当于我们可以通过 VAO 准确告诉 GPU 缓存对象的地址。如下图:
    在这里插入图片描述
    它的创建跟 VBO 差不多,只不过方法不一样:

    1. 创建 VAO: glGenVertexArrays
    2. 绑定 VAO : glBindVertexArray ,需要注意,这里绑定了 VAO 后,再绑定 VBO,这样他们才能关联起来。
    3. 解绑数据:GLES30.glBindVertexArray(0)

    这样,我们修改一下上面的代码:

    private val vao = IntArray(2)
    private fun useVaoVbo(){
         val vbo = IntArray(2)
        val vertexData = BufferUtil.createFloatBuffer(POINT_COLOR_DATA)
        //创建 VAO
        GLES30.glGenVertexArrays(2,vao,0)
        // //创建 VBO
        GLES30.glGenBuffers(2,vbo,0)
        //绑定 VAO ,之后再绑定 VBO
        GLES30.glBindVertexArray(vao[0])
        //绑定VBO
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,vbo[0])
        GLES30.glBufferData(
            GLES30.GL_ARRAY_BUFFER,
            vertexData.capacity() * 4,
            vertexData,
            GLES30.GL_STATIC_DRAW)
        //绘制位置
        GLES30.glVertexAttribPointer(
            0, 3, GLES30.GL_FLOAT,
            false, 24, 0
        )
        GLES30.glEnableVertexAttribArray(0)
    
        //绘制颜色,颜色地址偏移量从3开始,前面3个为位置
        vertexData.position(3)
        GLES30.glVertexAttribPointer(
            1, 3, GLES30.GL_FLOAT,
            false, 24, 12 //需要指定颜色的地址 3 * 4
        )
        GLES30.glEnableVertexAttribArray(1)
        //解绑数据
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0)
        GLES30.glBindVertexArray(0)
    
    
        //绑定第二个 vbo
        val vertexData2 = BufferUtil.createFloatBuffer(POINT_COLOR_DATA2)
    
       // GLES30.glBindVertexArray(vao[1])
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,vbo[1])
        GLES30.glBufferData(
            GLES30.GL_ARRAY_BUFFER,
            vertexData2.capacity() * 4,
            vertexData2,
            GLES30.GL_STATIC_DRAW)
        //绘制位置
        GLES30.glVertexAttribPointer(
            0, 3, GLES30.GL_FLOAT,
            false, 24, 0
        )
        GLES30.glEnableVertexAttribArray(0)
    
        //绘制颜色,颜色地址偏移量从3开始,前面3个为位置
        vertexData2.position(3)
        GLES30.glVertexAttribPointer(
            1, 3, GLES30.GL_FLOAT,
            false, 24, 12 //需要指定颜色的地址 3 * 4
        )
        GLES30.glEnableVertexAttribArray(1)
    
        //解绑数据
        GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0)
        GLES30.glBindVertexArray(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
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

    绘制,使用 vao

    //绘制第一个三角形
    GLES30.glBindVertexArray(vao[0])
    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP,0,3)
    
    
    • 1
    • 2
    • 3
    • 4

    可以看到,我们第一个三角形可以被正确识别了
    在这里插入图片描述

    三. EBO / IBO

    元素缓冲对象:Element Buffer Object,EBO 或 索引缓冲对象 Index Buffer Object,IBO

    什么叫元素缓冲对象呢,你可以简单理解成 顶点数据复用,举个栗子:
    上面我们要画一个矩形,需要用到两个三角形,顶点如下:

    //第一个三角形
    private val POINT_COLOR_DATA = floatArrayOf(
        // positions         // colors
        0.5f, 0.5f, 0.0f,   1.0f, 0.5f, 0.5f,// 右上角
        0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 1.0f,// 右下角
        -0.5f, 0.5f, 0.0f,  0.0f, 0.5f, 1.0f,// 左上角
    )
    //第二个三角形
    private val POINT_COLOR_DATA2 = floatArrayOf(
        // positions         // colors
        0.5f, -0.5f, 0.0f,  1.0f, 0.5f, 0.5f,// 右下角
        -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f,// 左下角
        -0.5f, 0.5f, 0.0f,   0.0f, 0.5f, 1.0f,// 左上角
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    但注意到,对角线的数据,是相同的,有几个顶点叠加了,一个矩形只有4个角而不是6个,这样就产生50%的额外开销。

    当有成千上百个三角形时,就会产生一大堆浪费。

    一个比较好的解决方案,就是存储不同的顶点,并按照顺序去绘制,这样,我们只需要4个顶点,就能绘制矩形了。而 EBO 就是这样一个缓冲对象,它会存储 GL 用来决定要绘制哪些顶点的索引。
    所以,我们先定义不重复的顶点:

    private val POINT_RECT_DATA2 = floatArrayOf(
         // 矩形4个顶点
         0.5f, 0.5f, 0.0f,   1.0f, 0.5f, 0.5f,// 右上角
         0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 1.0f,// 右下角
    
         -0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 1.0f,// 左下角
         -0.5f, 0.5f, 0.0f,   0.0f, 0.5f, 1.0f,// 左上角
     )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    需要按顺序的数据:

    private val indeices = intArrayOf(
        // 注意索引从0开始!
        // 此例的索引(0,1,2,3)就是顶点数组vertices的下标,
        // 这样可以由下标代表顶点组合成矩形
    
        0, 1, 3, // 第一个三角形
        1, 2, 3  // 第二个三角形
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    它的创建也跟 VAO 特别像

    1. 创建对象:glGenBuffers
    2. 绑定对象到上下文:glBindBuffer
    3. 绑定顺序的数据:glBufferData ,根据前面的解释,这个对象参数,选择 GL_ELEMENT_ARRAY_BUFFER

    代码如下:

    //创建 ebo
    GLES30.glGenBuffers(1,ebo,0)
    //绑定 ebo 到上下文
    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER,ebo[0])
    //昂丁
    GLES30.glBufferData(GLES30.GL_ELEMENT_ARRAY_BUFFER,
        indexData.capacity() * 4,
        indexData,
        GLES30.GL_STATIC_DRAW
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    跟 VBO 和 VAO 结合后的代码:

    private fun useVaoVboAndEbo(){
    val vertexData = BufferUtil.createFloatBuffer(POINT_RECT_DATA2)
    val indexData = BufferUtil.createIntBuffer(indeices)
    
    
    //使用 vbo,vao 优化数据传递
    //创建 VAO
    GLES30.glGenVertexArrays(1,vao,0)
    // //创建 VBO
    GLES30.glGenBuffers(1,vbo,0)
    //绑定 VAO ,之后再绑定 VBO
    GLES30.glBindVertexArray(vao[0])
    //绑定VBO
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,vbo[0])
    GLES30.glBufferData(
        GLES30.GL_ARRAY_BUFFER,
        vertexData.capacity() * 4,
        vertexData,
        GLES30.GL_STATIC_DRAW)
    //创建 ebo
    GLES30.glGenBuffers(1,ebo,0)
    //绑定 ebo 到上下文
    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER,ebo[0])
    //昂丁
    GLES30.glBufferData(GLES30.GL_ELEMENT_ARRAY_BUFFER,
        indexData.capacity() * 4,
        indexData,
        GLES30.GL_STATIC_DRAW
    )
    
    //绘制位置
    GLES30.glVertexAttribPointer(
        0, 3, GLES30.GL_FLOAT,
        false, 24, 0
    )
    GLES30.glEnableVertexAttribArray(0)
    
    //绘制颜色,颜色地址偏移量从3开始,前面3个为位置
    vertexData.position(3)
    GLES30.glVertexAttribPointer(
        1, 3, GLES30.GL_FLOAT,
        false, 24, 12 //需要指定颜色的地址 3 * 4
    )
    GLES30.glEnableVertexAttribArray(1)
    
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0)
    
    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER,0)
    GLES30.glBindVertexArray(0)
    //注意顺序,ebo 要在 vao 之后
    GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER,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
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    关于 EBO 为啥要再 VAO 之后,可以参考 https://www.zhihu.com/question/39082624

    绘制:

    GLES30.glBindVertexArray(vao[0])
    GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP,6,GLES30.GL_UNSIGNED_INT,0)
    
    • 1
    • 2

    注意这里把 glDrawArrays 替换成了 glDrawElements ,表示我们要从索引缓存中渲染三角形。它的参数如下:

    1. 绘制类型,这里也是三角形 GLES30.GL_TRIANGLE_STRIP
    2. 顶点个数:6 个,实际是两个三角形
    3. 索引的类型,这里是GL_UNSIGNED_INT
    4. 最后是偏移量,这里填0即可

    glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取其索引。但前面说道,VAO 可以关联 buffer object 的索引,所以,统一使用 VAO 去关联即可。
    如下图:
    在这里插入图片描述
    可以看到,VAO 实际上关联了 VAO 和 EBO 。
    效果:
    在这里插入图片描述
    参考:
    https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/
    https://www.zhihu.com/question/39082624
    https://juejin.cn/post/7149775557398364167

  • 相关阅读:
    图像轮廓检测
    Dao、Dto、Entity和pojo、mapper概念及关系
    YUV采样格式
    【测开方法论】追踪溯源
    Reading Notes For Introduction To Linux
    2.1 初探大数据
    2023最新版JavaSE教程——第3天:流程控制语句之顺序语句与分支语句
    跨平台代码编写规范——参考《Loup&卡普》的文档
    P2895 [USACO08FEB]Meteor Shower S——bfs
    Unity与IOS⭐Xcode打包,上架TestFlight的完整教程
  • 原文地址:https://blog.csdn.net/u011418943/article/details/128150904