底下敘述使用 Cocos2d-x 製作 Android Live Wallpaper 的方法。(建構環境 : OSX ML,cocos2d-2.1rc0-x-2.1.3,Android NDK r8e,Android SDK r22。執行環境 : Sony Xperia TX with Android 4.1.2)
A. 程式架構 :
Cocos2d-x 本身提供一個便利的方法建立 Android Project ( 執行 Cocos2d-x 裡的 create-android-project.sh 即可 ),從自動建立的 Android Project 大概可以了解 App 模式之 Cocos2d-x 與 Android 作業系統間的關係,目前 Cocos2d-x 的程式架構在 Android 環境下,單一 Process 只能同時擁有及處理單一 GL Context 及 GLSurface,若要直接把 Cocos2dxRenderer 及 Cocos2dxGLSurfaceView 整合至 WallpaperService.Engine 內會衍生不少問題。因此這裡實作把它們整合在 WallpaperService。各別 Engine 在進行繪圖時 ( onDrawFrame ),只實作貼圖 ( Texture ) 的動作 ( 使用 Android 系統提供的 GL Function ),其中圖片的來源則是向 WallpaperService 的 Cocos2dxRenderer 取得 ( 此處的 Cocos2dxRenderer 是以 off-screen rendering 方式產生貼圖 )。
在不同的 GL Context 之間分享貼圖的方法,這裡選擇使用 EGL_KHR_gl_texture_2D_image Extension 的功能來實作,可避免因為使用 glReadPixles 及 glTexImage2D 造成效能低落的問題。
B. 實作程式 :
1. 建立 Android Project
從 cocos2d-x 網站下載檔案 cocos2d-2.1rc0-x-2.1.3.zip ,將壓縮檔解開並編輯檔案 create-android-project.sh,找到 :
輸入 package path (com.macaronics.cclw) 之後,畫面會出現目前 Android SDK 已安裝的 API (依照各電腦情況不同),此範例選擇的是 id:1 (API Level 10),最後輸入專案名稱 (cclw),就完成了。
打開 eclipse 並匯入 cclw 這個專案,完成之後會發現幾個錯誤回報,那是因為找不到 cocos2dx package,此時匯入 cocos2dx 專案即可解決 ( cocos2dx 專案的路徑為 cocos2d-2.1rc0-x-2.1.3/cocos2dx/platform/android/java )。
本專案建立時選擇之 API Level 為 10,因此匯入完成之後檢查 AndroidManifest.xml 檔案,將 :
2. 新增 PixelBuffer
如圖所示,在 cclw/src/com/macaronics/cclw 資料夾底下增加檔案 PixelBuffer.java :
PixelBuffer 的作用是向系統要求建立 GL Context 與 Surface Buffer ,它和 GLSurfaceView 的功能幾乎一樣,只是它所建立的 Surface Buffer 是 PBuffer (作為 off-screen rendering 使用),與 GLSurfaceView 建立的 Window Surface Buffer 不同。此段程式碼參考的來源在此,但 EGL Config Chooser 的部分參考 GLSurfaceView 做了一些修改,程式碼內容如下 :
WallpaperService.Engine 命令 Cocos2d-x 以 off-screen rendering 的方式將圖案畫在 PBuffer 裡,然後將 PBuffer 裡的資料複製到自己的 GL Context,此處複製 PBuffer 資料的步驟使用 EGL_KHR_gl_texture_2D_image Extension 以 C 語言來實作,讓 WallpaperService.Engine 以 JNI 的方式呼叫。實作時,直接將函數寫在檔案 cclw/jni/hellocpp/main.cpp 內,底下列出修改後的結果 :
4. WallpaperService 的部分 :
新增檔案 cclwservice.java 於資料夾 cclw/src/com/macaronics/cclw 。檔案的程式碼如底下所示,其中 WallpaperService 主要分為 Engine、Renderer 及 Service (負責創造 Engine Instance 、 Cocos2d-x Renderer 執行緒及收拾殘局) :
底下為 Engine 部分的程式 ( Engine 負責建立自己的 Renderer、接收系統訊息,及乎如同一般的 Activity ) :
WallpaperGLRenderer 的部分 : (底下的程式如同一般的 GL 程式,使用 OpenGL ES 2.0 來顯示一張 Texture,只是這個 Texture 的資料是 PixelBuffer 的 PBuffer 的資料 )
5. 設定 AndroidManifest.xml
為 WallpaperService 新增 service 標籤,如底下所示 :
6. 設定 jni/Android.mk :
此時若執行 native_build.sh 會顯示一些錯誤,那是因為尚未設定連結 EGL Library,將 jni/Android.mk 修改如下即可正常編譯 :
7. 執行結果 :
A. 程式架構 :
Cocos2d-x 本身提供一個便利的方法建立 Android Project ( 執行 Cocos2d-x 裡的 create-android-project.sh 即可 ),從自動建立的 Android Project 大概可以了解 App 模式之 Cocos2d-x 與 Android 作業系統間的關係,目前 Cocos2d-x 的程式架構在 Android 環境下,單一 Process 只能同時擁有及處理單一 GL Context 及 GLSurface,若要直接把 Cocos2dxRenderer 及 Cocos2dxGLSurfaceView 整合至 WallpaperService.Engine 內會衍生不少問題。因此這裡實作把它們整合在 WallpaperService。各別 Engine 在進行繪圖時 ( onDrawFrame ),只實作貼圖 ( Texture ) 的動作 ( 使用 Android 系統提供的 GL Function ),其中圖片的來源則是向 WallpaperService 的 Cocos2dxRenderer 取得 ( 此處的 Cocos2dxRenderer 是以 off-screen rendering 方式產生貼圖 )。
在不同的 GL Context 之間分享貼圖的方法,這裡選擇使用 EGL_KHR_gl_texture_2D_image Extension 的功能來實作,可避免因為使用 glReadPixles 及 glTexImage2D 造成效能低落的問題。
B. 實作程式 :
1. 建立 Android Project
從 cocos2d-x 網站下載檔案 cocos2d-2.1rc0-x-2.1.3.zip ,將壓縮檔解開並編輯檔案 create-android-project.sh,找到 :
# set environment paramters NDK_ROOT_LOCAL="/home/laschweinski/android/android-ndk-r5" ANDROID_SDK_ROOT_LOCAL="/home/laschweinski/android/android-sdk-linux_86"將它們修改成自己電腦裡 NDK 及 Android SDK 的路徑,例如 :
# set environment paramters NDK_ROOT_LOCAL="/Users/macaronics/Downloads/android-ndk-r8e" ANDROID_SDK_ROOT_LOCAL="/Users/macaronics/android-sdks"完成之後執行這個 sh 檔案,執行畫面如下 :
執行 create-android-project.sh
輸入 package path (com.macaronics.cclw) 之後,畫面會出現目前 Android SDK 已安裝的 API (依照各電腦情況不同),此範例選擇的是 id:1 (API Level 10),最後輸入專案名稱 (cclw),就完成了。
打開 eclipse 並匯入 cclw 這個專案,完成之後會發現幾個錯誤回報,那是因為找不到 cocos2dx package,此時匯入 cocos2dx 專案即可解決 ( cocos2dx 專案的路徑為 cocos2d-2.1rc0-x-2.1.3/cocos2dx/platform/android/java )。
匯入 cclw 及 cocos2dx 專案
<uses-sdk android:minSdkVersion="8"/>改為
<uses-sdk android:minSdkVersion="10"/>到目前為止已經可以正常編譯並執行 cclw 專案,首先在資料夾 cocos2d-2.1rc0-x-2.1.3/cclw/proj.android 底下執行 build_native.sh,完成之後會看到專案裡的 lib 資料夾多出檔案 libgame.so,接著在 eclipse 點選 cclw 專案,並於功能表上選擇 Run --> Run As --> Android Application 即可實際編譯執行。此外也可直接在 Terminal 裡輸入 :
ant release -Dsdk.dir=/Users/macaronics/android-sdks/編譯 apk 檔案 (/Users/macaronics/android-sdks/ 為 Android SDK 之路徑)。
2. 新增 PixelBuffer
如圖所示,在 cclw/src/com/macaronics/cclw 資料夾底下增加檔案 PixelBuffer.java :
新增檔案 PixelBuffer.java
PixelBuffer 的作用是向系統要求建立 GL Context 與 Surface Buffer ,它和 GLSurfaceView 的功能幾乎一樣,只是它所建立的 Surface Buffer 是 PBuffer (作為 off-screen rendering 使用),與 GLSurfaceView 建立的 Window Surface Buffer 不同。此段程式碼參考的來源在此,但 EGL Config Chooser 的部分參考 GLSurfaceView 做了一些修改,程式碼內容如下 :
public class PixelBuffer { final static String TAG = "PixelBuffer"; final static boolean LIST_CONFIGS = true; GLSurfaceView.Renderer mRenderer; // borrow this interface int mWidth, mHeight; Bitmap mBitmap; EGL10 mEGL; EGLDisplay mEGLDisplay; EGLConfig[] mEGLConfigs; EGLConfig mEGLConfig; EGLContext mEGLContext; EGLSurface mEGLSurface; GL10 mGL; EGLConfigChooser mEGLConfigChooser; int mEGLContextClientVersion; String mThreadOwner; private static int EGL_CONTEXT_CLIENT_VERSION = 0x3098; private static int EGL_LARGEST_PBUFFER = 0x3058; private static int EGL_OPENGL_ES2_BIT = 4; public PixelBuffer(int width, int height) { mWidth = width; mHeight = height; mEGLContextClientVersion =2; int[] version = new int[2]; int[] attribList = new int[] { EGL_WIDTH, mWidth, EGL_HEIGHT, mHeight, EGL_LARGEST_PBUFFER, 1, EGL_NONE }; int [] context_attribList =new int[] { EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, EGL_NONE }; // No error checking performed, minimum required code to elucidate logic mEGL = (EGL10) EGLContext.getEGL(); mEGLDisplay = mEGL.eglGetDisplay(EGL_DEFAULT_DISPLAY); mEGL.eglInitialize(mEGLDisplay, version); Log.i("cclw", "eglInitialized, version ="+version[0]+"."+version[1]); Log.i("cclw", "EGL Extension : "+mEGL.eglQueryString(mEGLDisplay, EGL_EXTENSIONS)); mEGLConfigChooser =new SimpleEGLConfigChooser(8, 8, 8, 0, true); mEGLConfig =mEGLConfigChooser.chooseConfig(mEGL, mEGLDisplay); if (mEGLConfig ==null) { mEGLConfigChooser =new SimpleEGLConfigChooser(5, 8, 5, 0, true); mEGLConfig =mEGLConfigChooser.chooseConfig(mEGL, mEGLDisplay); } mEGLContext = mEGL.eglCreateContext(mEGLDisplay, mEGLConfig, EGL_NO_CONTEXT, context_attribList); mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, mEGLConfig, attribList); mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext); mGL = (GL10) mEGLContext.getGL(); // Record thread owner of OpenGL context mThreadOwner = Thread.currentThread().getName(); Log.i("cclw", "PixelBuffer created. mThreadOwner ="+mThreadOwner); } public void dispose() { if (mEGLDisplay !=EGL_NO_DISPLAY) { mEGL.eglMakeCurrent(mEGLDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); if (mEGLContext !=EGL_NO_CONTEXT) mEGL.eglDestroyContext(mEGLDisplay, mEGLContext); if (mEGLSurface !=EGL_NO_SURFACE) mEGL.eglDestroySurface(mEGLDisplay, mEGLSurface); mEGL.eglTerminate(mEGLDisplay); } } public void setRenderer(GLSurfaceView.Renderer renderer) { mRenderer = renderer; // Does this thread own the OpenGL context? if (!Thread.currentThread().getName().equals(mThreadOwner)) { Log.e(TAG, "setRenderer: This thread does not own the OpenGL context."); return; } // Call the renderer initialization routines mRenderer.onSurfaceCreated(mGL, mEGLConfig); mRenderer.onSurfaceChanged(mGL, mWidth, mHeight); } public void drawFrame() { makeCurrent(); mRenderer.onDrawFrame(mGL); } public void makeCurrent() { // Do we have a renderer? if (mRenderer == null) { Log.e(TAG, "getBitmap: Renderer was not set."); return; } // Does this thread own the OpenGL context? if (!Thread.currentThread().getName().equals(mThreadOwner)) { Log.e(TAG, "getBitmap: This thread does not own the OpenGL context."); return; } // Call the renderer draw routine mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext); } public interface EGLConfigChooser { EGLConfig chooseConfig(EGL10 egl, EGLDisplay display); } private abstract class BaseConfigChooser implements EGLConfigChooser { public BaseConfigChooser(int[] configSpec) { mConfigSpec =configSpec; } public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) { int[] num_config = new int[1]; if (!egl.eglChooseConfig(display, mConfigSpec, null, 0, num_config)) { throw new IllegalArgumentException("eglChooseConfig failed"); } int numConfigs = num_config[0]; if (numConfigs <= 0) { throw new IllegalArgumentException("No configs match configSpec"); } EGLConfig[] configs = new EGLConfig[numConfigs]; if (!egl.eglChooseConfig(display, mConfigSpec, configs, numConfigs, num_config)) { throw new IllegalArgumentException("eglChooseConfig#2 failed"); } //list configs listConfig(configs); EGLConfig config = chooseConfig(egl, display, configs); if (config == null) { throw new IllegalArgumentException("No config chosen"); } return config; } abstract EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs); protected int[] mConfigSpec; } private class ComponentSizeChooser extends BaseConfigChooser { public ComponentSizeChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize, int stencilSize) { super(new int[] { EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT, EGL10.EGL_SURFACE_TYPE, EGL_WINDOW_BIT | EGL_PBUFFER_BIT, EGL10.EGL_RED_SIZE, redSize, EGL10.EGL_GREEN_SIZE, greenSize, EGL10.EGL_BLUE_SIZE, blueSize, EGL10.EGL_ALPHA_SIZE, alphaSize, EGL10.EGL_DEPTH_SIZE, depthSize, EGL10.EGL_STENCIL_SIZE, stencilSize, EGL10.EGL_NONE}); mValue = new int[1]; mRedSize = redSize; mGreenSize = greenSize; mBlueSize = blueSize; mAlphaSize = alphaSize; mDepthSize = depthSize; mStencilSize = stencilSize; } @Override public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display, EGLConfig[] configs) { for (EGLConfig config : configs) { int d = findConfigAttrib(egl, display, config, EGL10.EGL_DEPTH_SIZE, 0); int s = findConfigAttrib(egl, display, config, EGL10.EGL_STENCIL_SIZE, 0); if ((d >= mDepthSize) && (s >= mStencilSize)) { int r = findConfigAttrib(egl, display, config, EGL10.EGL_RED_SIZE, 0); int g = findConfigAttrib(egl, display, config, EGL10.EGL_GREEN_SIZE, 0); int b = findConfigAttrib(egl, display, config, EGL10.EGL_BLUE_SIZE, 0); int a = findConfigAttrib(egl, display, config, EGL10.EGL_ALPHA_SIZE, 0); if ((r == mRedSize) && (g == mGreenSize) && (b == mBlueSize) && (a == mAlphaSize)) { return config; } } } return null; } private int findConfigAttrib(EGL10 egl, EGLDisplay display, EGLConfig config, int attribute, int defaultValue) { if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) { return mValue[0]; } return defaultValue; } private int[] mValue; // Subclasses can adjust these values: protected int mRedSize; protected int mGreenSize; protected int mBlueSize; protected int mAlphaSize; protected int mDepthSize; protected int mStencilSize; } private class SimpleEGLConfigChooser extends ComponentSizeChooser { public SimpleEGLConfigChooser(int red, int green, int blue, int alpha, boolean withDepthBuffer) { super(red, green, blue, alpha, withDepthBuffer ? 16 : 0, 0); } } private void listConfig(EGLConfig[] tmpConfig) { Log.i("cclw", "Config List {"); for (EGLConfig config : tmpConfig) { int d, s, r, g, b, a; // Expand on this logic to dump other attributes d = getConfigAttrib(config, EGL_DEPTH_SIZE); s = getConfigAttrib(config, EGL_STENCIL_SIZE); r = getConfigAttrib(config, EGL_RED_SIZE); g = getConfigAttrib(config, EGL_GREEN_SIZE); b = getConfigAttrib(config, EGL_BLUE_SIZE); a = getConfigAttrib(config, EGL_ALPHA_SIZE); Log.i("cclw", " <d,s,r,g,b,a> = <" + d + "," + s + "," + r + "," + g + "," + b + "," + a + ">"); } Log.i("cclw", "}"); } private int getConfigAttrib(EGLConfig config, int attribute) { int[] value = new int[1]; return mEGL.eglGetConfigAttrib(mEGLDisplay, config, attribute, value)? value[0] : 0; } }3. EGL_KHR_gl_texture_2D_image Extension 的部分 :
WallpaperService.Engine 命令 Cocos2d-x 以 off-screen rendering 的方式將圖案畫在 PBuffer 裡,然後將 PBuffer 裡的資料複製到自己的 GL Context,此處複製 PBuffer 資料的步驟使用 EGL_KHR_gl_texture_2D_image Extension 以 C 語言來實作,讓 WallpaperService.Engine 以 JNI 的方式呼叫。實作時,直接將函數寫在檔案 cclw/jni/hellocpp/main.cpp 內,底下列出修改後的結果 :
#include "AppDelegate.h" #include "platform/android/jni/JniHelper.h" #include <jni.h> #include <android/log.h> #include "cocos2d.h" #include "HelloWorldScene.h" #define EGL_EGLEXT_PROTOTYPES #include <EGL/egl.h> #include <EGL/eglext.h> #include <GLES2/gl2ext.h> #define LOG_TAG "main" #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__) using namespace cocos2d; extern "C" { jint JNI_OnLoad(JavaVM *vm, void *reserved) { JniHelper::setJavaVM(vm); return JNI_VERSION_1_4; } void Java_org_cocos2dx_lib_Cocos2dxRenderer_nativeInit(JNIEnv* env, jobject thiz, jint w, jint h) { if (!CCDirector::sharedDirector()->getOpenGLView()) { CCEGLView *view = CCEGLView::sharedOpenGLView(); view->setFrameSize(w, h); AppDelegate *pAppDelegate = new AppDelegate(); CCApplication::sharedApplication()->run(); } /* else { ccDrawInit(); ccGLInvalidateStateCache(); CCShaderCache::sharedShaderCache()->reloadDefaultShaders(); CCTextureCache::reloadAllTextures(); CCNotificationCenter::sharedNotificationCenter()->postNotification(EVNET_COME_TO_FOREGROUND, NULL); CCDirector::sharedDirector()->setGLDefaultValues(); } */ } EGLImageKHR eglImage =NULL; EGLDisplay eglDisplay =NULL; void Java_com_macaronics_cclw_cclwservice_nativeAttachEGLImageKHR(JNIEnv* env, jobject thiz) { if (eglImage !=NULL) { glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage); } } void Java_com_macaronics_cclw_cclwservice_nativeDestroyEGLImageKHR(JNIEnv* env, jobject thiz) { if (eglImage !=NULL && eglDisplay !=NULL) { bool ret =eglDestroyImageKHR(eglDisplay, eglImage); LOGD("cclw, nativeDestroyEGLImageKHR ret val =%d", ret); eglImage =NULL; eglDisplay =NULL; } } void Java_com_macaronics_cclw_cclwservice_nativeRenderToTexture(JNIEnv* env, jobject thiz) { int textureID =HelloWorld::renderToTexture(); if (eglImage ==NULL) { LOGD("cclw, nativeRenderToTexture - Generate EGLImageKHR ..."); EGLint imageAttributes[] = { EGL_GL_TEXTURE_LEVEL_KHR, 0, // mip map level to reference EGL_IMAGE_PRESERVED_KHR, EGL_FALSE, EGL_NONE, EGL_NONE }; eglDisplay =eglGetCurrentDisplay(); EGLContext eglContext =eglGetCurrentContext(); LOGD("cclw, eglDisplay=%d, eglContext=%d, textureID=%d", eglDisplay, eglContext, textureID); eglImage = eglCreateImageKHR( eglDisplay, eglContext, EGL_GL_TEXTURE_2D_KHR, reinterpret_cast<EGLClientBuffer>(textureID), imageAttributes ); } } }其中值得注意的是,並不是所有的 GPU 都支援 EGL_GL_TEXTURE_2D_KHR (例如: Adreno 205 GPU 不支援),此外,函數 :
HelloWorld::renderToTexture();利用 Cocos2d-x 提供的 CCRenderTexture 物件將圖案畫在 Texture 裡,實作內容如下 (檔案 HelloWorldScene.cpp) :
CCRenderTexture* g_RT =NULL; int HelloWorld::renderToTexture() { if (g_RT==NULL) { g_RT =CCRenderTexture::create(720, 1184, kCCTexture2DPixelFormat_RGBA8888); g_RT->retain(); g_RT->setPosition(ccp(720 / 2, 1184 / 2)); CCLOG("cclw, create RenderTexture (id=%d)...", g_RT->getSprite()->getTexture()->getName()); } g_RT->beginWithClear(0.0f, 0.0f, 0.0f, 1.0f); CCDirector::sharedDirector()->mainLoop(); g_RT->end(); return g_RT->getSprite()->getTexture()->getName(); }其中的 720 是畫面寬度,1184 是畫面高度。
4. WallpaperService 的部分 :
新增檔案 cclwservice.java 於資料夾 cclw/src/com/macaronics/cclw 。檔案的程式碼如底下所示,其中 WallpaperService 主要分為 Engine、Renderer 及 Service (負責創造 Engine Instance 、 Cocos2d-x Renderer 執行緒及收拾殘局) :
public class cclwservice extends WallpaperService { public final static long NANOSECONDSPERSECOND = 1000000000L; public final static long NANOSECONDSPERMICROSECOND = 1000000; public static native void nativeAttachEGLImageKHR(); public static native void nativeDestroyEGLImageKHR(); public static native void nativeRenderToTexture(); //---------------------- // LIBRARY // static { System.loadLibrary("game"); } //---------------------- // VARIABLES // public static cclwservice inst =null; public static PixelBuffer pb =null; public static Handler ph =null; public static HandlerThread mThread; private Cocos2dxRenderer pc2r =null; //---------------------- // MAIN FUNCTION // @Override public Engine onCreateEngine() { Log.i("cclw", "cclw - onCreateEngine"); Engine retEng =null; retEng =new GLEngine(); return retEng; } @Override public void onCreate(){ Log.i("cclw", "cclw - onCreate"); super.onCreate(); inst =this; mThread =new HandlerThread("Rendering Thread"); mThread.start(); ph =new Handler(mThread.getLooper()); ph.post(new Runnable(){ @Override public void run(){ Log.i("cclw", "cclw - calling Cocos2dxHelper.init..."); Cocos2dxHelper.init(cclwservice.inst, null); //prepare offscreen buffer Log.i("cclw", "cclw - prepare offscreen buffer..."); if (pb ==null) pb =new PixelBuffer(720, 1184); Log.i("cclw", "cclw - create native renderer..."); pc2r =new Cocos2dxRenderer(); pc2r.setScreenWidthAndHeight(720, 1184); pb.setRenderer(pc2r); } }); } @Override public void onDestroy(){ Log.i("cclw", "cclw - onDestroy"); super.onDestroy(); //dispose handler ph =null; //dispose thread Log.i("cclw", "cclw - dispose Rendering Thread..."); mThread.quit(); cclwservice.nativeDestroyEGLImageKHR(); Log.i("cclw", "cclw - dispose PixelBuffer"); if (pb !=null){ pb.dispose(); } pb =null; } synchronized static public void renderToTexture(){ pb.makeCurrent(); cclwservice.nativeRenderToTexture(); } }函數 renderToTexture 執行時,會透過 JNI 呼叫 cclw/jni/hellocpp/main.cpp 檔案裡的函數 Java_com_macaronics_cclw_cclwservice_nativeRenderToTexture 讓 Cocos2d-x 將圖案畫在 PixelBuffer 所建立之 PBuffer 的 Texture 裡,並將此 Texture 設定為 KHR_image。
底下為 Engine 部分的程式 ( Engine 負責建立自己的 Renderer、接收系統訊息,及乎如同一般的 Activity ) :
//---------------------- // GL Engine // private class GLEngine extends Engine { //---------------------- // VARIABLES // private WallpaperGLSurfaceView glSurfaceView; private boolean rendererHasBeenSet; private WallpaperGLRenderer glRenderer; //---------------------- // FUNCTION // @Override public Bundle onCommand(String action, int x, int y, int z, Bundle extras, boolean resultRequested) { Log.i("cclw", "onCommand"); return super.onCommand(action, x, y, z, extras, resultRequested); } @Override public void onCreate(SurfaceHolder surfaceHolder) { super.onCreate(surfaceHolder); setTouchEventsEnabled(true); Log.i("cclw", "onCreate"); glSurfaceView = new WallpaperGLSurfaceView(cclwservice.this); setEGLContextClientVersion(2); glRenderer =new WallpaperGLRenderer(); setRenderer(glRenderer); } @Override public void onDestroy() { super.onDestroy(); glRenderer.onDestroy(); glRenderer =null; Log.i("cclw", "onDestroy"); glSurfaceView.onDestroy(); } @Override public void onVisibilityChanged(boolean visible) { super.onVisibilityChanged(visible); Log.i("cclw", "onVisibilityChanged, visible ="+visible); if (rendererHasBeenSet) { if (visible) { Log.i("cclw", "calling onResume..."); glSurfaceView.onResume(); } else { Log.i("cclw", "calling onPause..."); glSurfaceView.onPause(); } } } @Override public void onOffsetsChanged(float xOffset, float yOffset, float xOffsetStep, float yOffsetStep, int xPixelOffset, int yPixelOffset) { super.onOffsetsChanged(xOffset, yOffset, xOffsetStep, yOffsetStep, xPixelOffset, yPixelOffset); //Log.i("cclw", "onOffsetsChanged, xOffset="+xOffset+", yOffset="+yOffset+", xOffsetStep="+xOffsetStep+", yOffsetStep="+yOffsetStep+", xPixelOffset="+xPixelOffset+", yPixelOffset="+yPixelOffset); } protected void setRenderer(Renderer renderer){ glSurfaceView.setRenderer(renderer); rendererHasBeenSet =true; } protected void setEGLContextClientVersion(int version) { glSurfaceView.setEGLContextClientVersion(version); } //------------------------ // Custom SurfaceView for WallpaperService.engine // private class WallpaperGLSurfaceView extends GLSurfaceView { WallpaperGLSurfaceView(Context context) { super(context); } @Override public SurfaceHolder getHolder() { return getSurfaceHolder(); } } }值得注意的是 WallpaperGLSurfaceView 類別裡的函數 getHolder,是透過 Engine 類別的函數 getSurfaceHolder 來取得 SurfaceHolder。此外,Engine 所建立的 Renderer 與 Cocos2d-x Renderer 無關,它擁有自己的 GL Context 及 Window Surface Buffer。
WallpaperGLRenderer 的部分 : (底下的程式如同一般的 GL 程式,使用 OpenGL ES 2.0 來顯示一張 Texture,只是這個 Texture 的資料是 PixelBuffer 的 PBuffer 的資料 )
//---------------------- // RENDERER // private class WallpaperGLRenderer implements GLSurfaceView.Renderer{ private String TAG ="cclw"; int[] mTextureNameWorkspace =new int[] {0}; private final float[] mTriangleVerticesData = { -1.0f, 1.0f, 0.0f, -1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 0.0f, -1.0f, 1.0f, 0.0f, 1.0f, -1.0f, 0.0f, 1.0f, 1.0f, 0.0f }; private final float[] mTriangleNormalData ={0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f,0.0f,1.0f,0.0f}; private final float[] mTriangleTexCoordData ={0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f }; private final float[] mTriangleTexCoordData2 ={1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f }; private FloatBuffer mTriangleVertices; private FloatBuffer mTriangleNormal; private FloatBuffer mTriangleTexCoord; private FloatBuffer mTriangleTexCoord2; private final String mVertexShader = "attribute vec4 a_position; \n"+ "attribute vec3 a_normal; \n"+ "attribute vec2 a_texCoord; \n"+ "varying vec2 v_texCoord; \n"+ "varying vec3 v_normal; \n"+ "void main() \n"+ "{ \n"+ " gl_Position =a_position; \n"+ " v_normal = a_normal; \n"+ " v_texCoord = a_texCoord; \n"+ "} \n"; private final String mFragmentShader = "precision mediump float; \n"+ "varying vec2 v_texCoord; \n"+ "varying vec3 v_normal; \n"+ "uniform sampler2D s_texture; \n"+ "void main() \n"+ "{ \n"+ " gl_FragColor = texture2D( s_texture, v_texCoord );\n"+ "} \n"; private int mProgram; private int mvPositionHandle; private int mvNormalHandle; private int mvTexCoordHandle; private int mvSamplerHandle; private long mLastTickInNanoSeconds; public long aniInterval; public int mOrientation =0; public boolean onDestroyCalled =false; public WallpaperGLRenderer() { mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mTriangleVertices.put(mTriangleVerticesData).position(0); mTriangleNormal = ByteBuffer.allocateDirect(mTriangleNormalData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mTriangleNormal.put(mTriangleNormalData).position(0); mTriangleTexCoord = ByteBuffer.allocateDirect(mTriangleTexCoordData.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mTriangleTexCoord.put(mTriangleTexCoordData).position(0); mTriangleTexCoord2 =ByteBuffer.allocateDirect(mTriangleTexCoordData2.length *4).order(ByteOrder.nativeOrder()).asFloatBuffer(); mTriangleTexCoord2.put(mTriangleTexCoordData2).position(0); this.mLastTickInNanoSeconds =System.nanoTime(); aniInterval =(long) (1.0 / 30 * NANOSECONDSPERSECOND); } public void onDestroy(){ Log.i("cclw", "onDestroy called..."); onDestroyCalled =true; } protected class WorkerRunnable implements Runnable{ private final CountDownLatch doneSignal; WorkerRunnable(CountDownLatch doneSignal){ this.doneSignal =doneSignal; } public void run(){ try{ cclwservice.renderToTexture(); this.doneSignal.countDown(); }catch(Exception ex){ Log.i("cclw", "Error : Runnable return exception : "+ex); } } } @Override public void onDrawFrame(GL10 gl) { try{ final long nowInNanoSeconds = System.nanoTime(); final long interval = nowInNanoSeconds - this.mLastTickInNanoSeconds; //------------------------ // FETCH DATA // EGL10 mEGL = (EGL10) EGLContext.getEGL(); EGLSurface mEGLSurface =mEGL.eglGetCurrentSurface(EGL10.EGL_DRAW); EGLDisplay mEGLDisplay =mEGL.eglGetCurrentDisplay(); EGLContext mEGLContext =mEGL.eglGetCurrentContext(); CountDownLatch doneSignal =new CountDownLatch(1); cclwservice.ph.post(new WorkerRunnable(doneSignal)); doneSignal.await(); //------------------------ // SETUP BASIC ENVIRONMENT // mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext); if (onDestroyCalled==true) { Log.i("cclw", "onDestroyCalled==true, ignore drawing..."); return; } //------------------------ // UPDATE TEXTURE // if (mTextureNameWorkspace[0]==0){ //Load texture GLES20.glActiveTexture(GLES20.GL_TEXTURE0); gl.glGenTextures(1, mTextureNameWorkspace, 0); Log.i("cclw", "mTextureNameWorkspace[0]="+mTextureNameWorkspace[0]); } gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextureNameWorkspace[0]); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE); gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE); cclwservice.nativeAttachEGLImageKHR(); gl.glBindTexture(GL10.GL_TEXTURE_2D, 0); //------------------------ // RENDER SCENE // GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f); GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT); GLES20.glUseProgram(mProgram); GLES20.glVertexAttribPointer(mvPositionHandle, 3, GLES20.GL_FLOAT, false, 0, mTriangleVertices); GLES20.glVertexAttribPointer(mvNormalHandle, 3, GLES20.GL_FLOAT, false, 0, mTriangleNormal); if (mOrientation ==1) GLES20.glVertexAttribPointer(mvTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTriangleTexCoord); else if(mOrientation ==0) GLES20.glVertexAttribPointer(mvTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, mTriangleTexCoord2); GLES20.glEnableVertexAttribArray(mvPositionHandle); GLES20.glEnableVertexAttribArray(mvNormalHandle); GLES20.glEnableVertexAttribArray(mvTexCoordHandle); GLES20.glActiveTexture(GLES20.GL_TEXTURE0); GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureNameWorkspace[0]); GLES20.glUniform1i(mvSamplerHandle, 0); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6); //------------------------ // FPS Limitation // final long val =(aniInterval - interval) / NANOSECONDSPERMICROSECOND; if (val >0) { try{ Thread.sleep(val); }catch(final Exception e){} } this.mLastTickInNanoSeconds = nowInNanoSeconds; } catch(Exception e){ } } @Override public void onSurfaceChanged(GL10 arg0, int arg1, int arg2) { //arg1:width, arg2:height // TODO Auto-generated method stub Log.i("cclw", "WallpaperGLRenderer::onSurfaceChanged, arg1="+arg1+", arg2="+arg2); float myRatio =1184.0f/720.0f; mOrientation =0; if (arg2>=arg1) { myRatio =720.0f/1184.0f; mOrientation =1; } int targetHeight =arg2; int targetWidth =(int)((float)arg2*myRatio); if (targetWidth >arg1) { targetWidth =arg1; targetHeight =(int)((float)arg1/myRatio); } Log.i("cclw", "WallpaperGLRenderer::onSurfaceChanged, fit targetWidth="+targetWidth+", targetHeight="+targetHeight); GLES20.glViewport((int)((arg1-targetWidth)*0.5f), (int)((arg2-targetHeight)*0.5f), targetWidth, targetHeight); } @Override public void onSurfaceCreated(GL10 arg0, EGLConfig arg1) { // TODO Auto-generated method stub Log.i("cclw", "WallpaperGLRenderer::onSurfaceCreated"); mProgram =createProgram(mVertexShader, mFragmentShader); if (mProgram ==0) return; mvPositionHandle =GLES20.glGetAttribLocation(mProgram, "a_position"); if (mvPositionHandle ==-1) return; mvNormalHandle =GLES20.glGetAttribLocation(mProgram, "a_normal"); mvTexCoordHandle =GLES20.glGetAttribLocation(mProgram, "a_texCoord"); mvSamplerHandle =GLES20.glGetUniformLocation(mProgram, "s_texture"); GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1); } ...其中值得注意的是函數 onDrawFrame 被系統呼叫之後,會產生一個實體 WorkerRunnable ,讓負責 cocos2d-x 繪圖的執行緒去執行,目的是讓 cocos2d-x 更新 PBuffer 裡的資料,更新完成之後,onDrawFrame 才繼續執行貼圖的動作。
5. 設定 AndroidManifest.xml
為 WallpaperService 新增 service 標籤,如底下所示 :
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.macaronics.cclw" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="10"/> <uses-feature android:glEsVersion="0x00020000" /> <application android:label="@string/app_name" android:icon="@drawable/icon"> <activity android:name=".cclw" android:label="@string/app_name" android:screenOrientation="landscape" android:theme="@android:style/Theme.NoTitleBar.Fullscreen" android:process="com.macaronics.cclw.cclw" android:configChanges="orientation"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name=".cclwservice" android:label="@string/app_name" android:screenOrientation="portrait" android:permission="android.permission.BIND_WALLPAPER" android:process="com.macaronics.cclw.cclwservice"> <intent-filter> <action android:name="android.service.wallpaper.WallpaperService" /> </intent-filter> <meta-data android:name="android.service.wallpaper" android:resource="@xml/cclw_res" /> </service> </application> <supports-screens android:largeScreens="true" android:smallScreens="true" android:anyDensity="true" android:normalScreens="true"/> </manifest>其中的 xml/cclw_res.xml 表示 Live Wallpaper 選單列表時所顯示之資訊,需要額外手動建立,步驟是首先建立資料夾 xml 於 cclw/res,然後建立檔案 cclw_res.xml,並設定內容如下 :
<?xml version="1.0" encoding="utf-8"?> <wallpaper xmlns:android="http://schemas.android.com/apk/res/android" android:thumbnail="@drawable/icon" />
6. 設定 jni/Android.mk :
此時若執行 native_build.sh 會顯示一些錯誤,那是因為尚未設定連結 EGL Library,將 jni/Android.mk 修改如下即可正常編譯 :
LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := game_shared LOCAL_MODULE_FILENAME := libgame LOCAL_SRC_FILES := hellocpp/main.cpp \ ../../Classes/AppDelegate.cpp \ ../../Classes/HelloWorldScene.cpp LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../Classes LOCAL_LDLIBS := -lEGL LOCAL_WHOLE_STATIC_LIBRARIES := cocos2dx_static cocosdenshion_static cocos_extension_static include $(BUILD_SHARED_LIBRARY) $(call import-module,CocosDenshion/android) \ $(call import-module,cocos2dx) \ $(call import-module,extensions)
7. 執行結果 :
動態桌布選單多出了 cocos2d-x 建立的 cclw
將 cclw 設定成動態桌布
完整範例程式碼可以到 Git 下載: http://github.com/phardera/cocos2dx_android_livewallpaper