上一篇文章介绍了GL10的常用方法,包括如何设置颜色、如何指定坐标系、如何调整镜头参数、如何挪动观测方位等等,不过这些方法只是绘图前的准备工作,真正描绘点、线、面的制图工作并未涉及,那么本文就来谈谈如何利用GL10进行实际的三维绘图操作。
首先在三维坐标系中,每个点都有x、y、z三个方向上的坐标值,这样需要三个浮点数来表示一个点。然后一个面又至少由三个点组成,例如三个点可以构成一个三角形,而四个点可以构成一个四边形。于是OpenGL使用浮点数组表达一块平面区域的时候,数组大小=该面的顶点个数*3,也就是说,每三个浮点数用来指定一个顶点的x、y、z三轴坐标,所以总共需要三倍于顶点数量的浮点数才能表示这些顶点构成的平面。以下举个定义四边形的浮点数组例子:
float verticesFront[] = {
1f, 1f, 1f, 1f, 1f, -1f, -1f, 1f, -1f, -1f, 1f, 1f };
上述的浮点数组一共有12个浮点数,其中每三个浮点数代表一个点,因此这个四边形由下列坐标的顶点构成:点1坐标(1,1,1)、点2坐标(1,1,-1)、点3坐标(-1,1,-1)、点4坐标(-1,1,1)。
不过这个浮点数组并不能直接传给OpenGL处理,因为OpenGL的底层是用C语言实现的,C语言与其它语言(如Java)默认的数据存储方式在字节顺序上可能不同(如大端小端问题),所以其它语言的数据结构必须转换成C语言能够识别的形式,说白了就是翻译。这里面C语言能听懂的数据结构名叫FloatBuffer,于是问题的实质就变成了如何将浮点数组folat[]转换为浮点缓存FloatBuffer,具体的转换过程已经有了现成的模板,开发者只管套进去即可,详细的转换函数代码如下所示:
public static FloatBuffer getFloatBuffer(float[] array) {
//初始化字节缓冲区的大小=数组长度*数组元素大小。float类型的元素大小为Float.SIZE,
//int类型的元素大小为Integer.SIZE,double类型的元素大小为Double.SIZE。
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(array.length * Float.SIZE);
//以本机字节顺序来修改字节缓冲区的字节顺序
//OpenGL在底层的实现是C语言,与Java默认的数据存储字节顺序可能不同,即大端小端问题。
//因此,为了保险起见,在将数据传递给OpenGL之前,需要指明使用本机的存储顺序
byteBuffer.order(ByteOrder.nativeOrder());
//根据设置好的参数构造浮点缓冲区
FloatBuffer floatBuffer = byteBuffer.asFloatBuffer();
//把数组数据写入缓冲区
floatBuffer.put(array);
//设置浮点缓冲区的初始位置
floatBuffer.position(0);
return floatBuffer;
}
现在有了可供OpenGL识别的FloatBuffer对象,接着描绘三维图形就有章可循了。绘制图形之前要先调用glEnableClientState方法启用顶点开关,绘制完成之后要调用glDisableClientState方法禁用顶点开关,在这两个方法之中再进行实际的点、线、面绘制操作。下面是绘制三维图形的函数调用顺序示例:
// 启用顶点开关
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 指定三维物体的顶点坐标集合
// gl.glVertexPointer(***);
// 在顶点坐标集合之间绘制点、线、面
// gl.glDrawArrays(***);
// 禁用顶点开关
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
注意到上面代码给出了描绘动作的两个方法glVertexPointer和glDrawArrays,其中前者指定了三维物体的顶点坐标集合,后者才在顶点坐标集合之间绘制点、线、面。那么这两个方法的输入参数又是怎样取值的呢?先来看看glVertexPointer方法的函数参数定义,说明如下:
void glVertexPointer(
int size, // 指定顶点的坐标维度。三维空间有x、y、z三个坐标轴,所以三维空间的size为3。同理,二维平面的size为2,相对论时空观的size为4(三维空间+时间)
int type, // 指定顶点的数据类型。GL10.GL_FLOAT表示浮点数,GL_SHORT表示短整型,等等。
int stride, // 指定顶点之间的间隔。通常取值为0,表示这些顶点是连续的。
java.nio.Buffer pointer // 所有顶点坐标的数据集合。这个便是前面转换而来的FloatBuffer对象了。
);
通常情况下,OpenGL用于处理三维空间的连续顶点的图形绘制,故而一般可按以下格式调用glVertexPointer方法:
// 三维空间,顶点的坐标值为浮点数,且顶点是连续的集合
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, buffer);
再来看看glDrawArrays方法的函数参数定义,说明如下:
void glDrawArrays(
int mode, // 指定顶点之间的绘制模式。是只描绘点,还是描绘顶点之间的线段,还是描绘顶点构成的平面。
int first, // 从第first个顶点开始绘制。若无意外都是取值0,表示从数组下标的第0个开始绘制。
int count // 本次绘制操作的顶点数量。也就是说,从第first个点描绘到第(first+count)个顶点。
);
这里补充介绍一下glDrawArrays方法的绘制模式取值,常见的几种绘制模式取值说明如下:
GL10.GL_POINTS : 只描绘各个独立的点
GL10.GL_LINE_STRIP : 前后两个顶点用线段连接,但不闭合(最后一个点与第一个点不连接)
GL10.GL_LINE_LOOP : 前后两个顶点用线段连接,并且闭合(最后一个点与第一个点有线段连接)
GL10.GL_TRIANGLES : 每隔三个顶点绘制一个三角形的平面
按照本文的演示要求,只需绘制一个立方体的线段框架,因此可按以下格式调用glDrawArrays方法:
// 每个面画闭合的四边形线段,从第0个点开始绘制,绘制四边形的所有顶点(pointCount=4)
gl.glDrawArrays(GL10.GL_LINE_LOOP, 0, pointCount);
好不容易啰嗦了这么多,绘制一个简单的立方体已经八九不离十了,还是先来瞅瞅OpenGL绘制的立方体效果长啥样:
见过了立方体的效果图,再来看看完整立方体的图形绘制代码片段:
// 声明立方体六个面的顶点集合的初始浮点数组定义
private ArrayList<FloatBuffer> mVertices = new ArrayList<FloatBuffer>();
float[] verticesFront = {
1f, 1f, 1f, 1f, 1f, -1f, -1f, 1f, -1f, -1f, 1f, 1f };
float[] verticesBack = {
1f, -1f, 1f, 1f, -1f, -1f, -1f, -1f, -1f, -1f, -1f, 1f };
float[] verticesTop = {
1f, 1f, 1f, 1f, -1f, 1f, -1f, -1f, 1f, -1f, 1f, 1f };
float[] verticesBottom = {
1f, 1f, -1f, 1f, -1f, -1f, -1f, -1f, -1f, -1f, 1f, -1f };
float[] verticesLeft = {
-1f, 1f, 1f, -1f, 1f, -1f, -1f, -1f, -1f, -1f, -1f, 1f };
float[] verticesRight = {
1f, 1f, 1f, 1f, 1f, -1f, 1f, -1f, -1f, 1f, -1f, 1f };
int pointCount = verticesFront.length/3;
// 把顶点集合的数据结构由float[]转换为FloatBuffer
private void initVertexs() {
mVertices.add(FileUtil.getFloatBuffer(verticesFront));
mVertices.add(FileUtil.getFloatBuffer(verticesBack));
mVertices.add(FileUtil.getFloatBuffer(verticesTop));
mVertices.add(FileUtil.getFloatBuffer(verticesBottom));
mVertices.add(FileUtil.getFloatBuffer(verticesLeft));
mVertices.add(FileUtil.getFloatBuffer(verticesRight));
}
// 根据顶点数据集合,绘制立方体的线段框架
private void drawCube(GL10 gl) {
// 启用顶点开关
gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
// 立方体由六个正方形平面组成
for (FloatBuffer buffer : mVertices) {
// 将顶点坐标传给 OpenGL 管道
//size: 每个顶点有几个数值描述。必须是2,3 ,4 之一。
//type: 数组中每个顶点的坐标类型。取值:GL_BYTE, GL_SHORT, GL_FIXED, GL_FLOAT。
//stride:数组中每个顶点间的间隔,步长(字节位移)。取值若为0,表示数组是连续的
//pointer:即存储顶点的Buffer
gl.glVertexPointer(3, GL10.GL_FLOAT, 0, buffer);
// 用画线的方式将点连接并画出来
//GL_POINTS ————绘制独立的点
//GL_LINE_STRIP————绘制连续的线段,不封闭
//GL_LINE_LOOP————绘制连续的线段,封闭
//GL_LINES————顶点两两连接,为多条线段构成
//GL_TRIANGLES————每隔三个顶点构成一个三角形
//GL_TRIANGLE_STRIP————每相邻三个顶点组成一个三角形
//GL_TRIANGLE_FAN————以一个点为三角形公共顶点,组成一系列相邻的三角形
gl.glDrawArrays(GL10.GL_LINE_LOOP, 0, pointCount);
}
// 禁用顶点开关
gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
}
立方体算是最简单的三维物体了,倘若是一个球体,也能按照上述的代码逻辑绘制球形框架,当然这个近似球体需要由许多个小三角形构成。下面是利用OpenGL绘制的球体效果图: