http://www.mamicode.com/info-detail-1723122.html
Android Lollipop 增加了Camera2 API,並將原來的Camera API標記為廢棄了。相對原來的Camera API來說,Camera2是重新定義的相機 API,也重構了相機 API 的架構。初看之下,可能會感覺Camera2使用起來比Camera要復雜,然而使用過后,你也許就會喜歡上使用Camera2了。無論是Camera還是Camera2,當相機遇到OpenGL就比較好玩了。
問題及思路
Camera的預覽比較常見的是使用SurfaceHolder來預覽,Camera2比較常見的是使用TextureView來預覽。如果利用SurfaceHolder作為Camera2的預覽,利用TextureView作為Camera的預覽怎么做呢?實現起來可能也很簡單,如果預覽之前,要做美顏、磨皮或者加水印等等處理呢?實現后又如何保證使用Camera還是Camera2 API,使用SurfaceHolder還是TextureView預覽,或者是直接編碼不預覽都可以迅速的更改呢?
本篇博客示例將數據、處理過程、預覽界面分離開,使得無論使用Camera還是Camera2,只需關注相機自身。無論最終期望顯示在哪里,都只需提供一個顯示的載體。詳細點說來就是:
- 以一個SurfaceTexture作為接收相機預覽數據的載體,這個SurfaceTexture就是處理器的輸入。
- SurfaceView、TextureView或者是Surface,提供SurfaceTexture或者Surface給處理器作為輸出,來接收處理結果。
- 重點就是處理器了。處理器利用GLSurfaceView提供的GL環境,以相機數據作為輸入,進行處理,處理的結果渲染到視圖提供的輸出點上,而不是GLSurfaceView內部的Surface上。當然,也可以不用GLSurfaceView,自己利用EGL來實現GL環境,問題也不大。具體實現就參照GLSurfaceView的源碼來了。
處理效果
既然是用OpenGL來處理,索性利用OpenGL在圖像上加兩個其他圖片,類似水印、貼紙的效果。隨便兩幅圖貼上去,也不管好看不好看了,重點是功能。依次為先貼紙然后灰色濾鏡,先灰色濾鏡然后貼紙,只有貼紙。 


具體實現
根據上述思路來,主要涉及到以下問題:
- 使用GLSurfaceView創建GL環境,但是要讓這個環境為離屏渲染服務,而不是直接渲染到GLSurfaceView的Surface上。在這其中還涉及到其他的一些問題,具體的問題,在下面再說。
- OpenGL 的使用。相關文章
- 務必使相機、處理過程、顯示視圖分離。以便能夠自由的替換數據源、顯示視圖,只需要關注處理過程。
GLSurfaceView的利用
通常我們在Android中使用OpenGL環境,只需要在GLSurfaceView的Renderer接口中,調用GL函數就好了。這是因為GLSurfaceView在內部幫我們創建了GL環境,如果我們要拋開GLSurfaceView的話,只需要根據GLSurfaceView創建GL環境的過程在,做相同實現就可了,也就是EGL的使用。也就是說,OpenGL是離不開EGL的。EGL的使用步驟參考。
首先,我們使用GLSurfaceView,是希望利用它的GL環境,而不是它的視圖,所以,我們需要改變它的渲染位置為我們期望的位置:
//這句是必要的,避免GLSurfaceView自帶的Surface影響渲染 getHolder().addCallback(null); //指定外部傳入的surface為渲染的window surface setEGLWindowSurfaceFactory(new GLSurfaceView.EGLWindowSurfaceFactory() { @Override public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display, EGLConfig config, Object window) { //這里的surface由外部傳入,可以為Surface、SurfaceTexture或者SurfaceHolder return egl.eglCreateWindowSurface(display,config,surface,null); } @Override public void destroySurface(EGL10 egl, EGLDisplay display, EGLSurface surface) { egl.eglDestroySurface(display, surface); } });
另外,GLSurfaceView的GL環境是受View的狀態影響的,比如View的可見與否,創建和銷毀,等等。我們需要盡可能的讓GL環境變得可控。因此,GLSurfaceView有兩個方法一頂要暴露出來:
public void attachedToWindow(){ super.onAttachedToWindow(); } public void detachedFromWindow(){ super.onDetachedFromWindow(); }
這里就又有問題了,因為GLSurfaceView的onAttachedToWindow和onDetachedFromWindow是需要保證它有parent的。所以,在這里必須給GLSurfaceView一個父布局。
//自定義的GLSurfaceView mGLView=new GLView(mContext); //避免GLView的attachToWindow和detachFromWindow崩潰 new ViewGroup(mContext) { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { } }.addView(mGLView);
另外,GLSurfaceView的其他設置:
setEGLContextClientVersion(2); setRenderer(TextureController.this); setRenderMode(RENDERMODE_WHEN_DIRTY); setPreserveEGLContextOnPause(true);
這樣,我們就可以愉快的使用GLSurfaceView來提供GL環境,給指定的Surface或者SurfaceTexture渲染圖像了。
數據的接收
我們針對的是相機的處理,相機的圖像數據和視頻的圖像數據,在Android中都可以直接利用SurfaceTexture來接收,所以我們可以提供一個SurfaceTexture給相機,然后將SurfaceTexture的數據拿出來,調整好方向,作為原始數據。具體處理和相機普通的預覽類似,不同的是,我們是不希望直接顯示到屏幕上的,而且在后續我們還會對這個圖像做其他處理。所以我們時將相機的當前幀數據渲染到一個2d的texture上,作為后續處理過程的輸入。所以在渲染時,需要綁定FrameBuffer。
@Override public void draw() { boolean a=GLES20.glIsEnabled(GLES20.GL_DEPTH_TEST); if(a){ GLES20.glDisable(GLES20.GL_DEPTH_TEST); } if(mSurfaceTexture!=null){ mSurfaceTexture.updateTexImage(); mSurfaceTexture.getTransformMatrix(mCoordOM); mFilter.setCoordMatrix(mCoordOM); } EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]); GLES20.glViewport(0,0,width,height); mFilter.setTextureId(mCameraTexture[0]); mFilter.draw(); Log.e("wuwang","textureFilter draw"); EasyGlUtils.unBindFrameBuffer(); if(a){ GLES20.glEnable(GLES20.GL_DEPTH_TEST); } }
上面所使用的mFilter就是用來渲染相機數據的Filter,該Filter所起的作用就是將相機數據的方向調整正確。然后通過綁定FrameBuffer並制定接受渲染的Texture,就可以將相機數據以一個正確的方向渲染到這個指定的Texture上了。
mFilter的頂點着色器為:
attribute vec4 vPosition; attribute vec2 vCoord; uniform mat4 vMatrix; uniform mat4 vCoordMatrix; varying vec2 textureCoordinate; void main(){ gl_Position = vMatrix*vPosition; textureCoordinate = (vCoordMatrix*vec4(vCoord,0,1)).xy; }
其片元着色器為:
#extension GL_OES_EGL_image_external : require precision mediump float; varying vec2 textureCoordinate; uniform samplerExternalOES vTexture; void main() { gl_FragColor = texture2D( vTexture, textureCoordinate ); }
渲染相機數據到指定窗口上
在之前利用OpenGLES預覽Camera的博客中,我們是直接將相機的數據“draw”到屏幕上了。在上面的處理中,我們在繪制之前調用了EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0]),這個方法是讓我們后續的渲染,渲染到fTexture[0]這個紋理上。具體可以參考前面的博客FBO離屏渲染。
所以,通過上面的方式接收數據,然后利用自定義的GLSurfaceView指定渲染窗口並執行渲染后,我們依舊無法看到相機的預覽效果。為了將相機的數據渲染到屏幕上,我們需要將fTexture[0]的內容再渲染到制定的窗口上。這個渲染比之前的接收相機數據,渲染到fTexture[0]上更為簡單:
AFilter filter=new NoFilter(getResource()); ... void onDrawFrame(GL10 gl){ GLES20.glViewPort(0,0,width,height) filter.setMatrix(matrix); filter.setTexture(fTexture[0]); filter.draw(); } ...
NoFilter的頂點着色器為:
attribute vec4 vPosition; attribute vec2 vCoord; uniform mat4 vMatrix; varying vec2 textureCoordinate; void main(){ gl_Position = vMatrix*vPosition; textureCoordinate = vCoord; }
片元着色器為:
precision mediump float; varying vec2 textureCoordinate; uniform sampler2D vTexture; void main() { gl_FragColor = texture2D( vTexture, textureCoordinate ); }
沒錯,就是顯示超簡單的渲染一張圖片的着色器,看過前面的博客的應該見過這段着色器代碼。
增加濾鏡、貼紙效果
如果僅僅是預覽相機,我們這種做法簡直就是多次一句,直接指定渲染窗口,渲染出來就萬事大吉了。但是僅僅是這樣的話,就太沒意思了。而現在要做的就是相機有意思的起點了。很多有意思的相機應用,都可以通過這樣的方式去實現,比如我們常見美妝、美顏、色彩處理(濾鏡),甚至瘦臉、大眼,或者其他的讓臉變胖的,以及一些給相機中的人帶眼鏡、帽子、發箍(這些一般需要做人臉識別特征點定位)等等等等。
通過上面的接收數據,和渲染相機數據到指定的窗口上,我們是已經可以看到渲染的結果了的。
然后我們要在渲染到指定窗口前,增加其他的Filter,為了保證易用性,我們增加一個GroupFilter,讓其他的Filter,直接加入到GroupFilter中來完成處理。
public class GroupFilter extends AFilter{ private Queue<AFilter> mFilterQueue; private List<AFilter> mFilters; private int width=0, height=0; private int size=0; public GroupFilter(Resources res) { super(res); mFilters=new ArrayList<>(); mFilterQueue=new ConcurrentLinkedQueue<>(); } @Override protected void initBuffer() { } public void addFilter(final AFilter filter){ //繪制到frameBuffer上和繪制到屏幕上的紋理坐標是不一樣的 //Android屏幕相對GL世界的紋理Y軸翻轉 MatrixUtils.flip(filter.getMatrix(),false,true); mFilterQueue.add(filter); } public boolean removeFilter(AFilter filter){ boolean b=mFilters.remove(filter); if(b){ size--; } return b; } public AFilter removeFilter(int index){ AFilter f=mFilters.remove(index); if(f!=null){ size--; } return f; } public 