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,所以當你試圖去 獲取原始畫素的時候請使用GL_BGR_EXT這個引數,其次bitmap是個結構體,在C,C++程式碼處寫起來還是需要一定的格式的,不然生成的bitmap檔案有問題,具體的格式可以去查,我貼下我從網上找來的一段實現bitmap的程式碼(經過驗證這個是可用的,寫這個的人還是比較靠譜的,贊一個),如下:glReadPixels
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:我也是剛剛研究這些東西,可能還有不全面的,只供參考,有什麼問題,希望大家熱心指認,謝謝。