Android OpenGL ES 1.x 教程的Native实现

花了一两天时间,改写了Android OpenGL ES 1.0 教程,用Native c实现OpenGL绘图部分

最近打算学习Android OpenGL ES开发,这个教程是个起点。在这里记录了从开发环境准备、到实现一个最基本的OpenGL应用的每个步骤

Demo程序执行效果:

开发环境

除了一般的Android应用开发环境(Windows+JDK+SDK+Eclipse+ADT),还需要安装NDK。我用的是VmWare+Ubuntu来跑NDK

Android设备使用2.2。根据Dev Guide,从2.2(API 8)开始支持OpenGL ES 2.0

配置Samba

由于Eclipse工程是在Windows下,而Native代码需要使用NDK来build。为了在Linux下能够访问Eclipse工程,在Windows下将Eclipse的工作空间文件夹共享、并允许修改,然后在Linux下通过Samba访问共享的工作空间文件夹。设置好网络后,参考Mount a Windows Shared Folder on Linux with Samba

(1)安装包 smbclient、smbfs

(2)在/etc/fstab中增加一行

//192.168.1.44/eclipse_GLES /home/toor/shared_eclipse_GLES cifs username=****,password=****,rw,user,noauto,exec,nounix,noserverino 0 0

特别注意其中 nounix,noserverino选项,如果不指定这两个选项,后面在执行ndk-build时会报错“Value too large for defined data type”。具体原因可google一下

(3)以toor用户执行下面命令即可

toor@ubuntu:~$ mount shared_eclipse_GLES/

安装NDK

NDK解压即可

toor@ubuntu:~$ tar xjvf android-ndk-r6b-linux-x86.tar.bz2

NDK的运行需要Linux中安装了make 3.8或以上、awk,详见docs/OVERVIEW.html、docs/INSTALL.html

程序框架

Android本身提供了android.opengl.GLSurfaceView和android.opengl.GLSurfaceView.Renderer API,但是GLSurfaceView将OpenGL渲染线程封装在内部,没有留给应用程序多少控制的自由度(在我看来,例如控制fps)。因此我自己实现了GLSurfaceView(扩展自SurfaceView)和渲染线程

之所以采用Native/JNI,是因为大多数OpenGL/OpenGL ES的教程、示例代码都是c的。另外,直觉(没有实际对比验证过)每个OpenGL命令都走JNI会带来较大的额外开销,我认为按照下面的策略来分隔Java与Native可能会好一些:

  1. 整个3D模型的状态的维护、修改用Native实现
  2. 3D模型的状态的查询、修改提供JNI接口给Java层;3D模型的状态改变提供JNI的接口以回调的方式通知Java层
  3. OpenGL帧渲染以Native实现,提供JNI接口给Java层;Native层被动地按照Java层的指示进行渲染。这样就能在Java层控制帧速
  4. UI事件(Touch、按键等)由Java层捕获、预处理,并通过JNI通知给Native层;Native层对事件进行处理(导致3D模型状态的更新;如果必要的话,以回调通知Java层)
  5. 渲染线程(渲染主循环)由Java实现;其生命周期(创建、销毁、暂停、恢复等)由Activity、SurfaceView控制

GLSurfaceView

自定义的GLSurfaceView扩展自SurfaceView,其主要功能是:(1)提供OpenGL的绘图窗口,(2)控制渲染线程的生命周期,(3)UI事件捕获、分发到渲染线程(渲染线程进一步通知到Native层)

public class GLSurfaceView extends SurfaceView implements Callback {

private Renderer renderer;
private RenderRunnable renderRunnable;

public GLSurfaceView(Context context) {
super(context);

SurfaceHolder holder = getHolder();
holder.addCallback(this);

// This is important!
// Not doing this will cause failure when eglCreateWindowSurface()
holder.setFormat(PixelFormat.RGBA_8888);
}

public void setRenderer(Renderer renderer) {
this.renderer = renderer;
}

public void surfaceCreated(SurfaceHolder holder) {

// ......

renderRunnable = new RenderRunnable();
new Thread(renderRunnable, "GL_Thread").start();

// ......
}

public void surfaceDestroyed(SurfaceHolder holder) {

renderRunnable.destroy();

// ......
}

public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {

if (width > 0 && height > 0
&& (width != this.surfaceWidth || height != this.surfaceHeight)) {

if (0 == this.surfaceWidth && 0 == this.surfaceHeight) {

this.surfaceWidth = width;
this.surfaceHeight = height;

// ......

} else {

this.surfaceWidth = width;
this.surfaceHeight = height;

// ......

renderRunnable.queueEvent(new ResizeEvent());
}
}

}

/**
* Called when Activity paused
*/
public void onPause() {
if (null != renderRunnable) {
renderRunnable.pause();
}
}

/**
* Called when Activity resumed
*/
public void onResume() {
if (null != renderRunnable) {
renderRunnable.resume();
}
}

// ......

}

渲染线程(RenderRunnable)

渲染线程由GLSurfaceView控制(创建、销毁、暂停、恢复等);主要任务是:(1)OpenGL初始化、销毁等,(2)主循环,帧渲染,(3)事件转发

    private class RenderRunnable implements Runnable {

private Thread renderThread;
private volatile boolean rendering = false;
private AtomicBoolean paused = new AtomicBoolean(false);
private ArrayList<Runnable> eventQueue = new ArrayList<Runnable>();

public void run() {
renderThread = Thread.currentThread();

initEGL();

if (null != renderer) {
renderer.init();
}


// wait until the size of surface is ready
// ......
if (null != renderer) {
renderer.resize(surfaceWidth, surfaceHeight);
}

// The main loop
for (rendering = true; rendering;) {

// Process events
synchronized (eventQueue) {
while (eventQueue.size() > 0) {
Runnable e = eventQueue.remove(0);
e.run();
}
}

// Check pause flag
synchronized (paused) {
if (paused.get()) {
try {
paused.wait();
} catch (InterruptedException e) {
break;
}
}
}

// Render a single frame
if (null != renderer) {
renderer.render();
egl.eglSwapBuffers(eglDisplay, eglSurface);
}

}// main loop

destroyEGL();
}

public void queueEvent(Runnable e) {
if (rendering) {
synchronized (eventQueue) {
eventQueue.add(e);
}
}
}

public void resume() {
if (rendering) {
synchronized (paused) {
if (paused.get()) {
paused.set(false);
paused.notifyAll();
}
}
}
}

public void pause() {
if (rendering) {
synchronized (paused) {
if (!paused.get()) {
paused.set(true);
}
}
}
}

public void destroy() {
rendering = false;
if (null != renderThread) {
renderThread.interrupt();
}

}

private boolean initEGL() {

egl = (EGL10) EGLContext.getEGL();

// ......

return true;
}

private void destroyEGL() {

// ......
}

}

渲染线程与主UI线程之间的同步归纳如下:

序号 同步? 主UI线程 渲染线程
1 Y surfaceCreated():进入  
  创建
  run():开始执行
surfaceCreated():返回  
2 Y   初始化EGL
  初始化GL
surfaceChanged():第一次Resize  
  OpenGL Resize
  进入主循环
3 N surfaceChanged():Resize 事件处理:OpenGL Resize
4 N UI事件 事件处理
5 N onPause() 暂停主循环
6 N onResume() 恢复主循环
7 Y surfaceDestroyed():进入  
  跳出主循环
surfaceDestroyed():返回  

EGL

关于EGL,请参考eglIntro

EGL作用是连接OpenGL与本地Window系统。对Android而言,本地Window系统为SurfaceHolder

初始化EGL的过程为

    private class RenderRunnable implements Runnable {

private EGL10 egl;
private EGLDisplay eglDisplay;
private EGLSurface eglSurface;
private EGLContext eglContext;

private boolean initEGL() {

egl = (EGL10) EGLContext.getEGL();

//
eglDisplay = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
if (EGL10.EGL_NO_DISPLAY == eglDisplay) {
Log.e(TAG_RENDER_RUNNABLE, "eglGetDisplay() failed");
destroyEGL();
return false;
}

//
int[] eglVersions = new int[2];
if (egl.eglInitialize(eglDisplay, eglVersions)) {
if (DEBUG) {
Log.d(TAG_RENDER_RUNNABLE, "EGL version = "
+ eglVersions[0] + "." + eglVersions[1]);
}
} else {
Log.e(TAG_RENDER_RUNNABLE, "eglInitialize() failed");
destroyEGL();
return false;
}

//
EGLConfig eglConfig;
int[] attrList = new int[] { //
EGL10.EGL_SURFACE_TYPE, EGL10.EGL_WINDOW_BIT, //
EGL10.EGL_RED_SIZE, 8, //
EGL10.EGL_GREEN_SIZE, 8, //
EGL10.EGL_BLUE_SIZE, 8, //
EGL10.EGL_DEPTH_SIZE, 16, //
EGL10.EGL_NONE //
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfig = new int[1];
if (egl.eglChooseConfig(eglDisplay, attrList, configs, 1, numConfig)
&& numConfig[0] > 0) {
eglConfig = configs[0];
} else {
Log.e(TAG_RENDER_RUNNABLE, "eglChooseConfig() failed");
destroyEGL();
return false;
}

//
eglContext = egl.eglCreateContext(eglDisplay, eglConfig,
EGL10.EGL_NO_CONTEXT, null);
if (EGL10.EGL_NO_CONTEXT == eglContext) {
Log.e(TAG_RENDER_RUNNABLE, "eglCreateContext() failed");
destroyEGL();
return false;
}

//
eglSurface = egl.eglCreateWindowSurface(eglDisplay, eglConfig,
getHolder(), null);
if (EGL10.EGL_NO_SURFACE == eglSurface) {
Log.e(TAG_RENDER_RUNNABLE, "eglCreateWindowSurface() failed");
destroyEGL();
return false;
}

//
if (!egl.eglMakeCurrent(eglDisplay, eglSurface, eglSurface,
eglContext)) {
Log.e(TAG_RENDER_RUNNABLE, "eglMakeCurrent() failed");
destroyEGL();
return false;
}

return true;
}

销毁EGL的过程:

    egl.eglMakeCurrent(eglDisplay, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);

egl.eglDestroySurface(eglDisplay, eglSurface);

egl.eglDestroyContext(eglDisplay, eglContext);

egl.eglTerminate(eglDisplay);

Renderer

Renderer接口定义了基本的OpenGL操作:(1)初始化3D场景,(2)Resize,(3)渲染一帧

public interface Renderer {

/**
* Initialize the OpenGL scene
*/
void init();

/**
* Called when window size changed
*/
void resize(int w, int h);

/**
* Render a frame
*/
void render();
}

如果要实现Native的Renderer,需要定义一个扩展Renderer 的 Native 接口,例如:

public class TriangleRenderer implements Renderer {

static {
System.loadLibrary("triangle");
}

public native void init() ;

public native void resize(int w, int h);

public native void render();

}

Activity

Activity主要功能:(1)构造GLSurfaceView、Renderer,(2)生命周期(暂停、恢复)控制

public class LearnGL_TriangleActivity extends Activity {

private GLSurfaceView glSurfaceView;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// setContentView(R.layout.main);

glSurfaceView = new GLSurfaceView(this);
glSurfaceView.setRenderer(new TriangleRenderer());
setContentView(glSurfaceView);
}

@Override
protected void onPause() {
super.onPause();
glSurfaceView.onPause();
}

@Override
protected void onResume() {
super.onResume();
glSurfaceView.onResume();
}


}

Native Renderer实现

生成JNI头文件

定义了Java的JNI接口后,利用JDK的javah工具生成Native的头文件。在Eclipse工程中,Java类(*.class)放在<PROJECT>/bin目录中,在此目录下执行

D:\eclipse_GLES\LearnGL_Triangle\bin>javah -jni -d ../jni learngl.triangle.TriangleRenderer

生成头文件<PROJECT>/jni/learngl_triangle_TriangleRenderer.h,该头文件中声明了Native接口函数的原型:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class learngl_triangle_TriangleRenderer */

#ifndef _Included_learngl_triangle_TriangleRenderer
#define _Included_learngl_triangle_TriangleRenderer
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: learngl_triangle_TriangleRenderer
* Method: init
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_learngl_triangle_TriangleRenderer_init
(JNIEnv *, jobject);

/*
* Class: learngl_triangle_TriangleRenderer
* Method: resize
* Signature: (II)V
*/
JNIEXPORT void JNICALL Java_learngl_triangle_TriangleRenderer_resize
(JNIEnv *, jobject, jint, jint);

/*
* Class: learngl_triangle_TriangleRenderer
* Method: render
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_learngl_triangle_TriangleRenderer_render
(JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Native实现

JNI头文件中使用JNI数据类型(jint等),而OpenGL定义了自己的数据类型(GLint等)。为了让结构清晰,OpenGL的Native实现并不直接实现JNI头文件,而是建一个“胶合层”实现JNI头文件,并将JNI数据类型转换为对应的OpenGL数据类型、然后调用对应的Native实现:

#include "learngl_triangle_TriangleRenderer.h"
#include "triangle.h"

void Java_learngl_triangle_TriangleRenderer_init(JNIEnv *jni, jobject obj) {
init();
}

void Java_learngl_triangle_TriangleRenderer_resize(JNIEnv *jni, jobject obj,
jint w, jint h) {
resize(w, h);
}

void Java_learngl_triangle_TriangleRenderer_render(JNIEnv *jni, jobject obj) {
render();
}

而Native实现的接口定义在其单独的头文件中:

#ifndef _TRIANGLE_H
#define _TRIANGLE_H

#include <GLES/gl.h>

void init();
void resize(GLint w, GLint h);
void render();

#endif // _TRIANGLE_H

Native的实现源文件中不会含有JNI数据类型:

#include "triangle.h"

static float triangleCoords[] = {//
-0.5f, -0.25f, 0, //
0.5f, -0.25f, 0, //
0.0f, 0.559016994f, 0 //
};

static float angle = 0;

void init() {
glClearColor(1.0f, 0.5f, 0.5f, 1.0f);
glEnableClientState(GL_VERTEX_ARRAY);

}

void resize(GLint w, GLint h) {
glViewport(0, 0, w, h);

// make adjustments for screen ratio
float ratio = w / (float) h;
glMatrixMode(GL_PROJECTION); // set matrix to projection mode
glLoadIdentity(); // reset the matrix to its default state
glFrustumf(-ratio, ratio, -1, 1, -1, 7); // apply the projection matrix

}

void render() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// Set GL_MODELVIEW transformation mode
glMatrixMode(GL_MODELVIEW);
glLoadIdentity(); // reset the matrix to its default state

// When using GL_MODELVIEW, you must set the view point
// GLU.gluLookAt(gl, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

// Create a rotation for the triangle
angle += 3;
if (angle > 360) {
angle -= 360;
}
glRotatef(angle, 0.0f, 0.0f, 1.0f);

// Draw the triangle
glColor4f(0.63671875f, 0.76953125f, 0.22265625f, 0.0f);
glVertexPointer(3, GL_FLOAT, 0, triangleCoords);
glDrawArrays(GL_TRIANGLES, 0, 3);
}

上面的代码在X-Y平面上画了一个等边三角形,并且连续逆时针旋转(每一帧旋转3度)。由于屏幕长宽比!=1,导致等边三角形变形了。怎么解决这个问题,我还不会,留待后面继续学习。。。

NDK Build

NDK的使用参考以下文档

  • docs/OVERVIEW.html
  • docs/NDK-BUILD.html
  • docs/ANDROID-MK.html

编写Android.mk

NDK使用<PROJECT>/jni/Android.mk文件来进行build。关于Android.mk文件的编写,参考以下文档:

  • docs/OVERVIEW.html
  • docs/ANDROID-MK.html
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := triangle
LOCAL_SRC_FILES := learngl_triangle_TriangleRenderer.c \
triangle.c

LOCAL_LDLIBS := -lGLESv1_CM

include $(BUILD_SHARED_LIBRARY)

第一、二行一般都是固定如此。具体含义参考docs/ANDROID-MK.html

LOCAL_MODULE 定义了生成的库的名称。假如库名称为<lib_name>,则

  • 生成的库文件的名称为 lib<lib_name>.so
  • Java Native接口中加载库的名称为<lib_name>,不用指定lib前缀和.so扩展名。例如:
    static {
System.loadLibrary("triangle");
}

LOCAL_SRC_FILES 列出用来build出库的源代码文件。多个文件用空白字符(SPACE、TAB、换行)分隔,换行在末尾用\脱字符

LOCAL_LDLIBS 列出要build的库所引用的系统库,格式为 -l<lib_name>,例如 -lGLESv1_CM对应libGLESv1_CM.so文件。多个文件用空白字符(SPACE、TAB、换行)分隔,换行在末尾用\脱字符

最后一行 include $(BUILD_SHARED_LIBRARY)告诉NDK build出共享库(Shared library,*.so)。与共享库相对的是“静态库”,命令为include $(BUILD_STATIC_LIBRARY)

ndk-build

在<PROJECT>/jni/或子目录下执行ndk-build即可build出Native库:

toor@ubuntu:~/shared_eclipse_GLES/LearnGL_Triangle/jni$ ~/android-ndk-r6b/ndk-build 

生成的库文件为<PROJECT>/libs/armeabi/lib<lib_name>.so

posted on 2011-09-25 07:37  bye_passer  阅读(8881)  评论(2编辑  收藏  举报

导航