1. 程式人生 > >Android opengl ES 實現後臺繪圖並儲存成bitmap

Android opengl ES 實現後臺繪圖並儲存成bitmap

最近在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 end
pixeldata = (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:我也是剛剛研究這些東西,可能還有不全面的,只供參考,有什麼問題,希望大家熱心指認,謝謝。