Android 上开发三维图形程序主要使用 OpenGL ES 接口了, 这个OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 的子集,主要针对移动设备。该API目前由 Khronos 维护,Khronos是一个图形软硬件行业协会,该协会主要关注图形和多媒体方面的开放标准。
这个 OpenGL ES 的接口和OpenGL一样,C风格的定义,说起来比 DirectX 的 C++ 风格要简洁明了, 但弄到面向对像的 Java 里来 -- 说实话 -- 就有点不伦不类的样子了, 尤其Java 一没指针二不支持引用传参, 所以看到用数组参数返回值的情况也不奇怪了。
首先建立一个Android OpenGL 程序的框架,主要用 GLSurfaceView 类及 Renderer 接口的实现类。 在 Eclipse 中新建一个Android Project, Project Name => HelloWorld, Package name => com.leftart.android.HelloWorld, 并勾选 Create Activity => Main。
在新建好的Project中, 打开 src/com.leftart.android.HelloWorld 下的 Main.java, 修改 onCreate 方法,把 setContentView(...) 一行删除,替换为以下内容:
/* 设置窗体为全屏模式,无标题 */ getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().requestFeature(Window.FEATURE_NO_TITLE); /* 创建一个 GLSurfaceView 用于绘制表面 */ GLSurfaceView view = new GLSurfaceView(this); /* 设置 Renderer 用于执行实际的绘制工作 */ view.setRenderer(new HelloWorldRenderer(this)); /* 设置绘制模式为 持续绘制 */ view.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); /* 将创建好的 GLSurfaceView 设置为当前 Activity 的内容视图 */ setContentView(view);
接下来要定义 HelloWorldRenderer 类了, 这个类将 GLSurfaceView.Renderer 接口。
public class HelloWorldRenderer implements Renderer { public HelloWorldRenderer(Main main) { } public void onDrawFrame(GL10 gl) { // 清除颜色缓冲区背景 gl.glClear(GL10.GL_COLOR_BUFFER_BIT); } public void onSurfaceChanged(GL10 gl, int width, int height) { // 宽高比 float aspect = (float)width / (float)(height == 0 ? 1 : height); // 设置视口 gl.glViewport(0, 0, width, height); // 设置当前矩阵堆栈为投影矩阵,并将矩阵重置为单位矩阵 gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity(); /* 对当前矩阵应用透视投影变换,这个GL辅助方法以非常直观的参数 * 来设置投影矩阵:设眼睛的座标为原点,眼睛朝向Z坐标轴负方向, * 以Y坐标轴正方向为上方,视野在水平(X-Z平面)方向上角度由参数 * fovy指定,而参数 aspect 指定视野垂直方向与水平方向的比率。 * 后面两个参数分别指定眼睛可以看到前边的最近距离和最远距离。 */ GLU.gluPerspective(gl, 45.0f, aspect, 0.1f, 200.0f); /* 变换当前的透视投影矩阵,该辅助方法假设当前眼睛位于原点并朝向Z轴 * 负方向, 应用 gluLookAt 后,眼睛的位置移动到了参数 {eyeX, eyeY, eyeZ} * 所表示的三维空间点, 并调整视线方向直视三个 center* 参数所示的点。 * 最后三个参数构成的向量表示正上方。 */ GLU.gluLookAt(gl, 5f, 5f, 5f, 0f, 0f, 0f, 0, 1, 0); } public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glDisable(GL10.GL_DITHER); // 颜色抖动据说可能严重影响性能,禁用 gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);// 设置清除颜色缓冲区时用的RGBA颜色值 } }现在我们已经定义好了一个OpenGL应用程序的基本部分, 这个程序在启动时创建GLSurfaceView作为主Activity的内容, 设置HelloWorldRenderer为绘图器对象,在表面创建时禁用颜色抖动,并设置颜色缓冲清除颜色为黑色。在表面尺寸变化时, 设置视口和投影矩阵。在绘制图形时仅仅清除颜色缓冲区。 现在运行程序,显示漆黑一片!
private float[] data_vertices = { 1, 1, 1, 1, -1, 1, -1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, };这是一个立方体的8个顶点的数据,顶点可以是二、三、四维的,这里用的三维形式。数据类型为float,也可以是integer的。 定义完顶点后,还要把这些顶点连成三形才能绘制,这里用顶点索引模式,所以接下来定义索引数据
private byte[] data_triangles = { 0, 1, 2, 0, 2, 3, 0, 3, 7, 0, 7, 4, 0, 4, 5, 0, 5, 1, 6, 5, 4, 6, 4, 7, 6, 7, 3, 6, 3, 2, 6, 2, 1, 6, 1, 5 };这里定义了构成立方体的12个三角形(6个表面,每个表面2个三角形)。 在有了顶点数据和索引数据后, 还要把数据装入缓冲对像中才能被OGL使用,所以定义缓冲成员变量和一个createBuffers方法
private ByteBuffer vertices; private ByteBuffer triangles; private void createBuffers() { // 创建顶点缓冲,顶点数组使用 float 类型,每个 float 长4个字节 vertices = ByteBuffer.allocateDirect(data_vertices.length * 4); // 设置字节顺序为本机顺序 vertices.order(ByteOrder.nativeOrder()); // 通过一个 FloatBuffer 适配器,将 float 数组写入 ByteBuffer 中 vertices.asFloatBuffer().put(data_vertices); // 重置Buffer的当前位置 vertices.position(0); // 创建索引缓冲,索引使用 byte 类型,所以无需设置字节顺序,也无需写入适配。 triangles = ByteBuffer.allocateDirect(data_triangles.length); triangles.put(data_triangles); triangles.position(0); }然后在 Renderer 的构造方法中调用 createBuffers 方法创建数据缓冲对象。
public HelloWorldRenderer(Main main)
{
createBuffer();
}
现在要绘制这个立方体了
public void onDrawFrame(GL10 gl) { // 清除颜色缓冲 gl.glClear(GL10.GL_COLOR_BUFFER_BIT); // 设置当前矩阵堆栈为模型堆栈,并重置堆栈, // 即随后的矩阵操作将应用到要绘制的模型上 gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); // 将旋转矩阵应用到当前矩阵堆栈上,即旋转模型 gl.glRotatef(angle, 1, 1, 1); angle += 0.1; // 递增角度值以便每次以不同角度绘制 // 设置颜色,模型将以此颜色绘制 gl.glColor4f(0, 1f, 0f, 1f); // 启用顶点数组 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); // 设置顶点数组指针为 ByteBuffer 对象 vertices // 第一个参数为每个顶点包含的数据长度(以第二个参数表示的数据类型为单位) gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertices); // 绘制 triangles 表示的三角形 gl.glDrawElements(GL10.GL_TRIANGLES, triangles.remaining(), GL10.GL_UNSIGNED_BYTE, triangles); // 禁用顶点数组 gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); }
private float angle = 0f;运行一下, 显示了一个旋转的立方体 -- 实际上确切的说是一个绿乎乎平平的变化的多边形。这是因为 glColor 模式简单的将颜色填充在多边形中,没有层次,所以看起来一点也不立体, 来简单怎么改进下吧。glColor 设置所有的顶点使用一个颜色值,所以一切都是平的,来为顶点单独设置下颜色看看
private float[] data_colors = { 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, }以上是8个顶点的8个颜色值,也要用 ByteBuffer 传递给GL,添加 名为colors 的ByteBuffer 成员变量
private ByteBuffer colors;
colors = ByteBuffer.allocateDirect(data_colors.length * 4); colors.order(ByteOrder.nativeOrder()); colors.asFloatBuffer().put(data_colors); colors.position(0);修改绘制代码
// 启用顶点数组、法向量、颜色数组 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_COLOR_ARRAY); // 设置顶点数组指针为 ByteBuffer 对象 vertices // 第一个参数为每个顶点包含的数据长度(以第二个参数表示的数据类型为单位) gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertices); gl.glColorPointer(4, GL10.GL_FLOAT, 0, colors); // 绘制 triangles 表示的三角形 gl.glDrawElements(GL10.GL_TRIANGLES, triangles.remaining(), GL10.GL_UNSIGNED_BYTE, triangles); // 禁用顶点、法向量、颜色数组 gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_COLOR_ARRAY);为方便观察修改一下旋转方块的代码
// 将旋转矩阵应用到当前矩阵堆栈上,即旋转模型 gl.glRotatef(anglez, 0, 0, 1); gl.glRotatef(angley, 0, 1, 0); gl.glRotatef(anglex, 1, 0, 0); anglex += 0.1; // 递增角度值以便每次以不同角度绘制 angley += 0.2; anglez += 0.3;并将成员变量
private float angle = 0f;修改为
private float anglex = 0f; private float angley = 0f; private float anglez = 0f;运行发现在旋转过程中方块有的面出现缺失,这是因为绘制三角形的顺序问题,如果先绘制了前边的,后边的三角形在绘制时会把前边的覆盖。 这个问题可以使用深度测试来解决。 修改 onSurfaceCreated 启用深度测试
// 启用深度测试 gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL); // 深度测试方法为小于等于(时绘制) gl.glClearDepthf(1f); // 清除深度缓冲区时使用的值再次运行,效果好多了。
/**** file : Main.java ****/ package com.leftart.android.HelloWorld; import android.app.Activity; import android.opengl.*; import android.os.Bundle; import android.view.*; public class Main extends Activity { GLSurfaceView view; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); /* 设置窗体为全屏模式,无标题 */ getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); getWindow().requestFeature(Window.FEATURE_NO_TITLE); /* 创建一个 GLSurfaceView 用于绘制表面 */ GLSurfaceView view = new GLSurfaceView(this); /* 设置 Renderer 用于执行实际的绘制工作 */ view.setRenderer(new HelloWorldRenderer(this)); /* 设置绘制模式为 持续绘制 */ view.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY); /* 将创建好的 GLSurfaceView 设置为当前 Activity 的内容视图 */ setContentView(view); } }
/**** file : HelloWorldRenderer.java ****/ package com.leftart.android.HelloWorld; import java.nio.ByteBuffer; import java.nio.ByteOrder; import javax.microedition.khronos.egl.EGLConfig; import javax.microedition.khronos.opengles.GL10; import android.opengl.GLSurfaceView.Renderer; import android.opengl.GLU; public class HelloWorldRenderer implements Renderer { public HelloWorldRenderer(Main main) { createBuffers(); } public void onSurfaceCreated(GL10 gl, EGLConfig config) { gl.glDisable(GL10.GL_DITHER); // 颜色抖动据说可能严重影响性能,禁用 gl.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);// 设置清除颜色缓冲区时用的RGBA颜色值 gl.glEnable(GL10.GL_DEPTH_TEST); gl.glDepthFunc(GL10.GL_LEQUAL); gl.glClearDepthf(1f); } public void onSurfaceChanged(GL10 gl, int width, int height) { // 宽高比 float aspect = (float) width / (float) (height == 0 ? 1 : height); // 设置视口 gl.glViewport(0, 0, width, height); // 设置当前矩阵堆栈为投影矩阵,并将矩阵重置为单位矩阵 gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadIdentity(); GLU.gluPerspective(gl, 45.0f, aspect, 0.1f, 200.0f); GLU.gluLookAt(gl, 5f, 5f, 5f, 0f, 0f, 0f, 0, 1, 0); } public void onDrawFrame(GL10 gl) { // 清除颜色缓冲 gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT); // 设置当前矩阵堆栈为模型堆栈,并重置堆栈, // 即随后的矩阵操作将应用到要绘制的模型上 gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadIdentity(); gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, new float[]{5, 5, 5, 1}, 0); // 将旋转矩阵应用到当前矩阵堆栈上,即旋转模型 gl.glRotatef(anglez, 0, 0, 1); gl.glRotatef(angley, 0, 1, 0); gl.glRotatef(anglex, 1, 0, 0); anglex += 0.1; // 递增角度值以便每次以不同角度绘制 angley += 0.2; anglez += 0.3; // 启用顶点数组、法向量、颜色数组 gl.glEnableClientState(GL10.GL_VERTEX_ARRAY); gl.glEnableClientState(GL10.GL_NORMAL_ARRAY); gl.glEnableClientState(GL10.GL_COLOR_ARRAY); // 设置顶点数组指针为 ByteBuffer 对象 vertices // 第一个参数为每个顶点包含的数据长度(以第二个参数表示的数据类型为单位) gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertices); gl.glColorPointer(4, GL10.GL_FLOAT, 0, colors); // 绘制 triangles 表示的三角形 gl.glDrawElements(GL10.GL_TRIANGLES, triangles.remaining(), GL10.GL_UNSIGNED_BYTE, triangles); // 禁用顶点、法向量、颜色数组 gl.glDisableClientState(GL10.GL_VERTEX_ARRAY); gl.glDisableClientState(GL10.GL_COLOR_ARRAY); } private void createBuffers() { // 创建顶点缓冲,顶点数组使用 float 类型,每个 float 长4个字节 vertices = ByteBuffer.allocateDirect(data_vertices.length * 4); // 设置字节顺序为本机顺序 vertices.order(ByteOrder.nativeOrder()); // 通过一个 FloatBuffer 适配器,将 float 数组写入 ByteBuffer 中 vertices.asFloatBuffer().put(data_vertices); // 重置Buffer的当前位置 vertices.position(0); // 创建索引缓冲,索引使用 byte 类型,所以无需设置字节顺序,也无需写入适配。 triangles = ByteBuffer.allocateDirect(data_triangles.length * 2); triangles.put(data_triangles); triangles.position(0); colors = ByteBuffer.allocateDirect(data_colors.length * 4); colors.order(ByteOrder.nativeOrder()); colors.asFloatBuffer().put(data_colors); colors.position(0); } private ByteBuffer vertices; private ByteBuffer triangles; private ByteBuffer colors; private float anglex = 0f; private float angley = 0f; private float anglez = 0f; private float[] data_vertices = { 1, 1, 1, 1, -1, 1, -1, -1, 1, -1, 1, 1, 1, 1, -1, 1, -1, -1, -1, -1, -1, -1, 1, -1, }; private float[] data_colors = { 1, 1, 0, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, }; private byte[] data_triangles = { 0, 1, 2, 0, 2, 3, 0, 3, 7, 0, 7, 4, 0, 4, 5, 0, 5, 1, 6, 5, 4, 6, 4, 7, 6, 7, 3, 6, 3, 2, 6, 2, 1, 6, 1, 5 }; }