底下敘述使用 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,找到 :
# 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 專案
本專案建立時選擇之 API Level 為 10,因此匯入完成之後檢查 AndroidManifest.xml 檔案,將 :
<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