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的預覽怎么做呢?實現起來可能也非常easy,假設預覽之前,要做美顏、磨皮或者加水印等等處理呢?實現后又怎樣保證使用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的。
首先。我們使用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 void clearAll(){
mFilterQueue.clear();
mFilters.clear();
size=0;
}
public void draw(){
updateFilter();
textureIndex=0;
if(size>0){
for (AFilter filter:mFilters){
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, fTexture[textureIndex%2], 0);
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
GLES20.GL_RENDERBUFFER, fRender[0]);
GLES20.glViewport(0,0,width,height);
if(textureIndex==0){
filter.setTextureId(getTextureId());
}else{
filter.setTextureId(fTexture[(textureIndex-1)%2]);
}
filter.draw();
unBindFrame();
textureIndex++;
}
}
}
private void updateFilter(){
AFilter f;
while ((f=mFilterQueue.poll())!=null){
f.create();
f.setSize(width,height);
mFilters.add(f);
size++;
}
}
@Override
public int getOutputTexture(){
return size==0?getTextureId():fTexture[(textureIndex-1)%2];
}
@Override
protected void onCreate() {
}
@Override
protected void onSizeChanged(int width, int height) {
this.width=width;
this.height=height;
updateFilter();
createFrameBuffer();
}
//創建離屏buffer
private int fTextureSize = 2;
private int[] fFrame = new int[1];
private int[] fRender = new int[1];
private int[] fTexture = new int[fTextureSize];
private int textureIndex=0;
//創建FrameBuffer
private boolean createFrameBuffer() {
GLES20.glGenFramebuffers(1, fFrame, 0);
GLES20.glGenRenderbuffers(1, fRender, 0);
genTextures();
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fFrame[0]);
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, fRender[0]);
GLES20.glRenderbufferStorage(GLES20.GL_RENDERBUFFER, GLES20.GL_DEPTH_COMPONENT16, width,
height);
GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0,
GLES20.GL_TEXTURE_2D, fTexture[0], 0);
GLES20.glFramebufferRenderbuffer(GLES20.GL_FRAMEBUFFER, GLES20.GL_DEPTH_ATTACHMENT,
GLES20.GL_RENDERBUFFER, fRender[0]);
// int status = GLES20.glCheckFramebufferStatus(GLES20.GL_FRAMEBUFFER);
// if(status==GLES20.GL_FRAMEBUFFER_COMPLETE){
// return true;
// }
unBindFrame();
return false;
}
//生成Textures
private void genTextures() {
GLES20.glGenTextures(fTextureSize, fTexture, 0);
for (int i = 0; i < fTextureSize; i++) {
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, fTexture[i]);
GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height,
0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, null);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
}
}
//取消綁定Texture
private void unBindFrame() {
GLES20.glBindRenderbuffer(GLES20.GL_RENDERBUFFER, 0);
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
}
private void deleteFrameBuffer() {
GLES20.glDeleteRenderbuffers(1, fRender, 0);
GLES20.glDeleteFramebuffers(1, fFrame, 0);
GLES20.glDeleteTextures(1, fTexture, 0);
}
}
將這個FilterGroup添加到已有的流程中。僅僅須要將保持的相機數據的Texture作為FilterGroup的輸入,然后將FilterGroup的輸出作為渲染到指定窗體的Filter的輸入就可以:
@Override
public void onDrawFrame(GL10 gl) {
if(isParamSet.get()){
mCameraFilter.draw();
mGroupFilter.setTextureId(mCameraFilter.getOutputTexture());
mGroupFilter.draw();
GLES20.glViewport(0,0,mWindowSize.x,mWindowSize.y);
mShowFilter.setMatrix(SM);
mShowFilter.setTextureId(mGroupFilter.getOutputTexture());
mShowFilter.draw();
if(mRenderer!=null){
mRenderer.onDrawFrame(gl);
}
callbackIfNeeded();
}
}
然后將須要添加的其它的Filter。依次添加到GroupFilter中就可以:
WaterMarkFilter filter=new WaterMarkFilter(getResources());
filter.setWaterMark(BitmapFactory.decodeResource(getResources(),R.mipmap.logo));
filter.setPosition(300,50,300,150);
mController.addFilter(filter);
//mController.addFilter(new GrayFilter(getResources()));
mController.setFrameCallback(720, 1280, Camera2Activity.this);
其它
假設之前沒有接觸過OpenGL,這篇博客看下來可能也是雲里霧里。主要是由於篇幅有限,加上之前的博客分享的也是從零開始學習OpenGLES的內容,所以在這篇博客中沒有贅述,如有問題,可在評論區留言,或給我發郵件,共同探討。另外。分享一下我們公司的項目——AiyaEffectSDK,能夠高速實現各種好玩的美顏、貼紙效果,歡迎Star和Fork。
源代碼
所有的代碼所有在一個項目中。托管在Github上——Android OpenGLES 2.0系列博客的Demo
歡迎轉載。轉載請保留文章出處。
湖廣午王的博客[http://blog.csdn.net/junzia/article/details/61207844]