花了一两天时间,改写了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
由于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解压即可
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可能会好一些:
自定义的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();
}
}
// ......
}
渲染线程由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,请参考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接口定义了基本的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主要功能:(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();
}
}
定义了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
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的使用参考以下文档
NDK使用<PROJECT>/jni/Android.mk文件来进行build。关于Android.mk文件的编写,参考以下文档:
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>,则
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)
在<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