一看就懂的OpenGL ES教程——缓冲对象优化程序(一)

·  阅读 1601
一看就懂的OpenGL ES教程——缓冲对象优化程序(一)

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

通过阅读本文,你将获得以下收获:
1.Opengl常见的缓冲对象
2.如何使用Vertex Buffer Objects(VBO)优化程序

上篇回顾

上一篇博文一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(五) 已经完成了绘制渐变色三角形的任务,这条路虽然漫长(5篇博文)但也算是一马平川,浩浩荡荡。经过前面7篇博文的洗礼,相信你一定已经可以真正理解如何使用OpenGL es绘制一个渐变色的三角形了吧,那么恭喜你,此时你已经翻过了入门的第一座大山。

本文就不继续翻山越岭了,而是讲一些优化性的内容,当然虽然不是什么大山,但也不代表内容就简单。

image.png

OpenGL对象

首先要谈的一个东西是OpenGL对象,这是一个怎样的对象呢? 官网# OpenGL Object 给出了定义:

An OpenGL Object is an OpenGL construct that contains some state. When they are bound to the context, the state that they contain is mapped into the context's state. Thus, changes to context state will be stored in this object, and functions that act on this context state will use the state stored in the object.

OpenGL is defined as a "state machine". The various API calls change the OpenGL state, query some part of that state, or cause OpenGL to use its current state to render something.

Objects are always containers for state. Each particular kind of object is defined by the particular state that it contains. An OpenGL object is a way to encapsulate a particular group of state and change all of it in one function call.

提取重点: 之前在一看就懂的OpenGL ES教程——再谈OpenGL工作机制我们讲过OpenGL是一个状态机,在这里OpenGL对象就其为状态机的一个具体表现。它是一个包含各种状态的结构体,它会与OpenGL的上下文(一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)已有提及过)进行绑定,并将自身的状态和上下文状态进行映射,即上下文状态一有改动它的对应状态就无条件跟着改动

怎么理解这句话呢,别急,后面等我献上代码之后,你们一定能理解~

OpenGL对象包含2类,一种是常规对象,一种是容器对象

常规对象有:

容器对象有:

对于目前的我们来说也不需要这么多“对象”吧。

d39195ed273b9167d6044d9a0ed750f7.jpeg

下面挑几个常见的缓冲对象来讲讲。

常见缓存对象

今天要聊的一种对象,叫做缓冲对象。缓冲对于大家应该都很熟悉了吧,我们做软件开发的经常说在内存汇总开辟一段缓冲区,那么这段缓冲区一般就用来暂时存储一些数据,方便以后可以快速获取。再通俗来讲,打个比方,比如我们要去用水冲洗楼梯,没有缓冲的时候,我们就要一次次跑到家里面舀一盆水再跑到楼梯去冲,但是有了大的木桶装水,我们只要装满木桶的水,再把大木桶带到楼梯去,再从大木桶中舀水去冲洗楼梯,那会省很多成本,那么大木桶就是缓冲

那么在OpenGL的世界中,缓冲又是以怎么的形态存在呢?缓冲在哪里?缓冲是为了解决什么问题?

一看就懂的OpenGL ES教程——再谈OpenGL工作机制一文中就说过,OpenGL图形渲染管线的执行是在gpu中的,而我们调用指令和发送数据是在cpu中的,所以这里必然需要在cpu和gpu中传输数据

首先要说的就是cpu和gpu之间的传输成本问题,虽然现在的cpu和gpu一般都是集成主板上,在宏观上它们之间的物理距离对于电子来说传输时间几乎可以忽略不计,但是当有非常大量的数据需要非常频繁地在cpu和gpu之间穿梭的时候,缓冲的出现就势在必行。

常大量的数据需要非常频繁地在cpu和gpu之间穿梭的场景,大家最熟悉的莫过于视频的渲染了。

具体例子,比如在绘制三角形的系列中,我们每次都是通过glVertexAttribPointer方法将顶点数据从cpu传输给gpu的,看起来9个浮点数也没什么,但是如果我们需要绘制一段视频,一帧需要渲染假如1万个三角形,帧率为30,那么即一秒中要传输90万个浮点数,那就有点刺激了。

b83b71a14cd3b00ccb7b92ead9f852bd.jpeg

对于高级程序员来说,看到这种情况都是条件反射地思考如何减少传输成本,那么第一个能想到的就是缓冲了。 所以缓冲的基本目标就是尽量减少cpu和gpu之间的传输成本,那么一般就是在gpu中开辟一段缓冲区去缓冲需要的数据

一句话,就回答了上面在哪里解决什么问题的问题。

24ea28a2e565c4ed50853f0e57794884.jpeg

在OpenGL的世界中,有几种常见缓冲也是起着类似的作用,它们分别是VBOVertex Buffer Objects),VAOVertex Array ObjectsEBO(Element Buffer Object)以及FBOFramebuffer Objects)等等。

由于FBO对于现在来说超纲,所以留在后面讲完纹理再讲,今天主要讲VBO,VAO,EBO

缓冲对象又各自缓冲不同的东西,上面已经有列举常见的缓冲种类了,有一类专门缓冲数据,比如VBO专门缓冲顶点数据EBO专门缓冲顶点索引数据,它们属于- Buffer Objects。而有一类专门缓冲状态,比如VAO,它就是上面说的Vertex Array Objects这种。那么接下来,就让我们来细细品味这三者的详情和用法。

Buffer Objects

先来看看Buffer Objects。

官网Buffer Object 中的定义是:

Buffer Objects are OpenGL Objects that store an array of unformatted memory allocated by the OpenGL context (AKA the GPU). These can be used to store vertex datapixel data retrieved from images or the framebuffer, and a variety of other things.

最关键的就是它是在gpu中开辟一段数组空间来存储数据

这玩意儿玩起来有固定的套路:

graph TD
创建缓冲区 --> 绑定缓冲区到上下文 --> 将顶点数据存入缓冲区中--> 将指定如何解析顶点属性数组--> 绘制 --> 解绑缓冲区--> 删除缓冲区

反映到代码里就是:

graph TD
glGenBuffers --> glBindBuffer --> glBufferData--> glVertexAttribPointer--> glDrawArrays --> 解绑的glBindBuffer--> glDeleteBuffers

关于代码,主要关注3点:
1.创建的时候通过指定glGenBuffers的type参数即可确定具体的Buffer Objects种类,常见的有以下种类:

GL_ARRAY_BUFFERGL_ELEMENT_ARRAY_BUFFERGL_SHADER_STORAGE_BUFFERGL_PIXEL_PACK_BUFFER等,其中GL_ARRAY_BUFFER对应的就是VBOGL_ELEMENT_ARRAY_BUFFER对应的就是EBO

2.通过glBindBuffer绑定当前缓冲对象到上下文。这里涉及到的就是OpenGL状态机的概念,最重要的就是要知道,也是上面已经提及过的,一旦一个缓冲对象和上下文都绑定了,之后所有的操作都是针对该缓冲对象直到它被解绑

3.通过glBufferData将数据存储在缓冲区中,关于这个函数,还是需要啰嗦几句讲讲。

函数声明为(官网):

void glBufferData(GLenum target, GLsizeiptr size
, const void * data, GLenum usage);
复制代码

target表示对应的具体的Buffer Objects种类,即上面列举的GL_ARRAY_BUFFER和GL_ELEMENT_ARRAY_BUFFER这些。

size表示传入的数据长度。

data就是具体的数据的指针。

usage指定数据的访问模式,模式可选值有GL_STREAM_DRAWGL_STREAM_READGL_STREAM_COPYGL_STATIC_DRAWGL_STATIC_READGL_STATIC_COPYGL_DYNAMIC_DRAWGL_DYNAMIC_READ, or GL_DYNAMIC_COPY

我们最常用的就是指定修改频率模式,即告诉OpenGL我们对数据的修改频率,目的是为了让OpenGL决定该如何存储这些数据,因为不同修改频率的数据会被存储在gpu的不同区域以尽量获取最优的性能

访问频率模式有以下几个:

  • STREAM:数据几乎每次被访问都会被修改。

  • STATIC:数据只被修改一次。

  • DYNAMIC:数据会被修改多次。

52cac471e20453d76ea471a5dfc406ca.jpeg

VBO

像上面的例子,一帧需要渲染假如1万个三角形,帧率为30,那么即一秒中要传输90万个浮点数。那么,按照上面缓冲对象的解释,是不是可以将顶点数据缓存在gpu中呢?假如渲染的图形都是不动的话(实际上即使需要变动,顶点数据也可以是固定的),那么其实只要传输一次顶点数据给gpu即可,即第一帧传递顶点数据即可,假如绘制每帧1万个三角形的话,那么只需要在第一帧传递3万个顶点坐标即可

那么VBO,即Vertex Buffer Objects,顾名思义,就是为了解决这种场景而诞生的。

VBO的使用代码:

  
    static float triangleVerWithColor[] = {
        0.0f, 0.8f, 0.0f,//顶点
        -0.8f, -0.8f, 0.0f,//顶点
        0.8f, -0.8f, 0.0f,//顶点
    };

    //定义vbo的id数组,因为可能需要创建多个vbo
    unsigned int VBOs[1];
    //创建vbo,将创建好的vbo的id存放在VBOs数组中
    glGenBuffers(1, VBOs);
    //此时上下文绑定VBOs[0]对应的vbo缓冲
    glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
    //将顶点数据存入vbo的缓冲区中
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVerWithColor), triangleVerWithColor, GL_STATIC_DRAW);
    //指定如何解析顶点属性数组,注意这里最后一个参数传的不是原数组地址,而是数据再vbo缓冲区中的相对地址
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0);
    //打开着色器中layout为0的输入变量
    glEnableVertexAttribArray(0);
  
    //清屏
    glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    //绘制三角形
    glDrawArrays(GL_TRIANGLES, 0, 3);
    //窗口显示,交换双缓冲区
    eglSwapBuffers(display, winSurface);
    //解绑VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    //删除VBO,即清空缓冲区
    glDeleteBuffers(1, VBOs);
    //释放着色器程序对象
    glDeleteProgram(program);

复制代码

很多人一开始看到类似glBindXX的代码会懵逼,这个方面主要要理解OpenGL状态机的概念,详细的已经在(一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)阐述过,总的来说,当调用了glBindXX指令之后,OpenGL状态机就进入了某种状态中,直到调用了对应的解绑方法。在这中间的所有调用都是针对这个状态的(貌似已经说了很多遍了。。)。

那么在VBO的使用中,可以理解为执行了 glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
之后,后面的glBufferData和glVertexAttribPointer等包括绘制都是基于VBO操作,所以注意到glVertexAttribPointer方法最后一个参数传的是(void*)0,而不是之前的数组。

这里可以这么理解:

之前是
cpu告诉gpu说:“哥们,物理内存中地址为xx的xx数组你拿去用”

现在是
cpu先把数组数据传送到gpu中的vbo缓冲区了,然后调用glVertexAttribPointer方法的时候对gpu说:“哥们,你要怎么怎么使用你里面的vbo中xx地址的数据……”

顶点着色器:

        #version 300 es
        layout (location = 0) 
        //输入的顶点坐标,会在程序指定将数据输入到该字段如果传入的向量是不够4维的,自动将前三个分量设置为0.0,最后一个分量设置为1.0
        in vec4 aPosition;

        void main() {
            //直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
            gl_Position = aPosition;
          
        };
复制代码

片段着色器:

        #version 300 es
        precision mediump float;
      
        out vec4 FragColor;

        void main() {
           //gl_FragColor是OpenGL内置的
            FragColor = vec4(1.0,0.0,0.0,1.0);
        };
复制代码

那么如果顶点属性不止一个属性呢?比如还是增加一个颜色属性。

代码只要微调下即可:

顶点属性数组改为:

static float triangleVerWithColor[] = {
        0.0f, 0.8f, 0.0f,//顶点
        1.0, 0.0, 0.0,//颜色
        -0.8f, -0.8f, 0.0f,//顶点
        0.0, 1.0, 0.0,//颜色
        0.8f, -0.8f, 0.0f,//顶点
        0.0, 0.0, 1.0,//颜色
};
复制代码

解析数据逻辑增加2行解析第二个属性:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0);
//关键点就是最后参数传地址偏移量
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, (void*)(3*4));

glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
复制代码

这里要注意的就是解析第二个属性的时候,要传第二个属性在数组中的地址对应的指针,其实也是其在VBO中的相对地址,因为第二个属性是从第4个元素开始的,即便偏移了3个浮点数元素,所以地址指针是(void*)(3*4)

顶点着色器:

        #version 300 es
        layout (location = 0) 
        //输入的顶点坐标,会在程序指定将数据输入到该字段如果传入的向量是不够4维的,自动将前三个分量设置为0.0,最后一个分量设置为1.0
        in vec4 aPosition;

        //输入的顶点的颜色//如果传入的向量是不够4维的,自动将前三个分量设置为0.0,最后一个分量设置为1.0
        layout (location = 1) 
       in vec4 aColor;
        //输出的颜色
       out vec4 vTextColor;

        void main() {
            //直接把传入的坐标值作为传入渲染管线。gl_Position是OpenGL内置的
            gl_Position = aPosition;
            vTextColor = aColor;
        };
复制代码

片段着色器:

        #version 300 es
        precision
        mediump float;
        //输入的颜色
        in vec4 vTextColor;
        out vec4 FragColor;

        void main() {
           //gl_FragColor是OpenGL内置的
            FragColor = vTextColor;
        };
复制代码

在平时的使用中,其实一般只需要配置一次VBO,后面可以调用很多次绘制VBO里的顶点数据,那么可以将配置和绘制逻辑拆开来

配置代码:

    unsigned int VBOs[1];

    glGenBuffers(1, VBOs);

    glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
    glBufferData(GL_ARRAY_BUFFER, sizeof(triangleVerWithColor), triangleVerWithColor, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 24, (void*)0);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 24, (void*)(3*4));

    glEnableVertexAttribArray(0);
    glEnableVertexAttribArray(1);
    //数据传输完就解绑
    glBindBuffer(GL_ARRAY_BUFFER, 0);
复制代码

绘制代码:

//记得绘制前绑定VBO
glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]);
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

glDrawArrays(GL_TRIANGLES, 0, 3);
//窗口显示,交换双缓冲区
eglSwapBuffers(display, winSurface);
//绘制完成解绑VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);
复制代码

想象在播放视频,后面的绘制代码可是一秒要绘制几十次的,这样就可以看出VBO是如何优化性能的,因为只需要配置一次数据,即cpu传输一次数据到gpu即可,以后每次绘制都是直接取gpu中缓存的顶点数据即可

运行一看,又是熟悉的老朋友了,只是这次绘制是从gpu中的缓冲区中取出的顶点数据

image.png

总结

本文主要介绍了OpenGL对象以及其中的缓冲对象,重点介绍了其中的VBO的作用和使用方法。介于篇幅有限,剩下的缓冲对象内容就放在下一篇继续介绍,感谢大家的支持和关注。

a20241bc1722029d6929b0dd299e1731.jpeg

代码地址

opengl-es-study-demo (不断更新中)

参考

OpenGL Object
Buffer Object
应该怎么理解 OpenGL 的 VAO 与 VBO
熟悉 OpenGL VAO、VBO、FBO、PBO 等对象,看这一篇就够了

原创不易,如果觉得本文对自己有帮助,别忘了随手点赞和关注,这也是我创作的最大动力~

系列文章目录

体系化学习系列博文,请看音视频系统学习的浪漫马车之总目录

实践项目: 介绍一个自己刚出炉的安卓音视频播放录制开源项目

C/C++基础与进阶之路

音视频理论基础系列专栏

音视频开发实战系列专栏

轻松入门OpenGL系列
一看就懂的OpenGL ES教程——图形渲染管线的那些事
一看就懂的OpenGL ES教程——再谈OpenGL工作机制
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(一)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(二)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(三)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(四)
一看就懂的OpenGL ES教程——这或许是你遇过最难画的三角形(五)
一看就懂的OpenGL ES教程——缓冲对象优化程序(一)
一看就懂的OpenGL ES教程——缓冲对象优化程序(二)
一看就懂的OpenGL ES教程——临摹画手的浪漫之纹理映射(理论篇)

收藏成功!
已添加到「」, 点击更改