編輯:Android開發實例
花了一兩天時間,改寫了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用戶執行下面命令即可
[email protected]:~$ mount shared_eclipse_GLES/
NDK解壓即可
[email protected]:~$ 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庫:
[email protected]:~/shared_eclipse_GLES/LearnGL_Triangle/jni$ ~/android-ndk-r6b/ndk-build
生成的庫文件為<PROJECT>/libs/armeabi/lib<lib_name>.so
Android提供了許多方法來控制播放的音頻/視頻文件和流。其中該方法是通過一類稱為MediaPlayer。Android是提供MediaPlayer類訪問內置的媒體播放
Android應用程序可以在許多不同地區的許多設備上運行。為了使應用程序更具交互性,應用程序應該處理以適合應用程序將要使用的語言環境方面的文字,數字,文件等。在本章中,我
JSON代表JavaScript對象符號。它是一個獨立的數據交換格式,是XML的最佳替代品。本章介紹了如何解析JSON文件,並從中提取所需的信息。Android提供了四個
這幾天因為項目需求,需要在ImageView上面疊加一層透明圓弧,並且在沿著圓弧的方向顯示相應的文字,效果如下圖所示: 拿到這個需求,首先想到的是自定義