本文将结合OpenGL 理论和Android OpenGL ES API,通过demo向大家阐述如何定义三角形和正方形。
(1)定义一个三角形
OpenGL ES允许你使用三维坐标系空间来绘制图形对象,因此在绘制三角形之前,你必须提前定义你的坐标值,在OpenGL中,典型的方式是为坐标定义一个浮点类型的顶点数组。为了高效,你应把这些坐标都写进一个ByteBuffer,它会被传到OpenGLES图形管线以进行处理。
class Triangle {
private FloatBuffer vertexBuffer;
// 数组中每个顶点的坐标数
static final int COORDS_PER_VERTEX = 3;
static float triangleCoords[] = { // 按逆时针方向顺序:
0.0f, 0.622008459f, 0.0f, // top
-0.5f, -0.311004243f, 0.0f, // bottom left
0.5f, -0.311004243f, 0.0f // bottom right
};
// 设置颜色,分别为red, green, blue 和alpha (opacity)
float color[] = { 0.63671875f, 0.76953125f, 0.22265625f, 1.0f };
public Triangle() {
// 为存放形状的坐标,初始化顶点字节缓冲
ByteBuffer bb = ByteBuffer.allocateDirect(
// (坐标数 * 4)float占四字节
triangleCoords.length * 4);
// 设用设备的本点字节序
bb.order(ByteOrder.nativeOrder());
// 从ByteBuffer创建一个浮点缓冲
vertexBuffer = bb.asFloatBuffer();
// 把坐标们加入FloatBuffer中
vertexBuffer.put(triangleCoords);
// 设置buffer,从第一个坐标开始读
vertexBuffer.position(0);
}
}
1.坐标系
如上图,在OpenGL里,我们要渲染的一切物体都要映射到X轴和Y轴以及Z轴上[-1,1]的范围内,对于Z轴也一样。这个范围内的坐标被称为归一化设备坐标,其独立于屏幕实际尺寸或形状。
2.Dalvik到OpenGL传输数据的方式
当我们在模拟器或者设备上编译和运行Java代码的时候,它并不是直接运行在硬件上的,相反,它运行在一个特殊的环境上,即Dalvik虚拟机。运行在虚拟机上的代码不能直接访问本地环境,除非通过特定的API。
Dalvik虚拟机有自己的自动垃圾回收机制。这意味着,当虚拟机检测到一个变量,对象或者其他内存片段不在被使用时,就会这些内存释放掉以备重用,它也能腾挪内存以提高空间使用效率。但是native环境并不是这样工作的,它不期望内存块会被移来移去或者被自动释放。Android之所以这样设计,是因为开发者在开发程序的时候不必关心特定的CPU或者机器架构,也不必关心底层的内存管理。这通常都能工作得很好,除非要与本地系统交互。
然而OpenGL作为本地系统库直接运行在硬件上,没有虚拟机,也没有垃圾回收或内存压缩,那么应用层代码运行在虚拟机内部,它是如何与OpenGL通信呢?有两种技术,第一种技术是使用Java本地接口JNI,这个技术已经由Android软件开发部提供,当调用android.opengl.GLES20包里方法时,软件开发包实际上就是在后台使用JNI调用本地系统库。
第二种技术就是改变内存分配的方式,Java有一个特殊的类集合,它们可以分配本地内存块,并且把Java数据复制到本地内存。本地内存可以被本地环境存取,而不受垃圾回收器的管理。
3.改变内存分配实现向OpenGL传输数据
首先,我们使用ByteBuffer.allocateDirect()分配了一块native内存,这块内存不会被垃圾回收器管理。这个方法需要知道要分配多少个字节的内存块,因为顶点都存储在一个浮点数组里,并且每个浮点数有4个字节,所以这块内存的大小应该是triangleCoords.length*BYTES_PER_FLOAT。
下一行告诉字节缓冲区按照本地字节序组织它的内容。本地字节序是指,当一个值占用多个字节时,比如32位整数,字节按照从最重要位到最不重要位或者相反顺序排列。可以认为这与从左到右或者从右到左写一个数类似。知道这个排序并不重要,重要的是作为一个平台要使用相同的排序,调用order(ByteOrder.nativeOrder())可以保证这一点。
最后,我们不愿意直接操作单独的字节,而是希望使用浮点数,因此,调用asFloatBuffer()得到一个可以反映底层字节的FloatBuffer类实例。然后就可以调用vertexData.put(rectangle)把数据从Dalvik的内存复制到本地内存了。
至于ByteBuffer的内存释放,ByteBuffer.allocateDirect分配的堆外内存不需要我们手动释放,而且ByteBuffer中也没有提供手动释放的API。也即是说,使用ByteBuffer不用担心堆外内存的释放问题,除非堆内存中的ByteBuffer对象由于错误编码而出现内存泄露。当进程结束时,这块内存会被释放掉,所以,我们一般情况下不用关心它。但是,如果你在编写代码的时候,创建了很多ByteBuffer,或者随着程序运行产生了很多 ByteBuffer,你也许想学习一些碎片化以及内存管理的技术。
(2)定义正方形
在OpenGL里,只能绘制点,直线以及三角形。因此典型的做法是使用两个三角形组合而成,如图:
定义正方形和三角形没有多大差别,主要区别就是为了避免增加两个三角形坐标数组,我们需要增加一个绘制列表来告诉OpenGLES图形管线按什么顺序(如上图)绘制这些顶点。下面就是这个形状的代码:
class Square {
private FloatBuffer vertexBuffer;
private ShortBuffer drawListBuffer;
// 每个顶点的坐标数
static final int COORDS_PER_VERTEX = 3;
static float squareCoords[] = { -0.5f, 0.5f, 0.0f, // top left
-0.5f, -0.5f, 0.0f, // bottom left
0.5f, -0.5f, 0.0f, // bottom right
0.5f, 0.5f, 0.0f }; // top right
private short drawOrder[] = { 0, 1, 2, 0, 2, 3 }; // 顶点的绘制顺序
public Square() {
// initialize vertex byte buffer for shape coordinates
ByteBuffer bb = ByteBuffer.allocateDirect(
// (坐标数 * 4)
squareCoords.length * 4);
bb.order(ByteOrder.nativeOrder());
vertexBuffer = bb.asFloatBuffer();
vertexBuffer.put(squareCoords);
vertexBuffer.position(0);
// 为绘制列表初始化字节缓冲
ByteBuffer dlb = ByteBuffer.allocateDirect(
// (对应顺序的坐标数 * 2)short是2字节
drawOrder.length * 2);
dlb.order(ByteOrder.nativeOrder());
drawListBuffer = dlb.asShortBuffer();
drawListBuffer.put(drawOrder);
drawListBuffer.position(0);
}
}