分类: 嵌入式
2012-01-17 12:31:44
一直没时间学习手机,以前写过一篇 OpenGL ES 的学习笔记,结果数据丢失。。。还好发现网上有朋友替我做了备份(排版丢失,花了点时间才所代码行分清楚)。这两天重新翻出了 eclipse 和 Android SDK,复习下基础先
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.leftcode.android.HelloWorld, 并勾选 Create Activity => Main。
在新建好的Project中, 打开 src/com.leftcode.android.HelloWorld 下的 Main.java,添加一个成员变量
修改 onCreate 方法,把 setContentView(…) 一行删除,替换为以下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 |
// 设置窗体为全屏模式,无标题
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
getWindow().requestFeature(Window.FEATURE_NO_TITLE);
// 创建一个 GLSurfaceView 用于绘制表面
view = new GLSurfaceView(this);
// 设置 Renderer 用于执行实际的绘制工作
view.setRenderer(new HelloWorldRenderer(this));
// 设置绘制模式为 持续绘制
view.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
// 将创建好的 GLSurfaceView 设置为当前 Activity 的内容视图
setContentView(view); |
接下来要定义 HelloWorldRenderer 类了, 这个类将 GLSurfaceView.Renderer 接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 |
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为绘图器对象,在表面创建时禁用颜色抖动,并设置颜色缓冲清除颜色为黑色。在表面尺寸变化时, 设置视口和投影矩阵。在绘制图形时仅仅清除颜色缓冲区。 现在运行程序,显示漆黑一片!
先来加点形状。OpenGL以三角形为基本绘图单位,数据表示上,三角形由三个顶点组成。先定义些顶点
1
2
3
4
5
6
7
8
9
10 |
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的。 定义完顶点后,还要把这些顶点连成三形才能绘制,这里用顶点索引模式,所以接下来定义索引数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14 |
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使用,所以定义缓冲成员变量
1
2 |
private ByteBuffer vertices;
private ByteBuffer triangles; |
和一个createBuffers方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14 |
private void createBuffers() {
// 创建顶点缓冲,顶点数组使用 float 类型,每个 float 长4个字节
// 并设置字节顺序为本机顺序
// 然后通过 FloatBuffer 适配器将 float 数组写入到 ByteBuffer 中
vertices = ByteBuffer.allocateDirect(data_vertices.length * 4);
vertices.order(ByteOrder.nativeOrder());
vertices.asFloatBuffer().put(data_vertices);
vertices.position(0); // 重置Buffer的当前位置
// 创建索引缓冲,索引使用 byte 类型,所以无需设置字节顺序,也无需写入适配。
triangles = ByteBuffer.allocateDirect(data_triangles.length * 2);
triangles.put(data_triangles);
triangles.position(0);
} |
然后在 Renderer 的构造方法中调用 createBuffers 方法创建数据缓冲对象。
1
2
3 |
public HelloWorldRenderer(Main main) {
createBuffers();
} |
现在要绘制这个立方体了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 |
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);
} |
其中用了一个成员变量angle 控制每次绘制的角度
1 |
private float angle = 0f; |
运行一下, 显示了一个旋转的立方体 — 实际上确切的说是一个绿乎乎平平的变化的多边形。这是因为 glColor 模式简单的将颜色填充在多边形中,没有层次,所以看起来一点也不立体, 来简单怎么改进下吧。glColor 设置所有的顶点使用一个颜色值,所以一切都是平的,来为顶点单独设置下颜色看看
1
2
3
4
5
6
7
8
9
10 |
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 成员变量
1 |
private ByteBuffer colors; |
并修改createBuffers 添加以下内容
1
2
3
4
5 |
// 创建颜色缓冲
colors = ByteBuffer.allocateDirect(data_colors.length * 4);
colors.order(ByteOrder.nativeOrder());
colors.asFloatBuffer().put(data_colors);
colors.position(0); |
修改绘制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 |
// 启用顶点数组、颜色数组
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); |
将表示角度的成员变量angle 一分为三以让旋转看上去更有魅力:)
1
2
3 |
private float anglex = 0f;
private float angley = 0f;
private float anglez = 0f; |
并修改旋转模型的代码为
1
2
3
4
5
6 |
// 将旋转矩阵应用到当前矩阵堆栈上,即旋转模型
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; |
运行发现在旋转过程中方块有的面出现缺失,这是因为绘制三角形的顺序问题,如果先绘制了前边的,后边的三角形在绘制时会把前边的覆盖。 这个问题可以使用深度测试来解决。 修改 onSurfaceCreated 添加以下代码启用深度测试
1
2
3 |
gl.glEnable(GL10.GL_DEPTH_TEST); // 开启深度测试
gl.glDepthFunc(GL10.GL_LEQUAL); // 尝试测试方法为小于等于
gl.glClearDepthf(1f); // 清除缓冲时要用的值 |
再次运行,效果好多了(虚拟机截图):
最终代码:
android-opengles-1.zip