学习OpenGL ES之VBO&VAO

简介: 学习OpenGL ES之VBO&VAO

获取示例代码


本文将要介绍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绘制三角形的方法。对重构部分有兴趣的可以自行阅读源码。

相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
4月前
|
XML 小程序 Java
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
【Android App】三维投影OpenGL ES的讲解及着色器实现(附源码和演示 超详细)
53 0
|
6月前
|
C语言 开发者
[笔记]音视频学习之OpenGL篇《一》创建窗口
[笔记]音视频学习之OpenGL篇《一》创建窗口
|
8月前
|
存储 编解码 编译器
QT+ OpenGL学习
什么是opengl open graphics library 他是一个由Khronos组织制定并且维护的规范 opengl核心是一个c库,同时也支持多种语言的派生
111 0
|
存储 编解码 算法
Opengl ES之LUT滤镜(上)
Opengl ES之连载系列
342 0
|
数据安全/隐私保护 开发者
OpenGL ES 多目标渲染(MRT)
Opengl ES连载系列
221 0
|
数据安全/隐私保护 索引
Opengl ES之纹理数组
Opengl ES连载系列
173 0
|
数据安全/隐私保护
Opengl ES之水印贴图
Opengl ES之连载系列
86 0
|
Java 数据安全/隐私保护 Android开发
Opengl ES之矩阵变换(下)
Opengl ES连载系列
84 0
|
Java API 数据安全/隐私保护
Opengl ES之矩阵变换(上)
Opengl ES连载系列
83 0
|
存储
Opengl ES之踩坑记
Opengl ES之连载系列
91 0