OpenGL Android课程七:介绍Vertex Buffer Objects(顶点缓冲区对象,简称:VBOs)

翻译文html

原文标题:Android Lesson Seven: An Introduction to Vertex Buffer Objects (VBOs) 原文连接:www.learnopengles.com/android-les…java


介绍Vertex Buffer Objects(顶点缓冲区对象,简称:VBOs)

在这节课中,咱们将介绍如何定义和如何去使用
顶点缓冲对象(VBOs)。下面是咱们要讲到的几点:

1.怎样用顶点缓冲对象定义和渲染
2.单个缓冲区、全部数据打包进去、多个缓冲区之间的区别
3.问题和陷阱咱们如何取处理它们
screenshot

什么是顶点缓冲区对象?为何使用它们?

到目前为止,咱们全部的课程都是将对象数据存储在客户端内存中,只有在渲染时将其传输到GPU中。没有大量数据传输时,这很好,但随着咱们的场景愈来愈复杂,有更多的物体和三角形,这会给GPU和内存增长额外的成本。android

咱们能作些什么呢?咱们可使用顶点缓冲对象,而不是每帧从客户端内存传输顶点信息,信息将被传输一次,而后渲染器将从该图形存储器缓存中获得数据。git

前提条件

请阅读OpenGL Android课程一:入门介绍如何从客户端的内存上传顶点数据。了解OpenGL ES如何与顶点数组一块儿工做对于理解本课相当重要。github

更详细的了解客户端缓冲区

一但了解了如何使用客户端内存进行渲染,切换到使用VBOs实际上并不太难。其主要的不一样在于添加了一个上传数据到图形内存的额外步骤,以及渲染时添加了绑定这个缓冲区的额外调用。算法

本节课将使用四种不一样的模式:编程

  1. 客户端,单独的缓冲区
  2. 客户端,打包的缓冲
  3. 顶点缓冲对象,单独的缓冲区
  4. 顶点缓冲对象,打包的缓冲

不管咱们是否使用顶点缓冲对象,咱们都须要先将咱们的数据存储在客户端本地缓冲区。会想到第一课中OpenGL ES 是一个本地系统库,而java是运行在Android上的一个虚拟机中。如何去桥接这个距离?咱们须要使用一组特殊的缓冲区类来在本地堆上分配内存,并使使其供OpenGL访问:数组

// Java 数组
float[] cubePositions;
...
// 浮点缓冲区
final FloatBuffer cubePositionsBuffer;
...
// 在本地堆上直接分配一块内存
// 字节大小为cubePositions的长度乘以每一个浮点数的字节大小
// 每一个float的字节大小为4,由于float是32位或4字节
cubePositionsBuffer = ByteBuffer.allocateDirect(cubePositions.length * BYTES_PRE_FLOAT)
// 浮点会以大端(big-endian)或小段(little-endian)的顺序排列
// 我想让其同本地平台相同的排列
.order(ByteOrder.nativeOrder())
// 在这个字节缓冲区上给咱们一个浮点视角
.asFloatBuffer();
复制代码

将Java堆上数据转换到本地堆上,就是两方法调用的事情:缓存

// 将java堆上的数据拷贝到本地堆
cubePositionsBuffer.put(cubePositions)

// 重置这个缓冲区开始的缓冲位置
.position(0);
复制代码

缓冲位置的目的是什么?一般,Java没有为咱们提供一种在内存中使用指针,任意指定位置的方法。然而,设置缓冲区的位置在功能上等同于更改指向内存块指针的值。经过改变指针的位置,咱们能够将缓冲区中任意的内存位置传递给OpenGL调用。当咱们使用打包的缓冲做业时,这将派上用场。app

一但数据存放到本地堆上,咱们就不须要长时间持有float[]数组了,咱们可让垃圾回收器清理它。

使用客户端缓冲区进行渲染很是简单,咱们仅须要启动对应属性的顶点素组,并将指针传递给咱们的数据:

// 传入位置信息
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttriPointer(mPositionHandle, POSITION_DATA_SIZE,
    GLES20.GL_FLOAT, false, 0, mCubePositions)
复制代码

glVertexAttriPointer参数说明:

  • mPositionHandle: 咱们着色器程序的位置属性索引
  • POSITION_DATA_SIZE: 定义这个属性须要多少个float元素
  • GL_FLOAT: 每一个元素的类型
  • false: 定点数据因该标准化吗?因为咱们使用的是浮点数据,所以不适用。
  • 0: 跨度,设置0,觉得着应安顺序读取。第一课中设置为7,表示每次读取跨度7个位置
  • mCubePositions: 指向缓冲区的的指针,包含全部位置数据

使用打包缓冲区

使用打包缓冲区是很是类似的,替换了每一个位置、法线等的缓冲区,如今一个缓冲区将包含全部这些数据。不一样点看下面:

使用单缓冲区

positions = X,Y,Z,X,Y,Z,X,Y,Z,...
colors = R,G,B,A,R,G,B,A,...
textureCoordinates = S,T,S,T,S,T...
复制代码

使用打包缓冲区

buffer = X,Y,Z,R,G,B,A,S,T...
复制代码

使用打包缓冲区的好处是它将会使GPU更高效的渲染,由于渲染三角形所需的全部信息都位于内存同一块地方。缺点是,若是咱们使用动态数据,更新可能会更困难,更慢。

当咱们使用打包缓冲区时,咱们须要如下几种方式更改渲染调用。首先,咱们须要告诉OpenGL跨度(stride) ,定义一个顶点的字节数。

final int stride = (POSITION_DATA_SIZE + NORMAL_DATA_SIZE + TEXTURE_COORDINATE_DATA_SIZE)
    * BYTES_PER_FLOAT;

// 传入位置信息
mCubeBuffer.position(0);
GLES20.glEnableVertexAttribArray(mPositionHandle);
GLES20.glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE,
    GLES20.GL_FLOAT, false, stride, mCubeBuffer);

// 传入法线信息
mCubeBuffer.position(POSITION_DATA_SIZE);
GLES20.glEnableVertexAttribArray(mNormalHandle);
GLES20.glVertexAttribPointer(mNormalHandle, NORMAL_DATA_SIZE,
    GLES20.GL_FLOAT, false, stride, mCubeBuffer);
...
复制代码

这个跨度告诉OpenGL ES下一个顶点的一样的属性要再跨多远才能找到。例如:若是元素0是第一个顶点的开始位置,而且这里每一个顶点有8个元素,而后这个跨度将是8个元素,也就是32个字节。下一个顶点的位置将找到第8个元素,下下个顶点的位置将找到第16个元素,以此类推。

请记住,传递给glVertexAttriPointer的跨度单位是字节,而不是元素,所以请记住进行该转换。

注意,当咱们从指定位置切换到指定法线时,咱们要更改缓冲区的其实位置。这是咱们以前提到的指针算法,这是咱们在使用OpengGL ES时用Java作的方式。咱们仍然使用同一个缓冲区mCubeBuffer,可是咱们告诉OpenGL从位置数据后的第一个元素开始读取法线信息。咱们也告诉OpenGL下一个法线要跨越8个元素(也能够说是32个字节)开始。

Dalvik和本地堆上的内存

若是你在本地堆上分配大量内存把并将其释放,您早晚会遇到心爱的OutOfMemoryError ,背后有几个缘由:

  1. 您可能认为经过让引用超出范围而自动释放了内存,可是本地内存彷佛须要一些额外的GC周期才能彻底清理,若是没有足够可用的内存而且还没有释放本地内存,Dalvik将抛出异常。
  2. 本地堆可能会碎片化,调用allocateDirect()将会莫名其妙失败,尽管彷佛有足够的内存可用。有时它有助于进行较小的分配,释放它,而后再次尝试更大的分配。

如何能避免这些问题?除了但愿Google在将来的版本中改进Dalvik的行为以外,并很少。或者经过本地代码进行分配或预先分配一大块内存来自行管理堆,并根据此分离缓冲区。

注意:这些信息最初写于2012年初,如今Android使用了一个名为ART的不一样运行时,它可能在相同程度上不会遇到这些问题。

移动到顶点缓冲区对象

如今咱们已经回顾了使用客户端缓冲区,让咱们继续讨论顶点缓冲区对象!首先,咱们须要回顾几个很是重要的问题:

1. 缓冲区必须建立在一个有效的OpenGL上下文中

这彷佛是一个明显的观点,可是它仅仅提醒你必须等到onSurfaceCreated()执行,而且你必须注意OpenGL ES调用是在GL线程上完成的。 看这个文档:iOS OpenGL ES编程指南,它多是为iOS写的,可是OpenGL ES在Android的行为和这相同。

2. 顶点缓冲区对象使用不当会致使图形驱动程序崩溃

当你使用顶点缓冲对象时,须要注意传递的数据。不当的值将会致使OpenGL ES系统库或图形驱动库本地崩溃。在个人Nexus S上,一些游戏彻底卡在个人手机上或致使手机重启,由于图形驱动由于他们的指令崩溃。并不是全部的崩溃都会锁定您的设备,但至少您不会看到“此应用已中止工做”的对话框。您的活动将在没有警告的状况下从新启动,您将得到惟一的信息多是日志中的本地调试跟踪。

上传顶点数据到GPU

要上传数据到GPU,咱们须要像之前同样建立客户端缓冲区的相同步骤:

...
cubePositionsBuffer = ByteBuffer.allocateDirect(cubePositions.length * BYTES_PER_FLOAT)
.order(ByteOrder.nativeOrder()).asFloatBuffer();
cubePositionsBuffer.put(cubePositions).position(0);
...
复制代码

一旦咱们有了客户端缓冲区,咱们就能够建立一个顶点缓冲区对象,并使用一下指令将数据从客户端内存上传到GPU:

// 首先,咱们要尽量的申请更多的缓冲区
// 这将为咱们提供这些缓冲区的handle
final int buffers[] = new int[3];
GLES20.glGenBuffers(3, buffers, 0);

// 绑定这个缓冲区,未来的指令将单独影响此缓冲区
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);

// 客户端内存中的数据转移到缓冲区
// 咱们能在这次调动后释放客户端内存
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, cubePositionsBuffer.capacity() * BYTES_PER_FLOAT,
    cubePositionsBuffer, GLES20.GL_STATIC_DRAW);

// 重要提醒:完成缓冲后,从缓冲区取消绑定
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
复制代码

一旦数据上传到了OpenGL ES,咱们就能够释放这个客户端内存,由于咱们不须要再继续保留它。这是glBufferData的解释:

  • GL_ARRAY_BUFFER: 这个缓冲区包含顶点数据数组
  • cubePositionsBuffer.capacity() * BYTES_PER_FLOAT: 这个缓冲区因该包含的字节数
  • cubePositionsBuffer: 将要拷贝到这个顶点缓冲区对象的源
  • GL_STATIC_DRAW: 这个缓冲区不会动态更新

咱们对glVertexAttribPointer的调用看起来有点儿不一样,由于最后一个参数如今是偏移量而不是指向客户端内存的指针:

// 传入位置信息
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mCubePositionsBufferIdx);
GLES20.glEnableVertexAttribArray(mPositionHandle);
mGlEs20.glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE, GLES20.GL_FLOAT, false, 0, 0);
...
复制代码

像之前同样,咱们绑定到缓冲区,而后启用顶点数组。因为缓冲区早已绑定,当从缓冲区读取数据时,咱们仅须要告诉OpenGL开始的偏移。由于咱们使用的特定的缓冲区,咱们传入偏移量0。另请注意,咱们使用自定义绑定来调用glVertexAttribPointer,由于官方SKD缺乏此特定函数调用。

一旦咱们用缓冲区绘制完成,咱们应该解除它:

GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
复制代码

当咱们不想在保留缓冲区时,咱们能够释放内存:

final int[] buffersToDelete = new int[] { mCubePositionsBufferIdx, mCubeNormalsBufferIdx,
    mCubeTexCoordsBufferIdx };
GLES20.glDeleteBuffers(buffersToDelete.length, buffersToDelete, 0);
复制代码

打包顶点缓冲区对象

咱们还可使用单个缓冲区打包顶点缓冲区对象的全部顶点数据。打包顶点缓冲区的建立和上面相同,惟一的区别是咱们从打包客户端缓冲区开始。打包缓冲区渲染也是同样的,除了咱们须要传偏移量,就像在客户端内存中使用打包缓冲区同样:

final int stride = (POSITION_DATA_SIZE + NORMAL_DATA_SIZE + TEXTURE_COORDINATE_DATA_SIZE)
    * BYTES_PER_FLOAT;

// 传入位置信息
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mCubeBufferIdx);
GLES20.glEnableVertexAttribArray(mPositionHandle);
mGlEs20.glVertexAttribPointer(mPositionHandle, POSITION_DATA_SIZE,
    GLES20.GL_FLOAT, false, stride, 0);

// 传入法线信息
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mCubeBufferIdx);
GLES20.glEnableVertexAttribArray(mNormalHandle);
mGlEs20.glVertexAttribPointer(mNormalHandle, NORMAL_DATA_SIZE,
    GLES20.GL_FLOAT, false, stride, POSITION_DATA_SIZE * BYTES_PER_FLOAT);
...
复制代码

注意:偏移量须要以字节为单位指定。与以前同样解除绑定和删除缓冲区的相同注意事项也适用。

将顶点数据放到一块儿

这节课已构建了多立方体组成的立方体,每一个面的立方体数量体相同。它将在1x1x1立方体和16x16x16立方体之间构创建方体。因为每一个立方体共享相同的法线和纹理数据,所以在初始化客户端缓冲区时将重复复制此数据。全部立方体都将在同一个缓冲区对象中结束。

您能够查看课程中的代码并查看使用和不使用VBO,以及使用和不使用打包缓冲区进行渲染的示例。检查代码以查看如何处理一下某些操做:

  • 经过runOnUiThread()将事件从OpenGL线程发布回UI主线程
  • 异步生成顶点数据
  • 处理内存溢出异常
  • 咱们移除了glEnable(GL_TEXTURE_2D)的调用,由于它实际在OpenGL ES 2是一个无效枚举。这是之前的固定写法延续下来的,在OpenGLES2中,这些东西由着色器处理,所以不须要使用glEnableglDisable
  • 怎样使用不一样的方式进行渲染,而不添加太多的if语句和条件。

进一步练习

您什么时候使用顶点缓冲区?何时从客户端内存传输数据更好?使用顶点缓冲区对象有哪些缺点?您将如何改进异步加载代码?

教程目录

打包教材

能够在Github下载本课程源代码:下载项目
本课的编译版本也能够再Android市场下:google play 下载apk
为了方便你们下载,“我”也编译了个apk,:github download

相关文章
相关标签/搜索