学习OpenGL ES之VBO&VAO

作者: handyTOOL | 来源:发表于2017-05-18 13:50 被阅读371次

    本系列所有文章目录

    获取示例代码


    本文将要介绍OpenGL ES的一个优化技巧,使用VBO和VAO减少CPU和GPU之间的数据传递,提高绘制速度。我们先来回顾一下之前绘制图形用到的代码。

    // 启用Shader中的两个属性
    // attribute vec4 position;
    // attribute vec4 color;
    GLuint positionAttribLocation = glGetAttribLocation(program, "position");
    glEnableVertexAttribArray(positionAttribLocation);
    GLuint colorAttribLocation = glGetAttribLocation(program, "normal");
    glEnableVertexAttribArray(colorAttribLocation);
    GLuint uvAttribLocation = glGetAttribLocation(program, "uv");
    glEnableVertexAttribArray(uvAttribLocation);
    
    // 为shader中的position和color赋值
    // glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
    // indx: 上面Get到的Location
    // size: 有几个类型为type的数据,比如位置有x,y,z三个GLfloat元素,值就为3
    // type: 一般就是数组里元素数据的类型
    // normalized: 暂时用不上
    // stride: 每一个点包含几个byte,本例中就是6个GLfloat,x,y,z,r,g,b
    // ptr: 数据开始的指针,位置就是从头开始,颜色则跳过3个GLFloat的大小
    glVertexAttribPointer(positionAttribLocation, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)triangleData);
    glVertexAttribPointer(colorAttribLocation, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)triangleData + 3 * sizeof(GLfloat));
    glVertexAttribPointer(uvAttribLocation, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)triangleData + 6 * sizeof(GLfloat));
    
    glDrawArrays(GL_TRIANGLES, 0, vertexCount);
    

    首先激活Vertex Shader中的属性,然后绑定数据到属性上,最后使用glDrawArrays�绘制图形。在这个过程中,triangleData会从系统内存传递到GPU的内存,也就是说每次渲染都会传递一次,如果数据量过多,可能会导致数据传递时间过长,严重影响帧率。

    帧率是指每秒绘制的次数,每次绘制消耗的时间越多,帧率也就越低。为了让动画看起来很平滑,帧率越高越好。不过因为屏幕的刷新频率一般都是60Hz,为了保持垂直同步,画面不容易撕裂,会限制帧率小于等于屏幕刷新频率。

    VBO

    如果我们可以把数据提前传递到GPU,CPU每次绘制的时候只需要告诉GPU自己引用了显存里面的哪一块数据就可以减少数据的传递了。这就是VBO做的事情,VBO全称Vertex Buffer Object,它就是把系统内存中的顶点数据传递给GPU后返回的引用凭证。创建的代码如下。

    - (GLfloat *)cubeData {
        static GLfloat cubeData[] = {
            // X轴0.5处的平面
            0.5,  -0.5,    0.5f, 1,  0,  0, 0, 0,
            0.5,  -0.5f,  -0.5f, 1,  0,  0, 0, 1,
            0.5,  0.5f,   -0.5f, 1,  0,  0, 1, 1,
            0.5,  0.5,    -0.5f, 1,  0,  0, 1, 1,
            0.5,  0.5f,    0.5f, 1,  0,  0, 1, 0,
            0.5,  -0.5f,   0.5f, 1,  0,  0, 0, 0,
            // X轴-0.5处的平面
            -0.5,  -0.5,    0.5f, -1,  0,  0, 0, 0,
            -0.5,  -0.5f,  -0.5f, -1,  0,  0, 0, 1,
            -0.5,  0.5f,   -0.5f, -1,  0,  0, 1, 1,
            -0.5,  0.5,    -0.5f, -1,  0,  0, 1, 1,
            -0.5,  0.5f,    0.5f, -1,  0,  0, 1, 0,
            -0.5,  -0.5f,   0.5f, -1,  0,  0, 0, 0,
            
            -0.5,  0.5,  0.5f, 0,  1,  0, 0, 0,
            -0.5f, 0.5, -0.5f, 0,  1,  0, 0, 1,
            0.5f, 0.5,  -0.5f, 0,  1,  0, 1, 1,
            0.5,  0.5,  -0.5f, 0,  1,  0, 1, 1,
            0.5f, 0.5,   0.5f, 0,  1,  0, 1, 0,
            -0.5f, 0.5,  0.5f, 0,  1,  0, 0, 0,
            -0.5, -0.5,   0.5f, 0,  -1,  0, 0, 0,
            -0.5f, -0.5, -0.5f, 0,  -1,  0, 0, 1,
            0.5f, -0.5,  -0.5f, 0,  -1,  0, 1, 1,
            0.5,  -0.5,  -0.5f, 0,  -1,  0, 1, 1,
            0.5f, -0.5,   0.5f, 0,  -1,  0, 1, 0,
            -0.5f, -0.5,  0.5f, 0,  -1,  0, 0, 0,
            
            -0.5,   0.5f,  0.5,   0,  0,  1, 0, 0,
            -0.5f,  -0.5f,  0.5,  0,  0,  1, 0, 1,
            0.5f,   -0.5f,  0.5,  0,  0,  1, 1, 1,
            0.5,    -0.5f, 0.5,   0,  0,  1, 1, 1,
            0.5f,  0.5f,  0.5,    0,  0,  1, 1, 0,
            -0.5f,   0.5f,  0.5,  0,  0,  1, 0, 0,
            -0.5,   0.5f,  -0.5,   0,  0,  -1, 0, 0,
            -0.5f,  -0.5f,  -0.5,  0,  0,  -1, 0, 1,
            0.5f,   -0.5f,  -0.5,  0,  0,  -1, 1, 1,
            0.5,    -0.5f, -0.5,   0,  0,  -1, 1, 1,
            0.5f,  0.5f,  -0.5,    0,  0,  -1, 1, 0,
            -0.5f,   0.5f,  -0.5,  0,  0,  -1, 0, 0,
        };
        return cubeData;
    }
    
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, 36 * 8 * sizeof(GLfloat), [self cubeData], GL_STATIC_DRAW);
    

    首先使用glGenBuffers生成Buffer,然后绑定缓存到GL_ARRAY_BUFFER,然后向GL_ARRAY_BUFFER中写数据。这样vbo所对应的GPU显存中就有cubeData的数据了。glBufferData最后一个参数GL_STATIC_DRAW表示这块数据不会改变。如果你想在后面改变vbo对应的数据的话,可以改为GL_DYNAMIC_DRAW或者GL_STREAM_DRAW,前者适合低频的修改,后者适合高频修改。

    只要再次调用glBindBufferglBufferData就可以修改vbo对应的数据了。

    有了VBO之后,绘制代码可以修改成。

    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    
    // 启用Shader中的两个属性
    // attribute vec4 position;
    // attribute vec4 color;
    GLuint positionAttribLocation = glGetAttribLocation(program, "position");
    glEnableVertexAttribArray(positionAttribLocation);
    GLuint colorAttribLocation = glGetAttribLocation(program, "normal");
    glEnableVertexAttribArray(colorAttribLocation);
    GLuint uvAttribLocation = glGetAttribLocation(program, "uv");
    glEnableVertexAttribArray(uvAttribLocation);
    
    // 为shader中的position和color赋值
    // glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
    // indx: 上面Get到的Location
    // size: 有几个类型为type的数据,比如位置有x,y,z三个GLfloat元素,值就为3
    // type: 一般就是数组里元素数据的类型
    // normalized: 暂时用不上
    // stride: 每一个点包含几个byte,本例中就是6个GLfloat,x,y,z,r,g,b
    // ptr: 数据开始的指针,位置就是从头开始,颜色则跳过3个GLFloat的大小
    glVertexAttribPointer(positionAttribLocation, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)NULL);
    glVertexAttribPointer(colorAttribLocation, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)NULL + 3 * sizeof(GLfloat));
    glVertexAttribPointer(uvAttribLocation, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)NULL + 6 * sizeof(GLfloat));
    

    首先要绑定你的VBOglBindBuffer(GL_ARRAY_BUFFER, vbo);,这样在绑定属性数据时就不再需要传递顶点数据triangleData了,直接改为NULL即可。现在数据传递已经被优化了,那么绑定属性数据这个步骤是不是也可以被优化呢?当然可以,这就是VAO做的事情。

    VAO

    VAO全称Vertex Array Object,无格式的顶点数据VBO转化为固定顶点格式的数组VAO,就可以被Vertex Shader直接使用了。创建过程如下。

    glGenVertexArraysOES(1, &vao);
    glBindVertexArrayOES(vao);
    
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    [self.context bindAttribs:NULL];
    
    glBindVertexArrayOES(0);
    

    bindAttribs实现如下。

    - (void)bindAttribs:(GLfloat *)triangleData {
        // 启用Shader中的两个属性
        // attribute vec4 position;
        // attribute vec4 color;
        GLuint positionAttribLocation = glGetAttribLocation(program, "position");
        glEnableVertexAttribArray(positionAttribLocation);
        GLuint colorAttribLocation = glGetAttribLocation(program, "normal");
        glEnableVertexAttribArray(colorAttribLocation);
        GLuint uvAttribLocation = glGetAttribLocation(program, "uv");
        glEnableVertexAttribArray(uvAttribLocation);
        
        // 为shader中的position和color赋值
        // glVertexAttribPointer (GLuint indx, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
        // indx: 上面Get到的Location
        // size: 有几个类型为type的数据,比如位置有x,y,z三个GLfloat元素,值就为3
        // type: 一般就是数组里元素数据的类型
        // normalized: 暂时用不上
        // stride: 每一个点包含几个byte,本例中就是6个GLfloat,x,y,z,r,g,b
        // ptr: 数据开始的指针,位置就是从头开始,颜色则跳过3个GLFloat的大小
        glVertexAttribPointer(positionAttribLocation, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)triangleData);
        glVertexAttribPointer(colorAttribLocation, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)triangleData + 3 * sizeof(GLfloat));
        glVertexAttribPointer(uvAttribLocation, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(GLfloat), (char *)triangleData + 6 * sizeof(GLfloat));
    }
    

    glGenVertexArraysOES等OES结尾的方法都是苹果自己扩展的,在其他平台上也会有VAO相关的方法集合,但名字会有所不同。VAO的创建过程是生成VAO Buffer,绑定VAO Buffer,执行属性绑定的操作,最后解绑VAO Buffer。这样VBO就转化成了VAO。绘制的代码变成如下。

    glBindVertexArrayOES(vao);
    glDrawArrays(GL_TRIANGLES, 0, vertexCount);
    

    这样一来,顶点数据经过VBO和VAO的优化,就不需要每次绘制前进行数据传递和属性绑定了,大大提高了绘制速度。

    重构

    本文的例子做了一些重构,创建了可渲染物体的基类GLObject,方便重用。Cube类继承GLObject,使用VAO绘制带有纹理的正方体。GLContext中增加了使用VBO和VAO绘制三角形的方法。对重构部分有兴趣的可以自行阅读源码。

    相关文章

      网友评论

      • 苹果API搬运工:ViewController.m里面的更新代理,感觉有些方法不需要放在循环里执行,投影矩阵,摄像机矩阵,和光照方向对每个cube都是一样的,只有模型和法线矩阵需要根据每个cube来算:
        - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
        [super glkView:view drawInRect:rect];

        [self.objects enumerateObjectsUsingBlock:^(GLObject *obj, NSUInteger idx, BOOL *stop) {
        [obj.context active];
        [obj.context setUniform1f:@"elapsedTime" value:(GLfloat)self.elapsedTime];
        [obj.context setUniformMatrix4fv:@"projectionMatrix" value:self.projectionMatrix];
        [obj.context setUniformMatrix4fv:@"cameraMatrix" value:self.cameraMatrix];

        [obj.context setUniform3fv:@"lightDirection" value:self.lightDirection];
        [obj draw:obj.context];
        }];

        }

        我觉得可以改成:
        - (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
        [super glkView:view drawInRect:rect];

        [self.glContext active];
        //[self.glContext setUniform1f:@"elapsedTime" value:(GLfloat)self.elapsedTime];
        [self.glContext setUniformMatrix4fv:@"projectionMatrix" value:self.projectionMatrix];
        [self.glContext setUniformMatrix4fv:@"cameraMatrix" value:self.cameraMatrix];

        [self.glContext setUniform3fv:@"lightDirection" value:self.lightDirection];

        for (GLObject *object in self.objects) {
        [object draw:object.context];
        }

        }

        elapsedTime也暂时不需要了,因为它已经更新到self.cameraMatrix里面去了,shader里面也暂时没用了.跑起来测试了一下,这样貌似没有问题
        handyTOOL:@史前图腾 嗯,是的,例子是有很大优化空间的。我在写例子的时候考虑的是尽量减少代码的改变,让读者的关注点在文章介绍的知识点上,所以有很多代码就保持了没有优化的状态。

      本文标题:学习OpenGL ES之VBO&VAO

      本文链接:https://www.haomeiwen.com/subject/eztsxxtx.html