翻译文html
原文标题:Android Lesson Seven: An Introduction to Vertex Buffer Objects (VBOs) 原文连接:www.learnopengles.com/android-les…java
在这节课中,咱们将介绍如何定义和如何去使用 顶点缓冲对象(VBOs)。下面是咱们要讲到的几点: 1.怎样用顶点缓冲对象定义和渲染 2.单个缓冲区、全部数据打包进去、多个缓冲区之间的区别 3.问题和陷阱咱们如何取处理它们 |
![]() |
到目前为止,咱们全部的课程都是将对象数据存储在客户端内存中,只有在渲染时将其传输到GPU中。没有大量数据传输时,这很好,但随着咱们的场景愈来愈复杂,有更多的物体和三角形,这会给GPU和内存增长额外的成本。android
咱们能作些什么呢?咱们可使用顶点缓冲对象,而不是每帧从客户端内存传输顶点信息,信息将被传输一次,而后渲染器将从该图形存储器缓存中获得数据。git
请阅读OpenGL Android课程一:入门介绍如何从客户端的内存上传顶点数据。了解OpenGL ES如何与顶点数组一块儿工做对于理解本课相当重要。github
一但了解了如何使用客户端内存进行渲染,切换到使用VBOs实际上并不太难。其主要的不一样在于添加了一个上传数据到图形内存的额外步骤,以及渲染时添加了绑定这个缓冲区的额外调用。算法
本节课将使用四种不一样的模式:编程
不管咱们是否使用顶点缓冲对象,咱们都须要先将咱们的数据存储在客户端本地缓冲区。会想到第一课中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参数说明:
使用打包缓冲区是很是类似的,替换了每一个位置、法线等的缓冲区,如今一个缓冲区将包含全部这些数据。不一样点看下面:
使用单缓冲区
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个字节)开始。
若是你在本地堆上分配大量内存把并将其释放,您早晚会遇到心爱的OutOfMemoryError
,背后有几个缘由:
allocateDirect()
将会莫名其妙失败,尽管彷佛有足够的内存可用。有时它有助于进行较小的分配,释放它,而后再次尝试更大的分配。如何能避免这些问题?除了但愿Google在将来的版本中改进Dalvik的行为以外,并很少。或者经过本地代码进行分配或预先分配一大块内存来自行管理堆,并根据此分离缓冲区。
注意:这些信息最初写于2012年初,如今Android使用了一个名为ART的不一样运行时,它可能在相同程度上不会遇到这些问题。
如今咱们已经回顾了使用客户端缓冲区,让咱们继续讨论顶点缓冲区对象!首先,咱们须要回顾几个很是重要的问题:
这彷佛是一个明显的观点,可是它仅仅提醒你必须等到onSurfaceCreated()
执行,而且你必须注意OpenGL ES调用是在GL线程上完成的。 看这个文档:iOS OpenGL ES编程指南,它多是为iOS写的,可是OpenGL ES在Android的行为和这相同。
当你使用顶点缓冲对象时,须要注意传递的数据。不当的值将会致使OpenGL ES系统库或图形驱动库本地崩溃。在个人Nexus S上,一些游戏彻底卡在个人手机上或致使手机重启,由于图形驱动由于他们的指令崩溃。并不是全部的崩溃都会锁定您的设备,但至少您不会看到“此应用已中止工做”的对话框。您的活动将在没有警告的状况下从新启动,您将得到惟一的信息多是日志中的本地调试跟踪。
要上传数据到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的解释:
咱们对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中,这些东西由着色器处理,所以不须要使用glEnable
或glDisable
。您什么时候使用顶点缓冲区?何时从客户端内存传输数据更好?使用顶点缓冲区对象有哪些缺点?您将如何改进异步加载代码?
能够在Github下载本课程源代码:下载项目
本课的编译版本也能够再Android市场下:google play 下载apk
为了方便你们下载,“我”也编译了个apk,:github download