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 surfacesetEGLWindowSurfaceFactory(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一个父布局。
//自己定义的GLSurfaceViewmGLView=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。
@Overridepublic 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 : requireprecision mediump float;varying vec2 textureCoordinate;uniform samplerExternalOES vTexture;void main() { gl_FragColor = texture2D( vTexture, textureCoordinate );}
渲染相机数据到指定窗体上
在之前的博客中,我们是直接将相机的数据“draw”到屏幕上了。在上面的处理中,我们在绘制之前调用了EasyGlUtils.bindFrameTexture(fFrame[0],fTexture[0])
,这种方法是让我们兴许的渲染,渲染到fTexture[0]这个纹理上。详细能够參考前面的博客。
所以。通过上面的方式接收数据。然后利用自己定义的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 QueuemFilterQueue; private List 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的内容,所以在这篇博客中没有赘述,如有问题,可在评论区留言,或给我发邮件,共同探讨。另外。分享一下我们公司的项目——,能够高速实现各种好玩的美颜、贴纸效果,欢迎Star和Fork。
源代码
所有的代码所有在一个项目中。托管在Github上——
欢迎转载。转载请保留文章出处。