Android OpenGL ES (1) -- 基础

 

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为绘图器对象,在表面创建时禁用颜色抖动,并设置颜色缓冲清除颜色为黑色。在表面尺寸变化时, 设置视口和投影矩阵。在绘制图形时仅仅清除颜色缓冲区。 现在运行程序,显示漆黑一片!

来加点形状吧。OpenGL以三角形为基本绘图单位,数据表示上,三角形由三个顶点组成。先定义些顶点吧。
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);
    }

其中用了一个成员变量angle 控制每次绘制的角度
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;

并修改createBuffers 添加以下内容
		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 
			};
}

你可能感兴趣的:(Android OpenGL ES (1) -- 基础)