最近在android 上有個構思,就是如何使用opengl ES在後台繪製個3D圖片,然後把這個繪製好的圖片儲存成bitmap格式。。。想了好幾天,也嘗試了多種方法,但是都不行,一開始嘗試用GLSurfaceView的方式,但是這樣會導致我的Activity和渲染的東東發生聯絡,我想要要的結果是無論如何我的主Acivity都不能和我渲染的圖片發生任何關係(也就是說主Acitivity不能顯示任何我渲染的東西出來)。
首先來說的話,opengl es是來自於Opengl(精簡版),ES針對嵌入式靈巧的裝置(embided device),而opengl是針對PC這樣的超級怪物
,這也就不難理解它為什麼要被"瘦身"了,在opengl中有個雙緩衝的概念,也就是說前面顯示,後面畫圖,這樣可以達到無閃爍的境界。所以理論上來說我們應該也要效仿這種方式,將圖片繪製到後台緩衝中,達到目的。這裡先貼個opengl的方式:
glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE);// draw ...// draw endpixeldata = (GlutByte)malloc(width*height*bytes);glReadPixels(x, y, width, height, GL_BGR_EXT, GL_UNSIGNED_BYTE, pixeldata);
這個用到glut包,上面幾個是關鍵函數,如果大家想知道如何去畫bitmap的話,下面我也貼下畫bitmap的方式,無非就是把讀到的像素值pixeldata最後寫道bitmap檔案中,不過這裡要注意兩點,1個是bitmap的像素排列格式是BGR,所以當你試圖去
glReadPixels
擷取原始像素的時候請使用GL_BGR_EXT這個參數,其次bitmap是個結構體,在C,C++代碼處寫起來還是需要一定的格式的,不然產生的bitmap檔案有問題,具體的格式可以去查,我貼下我從網上找來的一段實現bitmap的代碼(經過驗證這個是可用的,寫這個的人還是比較靠譜的,贊一個),如下:
typedef long LONG;typedef unsigned char BYTE;typedef unsigned int DWORD;typedef unsigned short WORD;typedef struct { WORD bfType; DWORD bfSize; WORD bfReserved1; WORD bfReserved2; DWORD bfOffBits;} BMPFILEHEADER_T;typedef struct{ DWORD biSize; DWORD biWidth; DWORD biHeight; WORD biPlanes; WORD biBitCount; DWORD biCompression; DWORD biSizeImage; DWORD biXPelsPerMeter; DWORD biYPelsPerMeter; DWORD biClrUsed; DWORD biClrImportant;} BMPINFOHEADER_T;void init();void display();static GLubyte *PixelData;void Snapshot( BYTE * pData, int width, int height, char * filename ,DWORD size){ // 位元影像第一部分,檔案資訊 BMPFILEHEADER_T bfh={0}; bfh.bfType = (WORD)0x4d42; //bm bfh.bfSize = (DWORD)(size+54); bfh.bfReserved1 = 0; // reserved bfh.bfReserved2 = 0; // reserved bfh.bfOffBits = 54; // 位元影像第二部分,資料資訊 BMPINFOHEADER_T bih={0}; bih.biSize = 40; bih.biWidth = width; bih.biHeight = height; bih.biPlanes = 1; bih.biBitCount = 24; //24真彩色位元影像 bih.biCompression = 0; bih.biSizeImage = 0; bih.biXPelsPerMeter = 0; bih.biYPelsPerMeter = 0; bih.biClrUsed = 0; bih.biClrImportant = 0; FILE * fp = fopen(filename,"wb"); if( !fp ) return; fwrite( &bfh.bfType,1,2,fp ); fwrite( &bfh.bfSize,1,4,fp ); fwrite( &bfh.bfReserved1,1,2,fp ); fwrite( &bfh.bfReserved2,1,2,fp ); fwrite( &bfh.bfOffBits,1,4,fp ); fwrite( &bih,1,sizeof(BMPINFOHEADER_T),fp ); fwrite(pData,1,size,fp); fclose( fp );}
廢話不多說開始入正題:
先構思下,我們需要要建個很一般的Acitivity,然後在上面加個按鈕,當點擊按鈕的時候,開始在後台繪製圖片,然後將圖片的pixel讀出來,轉化成bitmap 儲存。
Idea有了,那麼開始幹活。
1 建立個Acivity,按照Android的工程步驟提示在eclipse裡面建立,這個不多說,我是App文盲,我都知道怎麼做。
2 在本地Avivity的onCreate裡面添加button和button監聽事件,並且在裡面處理初始化後台畫圖的一些操作。
btn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub //prepare init EGL environment BackDraw = new BackDraw(); //init backdraw Log.d("GlActivity:", "render in background"); } });
3 下面就是BackDraw的類的具體搭建了,我在這個類的建構函式中去初始化EGL環境,為後面的畫圖渲染創造條件。這個裡面需要說明的是,一般我們想要用opengl渲染圖片或者繪製圖片都是通過GLSurfaceView.render做的,目的是通過在onDrawFrame裡面調用gl函數在後台framebuffer中畫圖片,然後把圖片顯示到前台,這個一般是自動的。如果你不改任何東西,那麼只要你一畫好,你就會在前台看到你畫的東東。那麼如何將圖片畫在後台,而不自動顯示到前台呢,我仔細看了下GLSurfaceView的實現,這個是繼承於SurfaceView類,這個類在surfacecreate裡面有我們想要的參考代碼,這裡不作具體說明,我只想說靈魂就是1個函數
eglCreatePbufferSurface
這個函數是在記憶體中建立1個off-screen的framebuffer,我們繪製圖片可以在這個上面繪製,具體每個函數幹麼用的可以參考EGL官方網站的函數說明EGL HOME, 這裡不多描述了,總之在我們要畫圖之前,我們先要解決如何構建畫圖的環境,畫在哪的問題,在我們開始搭建環境之間,EGL需要有些屬性建立,如長寬,像素的byte大小,Surface類型等等,如下面
private int[] version = new int[2]; EGLConfig[] configs = new EGLConfig[1];int[] num_config = new int[1]; //EglchooseConfig used this config int[] configSpec ={EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT, EGL10.EGL_RED_SIZE, 8,EGL10.EGL_GREEN_SIZE, 8,EGL10.EGL_BLUE_SIZE, 8,EGL10.EGL_ALPHA_SIZE, 8,EGL10.EGL_NONE }; //eglCreatePbufferSurface used this config int attribListPbuffer[] = {EGL10.EGL_WIDTH, 480,EGL10.EGL_HEIGHT, 800,EGL10.EGL_NONE };
這裡面要說明的是
EGL10.EGL_SURFACE_TYPE, EGL10.EGL_PBUFFER_BIT,
如果你要建立的是PbufferSurface(後台顯示)類型,就要說明這個,如果要建立WindowSurface(前台顯示),需要EGL10.EGL_WINDOW_BIT類型,還有的是屬性數組最好不要亂加值,有些函數只能接受特定的值,如果你亂加,函數在執行的時候會失敗,比如
attribListPbuffer
它是eglCreatePbufferSurface函數在建立PbufferSuface時傳入的屬性,它只接受三個屬性,我一開始加了個其他的屬性,結果導致建立失敗。
還有個要提到的,就是attribListPbuffer[]數組,一定要把長寬的配置設定了
EGL10.EGL_WIDTH, 480,EGL10.EGL_HEIGHT, 800,
因為如果不設定,預設是0,如果你等會在這個surface上畫圖你會悲催的哭,因為你無論怎麼畫,畫到地球毀滅,最後在surface上的只有空氣。。。如果你要畫480*800的圖,那麼你就把surface的長寬也相應的設下。
好了屬性配置好了,下面就是構造EGL環境,做的事情可以概括為三件事
1 弄個PbufferSurface出來
2 弄個context出來,並把這個context綁定到surface中
3 通過context弄個GL對象,用這個GL對象繪製渲染圖片。
Here we go...
private void initEGL(){mEgl = (EGL10)EGLContext.getEGL();EGLDisplay mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);mEgl.eglInitialize(mEglDisplay, version);mEgl.eglChooseConfig(mEglDisplay, configSpec, configs, 1, num_config);EGLConfig mEglConfig = configs[0];EGLContext mEglContext = mEgl.eglCreateContext(mEglDisplay, mEglConfig,EGL10.EGL_NO_CONTEXT,null);if (mEglContext == EGL10.EGL_NO_CONTEXT){//mEgl.eglDestroySurface(mEglDisplay, mEglSurface);Log.d("ERROR:", "no CONTEXT");}//注意這個attribListPbuffer,屬性工作表EGLSurface mEglPBSurface = mEgl.eglCreatePbufferSurface(mEglDisplay, mEglConfig, attribListPbuffer);if (mEglPBSurface == EGL10.EGL_NO_SURFACE){//mEgl.eglDestroySurface(mEglDisplay, mEglPBSurface);int ec = mEgl.eglGetError();if (ec == EGL10.EGL_BAD_DISPLAY){Log.d("ERROR:", "EGL_BAD_DISPLAY");}if (ec == EGL10.EGL_BAD_DISPLAY){Log.d("ERROR:", "EGL_BAD_DISPLAY");}if (ec == EGL10.EGL_NOT_INITIALIZED){Log.d("ERROR:", "EGL_NOT_INITIALIZED");}if (ec == EGL10.EGL_BAD_CONFIG){Log.d("ERROR:", "EGL_BAD_CONFIG");}if (ec == EGL10.EGL_BAD_ATTRIBUTE){Log.d("ERROR:", "EGL_BAD_ATTRIBUTE");}if (ec == EGL10.EGL_BAD_ALLOC){Log.d("ERROR:", "EGL_BAD_ALLOC");}if (ec == EGL10.EGL_BAD_MATCH){Log.d("ERROR:", "EGL_BAD_MATCH");}}if (!mEgl.eglMakeCurrent(mEglDisplay, mEglPBSurface, mEglPBSurface,mEglContext))//這裡mEglPBSurface,意思是畫圖和讀圖都是從mEglPbSurface開始 {Log.d("ERROR:", "bind failed ECODE:"+mEgl.eglGetError()); } GL10 gl = (GL10) mEglContext.getGL(); }
以上具體的初始化流程我是參考http://blog.sina.com.cn/s/blog_413978670100bxsl.html. 小提示: 有的時候在這些建立過程中,會有失敗,我們可以通過調用
mEgl.eglGetError();
擷取上次egl執行函數的錯誤碼,通過比較錯誤碼,可以找到錯誤問題點,我的
eglCreatePbufferSurface
就是這麼做的,可以參考下。
好了現在該有的都有了,下面就是開始在你"紙"上畫圖了,具體怎麼畫我就不多說了吧,無非是什麼gl.clear()啊。。。。
Ok現在圖畫完了,那麼該儲存圖片了,這些圖片的像素值都被儲存在剛才我們建立的PB Framebuffer中,也就是那個存在於記憶體中的off-screen surface.下面就是讀圖了,和Opengl一樣,讀取當前framebuffer中的像素的方式都是gl.glReadPixels這個函數,實現如下,注意它的pixel參數是IntBuffer類型
IntBuffer PixelBuffer = IntBuffer.allocate(width*height);PixelBuffer.position(0);gl.glReadPixels(0, 0, width, height, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, PixelBuffer);
具體這個函數參數怎麼用不解釋自己看函數說明。
好了現在framebuffer中的像素已經被讀到pixelBuffer中了,這裡面還要說明的是因為我的是RGBA格式所以1個像素是4個位元組,如果是RGB那麼就是3位元組,分配記憶體的時候要注意。
原始像素有了,最後就是畫bitmap了,Android有個建立bitmap的方法,如下:
PixelBuffer.position(0);//這裡要把讀寫位置重設下int pix[] = new int[width*height];PixelBuffer.get(pix);//這是將intbuffer中的資料賦值到pix數組中Bitmap bmp = Bitmap.createBitmap(pix, width, height,Bitmap.Config.ARGB_8888);//pix是上面讀到的像素FileOutputStream fos = null;try {fos = new FileOutputStream("/sdcard/screen.png");//注意app的sdcard讀寫權限問題} catch (FileNotFoundException e) {// TODO Auto-generated catch block e.printStackTrace();} bmp.compress(CompressFormat.PNG, 100, fos);//壓縮成png,100%顯示效果try {fos.flush();} catch (IOException e) {// TODO Auto-generated catch block e.printStackTrace(); }}
好了,這下完是Ok了,打完收工。
PS:我也是剛剛研究這些東西,可能還有不全面的,只供參考,有什麼問題,希望大家熱心指認,謝謝。