实现Camera数据的预览,可以使用TextureView,作为View heirachy的一个硬件加速层,从SurfaceTexture中获取到的纹理数据更新到HardwareLayer中,完成显示;
也可以如这篇文章里讨论的,使用opengles完成绘制,相比较与前一种,使用opengles实现绘制,可以更方便的借助opengl的强大api添加特效处理,比如完成美颜的功能等.
OpenGL(open Graphics library)是开放的图形库,是用于渲染2D,3D矢量图形的跨语言,跨平台的应用程序编程接口PAI.
OpenGLEs(openGL for Embedded systems)是Opengl的api子集,针对手机,pad等嵌入式设备来设计.
OpenGL之所以能做到跨平台,主要得益于EGL的中介作用.
EGL ,是介于本地窗口系统和Rendering api之间的一层接口,主要负责图形环境管理,buffer绑定,渲染同步等.
一,在Android中,可以借助GLSurfaceView来方便的使用opengles, GLSurfaceView内部会启动一个线程(GLThread)来初始化EGL环境,并完成opengl的绘制,需要注意的一点是,opengl的所有操作,都需要在opengl的上下文环境中执行.这里demo就是使用的GLSurfaceView来搭建的EGL环境.
GLSUrfaceView具体的使用流程:
1,在布局文件中添加GLSUrfaceView控件,demo中时重写了这个组件
2,设置渲染回调的实现类GLSurfaceView.Renderer,剩下的主要工作都是在这个Renderer的实现类中完成的.
public class CameraView extends GLSurfaceView {
CameraRender cameraRender;
public CameraView(Context context, AttributeSet attrs) {
super(context, attrs);
setEGLContextClientVersion(2);
cameraRender = new CameraRender(this);
setRenderer(cameraRender);
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
}
}
GLSurfaceView.Renderer的实现类,需要实现三个主要的接口函数:
//窗口画布准备完成
void onSurfaceCreated(GL10 gl, EGLConfig config);
//画布发生改变(如:横竖切换)
void onSurfaceChanged(GL10 gl, int width, int height);
//绘制
void onDrawFrame(GL10 gl);
二,使用opengl把摄像头数据显示到GLSurfaceView中的流程:
1,摄像头数据的捕获.
2,把摄像头数据绑定到一个纹理.
3,通过着色器绘制与摄像头数据绑定的纹理.
三,摄像头数据的捕获.
借助CameraX使用摄像头,完成显示预览.CameraX的使用,可以参考官方文档https://developer.android.google.cn/training/camerax,或者上一篇博文https://blog.csdn.net/lin20044140410/article/details/104978447.这里不在赘述.
当摄像头数据有更新时,可以通过这个回调 onUpdated (Preview.OnPreviewOutputUpdateListener ),其中output.getSurfaceTexture返回就是最新的Camera图像数据SurfaceTexture.
@Override
public void onUpdated(Preview.PreviewOutput output) {
//拿到摄像头的图像,用opengles画出来,
mCameraTexture = output.getSurfaceTexture();
}
四,把摄像头数据绑定到一个纹理.
opengl纹理,可以看做是opengl使用的图像,通过纹理ID来表示.
1,创建一个opengl纹理,把摄像头的图像数据跟纹理关联,
2,纹理ID跟摄像头数据绑定,
3,后续的操作就是把纹理ID texture[0]交给Opengl的shader着色器去渲染,在渲染时可以添加特效处理.
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//创建一个opengl纹理,把摄像头的图像数据跟纹理关联,
texture = new int[1];//可以认为是一个可以在opengls中使用的图片的ID,
//纹理ID跟摄像头数据绑定,
mCameraTexture.attachToGLContext(texture[0]);
//当摄像头有新数据时,回调OnFrameAvailable方法,
mCameraTexture.setOnFrameAvailableListener(this);
}
在onSurfaceCreated中,为SurfaceTexture设置了onFrameAvailable的回调,这样在摄像头有新数据时,就会执行
public void onFrameAvailable(SurfaceTexture surfaceTexture),在这个回调方法,手动请求渲染新的数据帧requestRender,之所以需要这个回调,是因为在SurfaceView中设置的渲染模式是手动刷新:
setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);如果把渲染模式设置为持续刷新RENDERMODE_CONTINUOUSLY 就不需要再去手动请求render了.在持续刷新模式下,即使Camera预览隐藏了,依然会回调onDrawFrame方法,这是一个不必要的操作,浪费了cpu的性能.
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
//有新数据,请求渲染,在手动渲染模式下,执行requestRender请求渲染后,OnDrawFrame函数就会回调,
//在OnDrawFrame中的实现中,把创建的opengl纹理用opengl着色器渲染出来.
mCameraView.requestRender();
}
在onDrawFrame方法中,首先把mCameraTexture中的摄像头数据,更新到opengl绑定的纹理texture中.然后 使用着色器 绘制与摄像头绑定的纹理,实际的绘制只是把参数传给着色器,
@Override
public void onDrawFrame(GL10 gl) {
//把mCameraTexture中的摄像头数据,更新到opengl绑定的纹理texture中.
mCameraTexture.updateTexImage();
//使用查询得到的矩阵来变换纹理坐标,每一次有新的摄像头数据到来updateTexImage()时,
// 都可能导致这个矩阵发生变化,因此每次更新纹理都要重新查询,重新设置.
mCameraTexture.getTransformMatrix(matrix);
screenFilter.setTransformMatrix(matrix);
//绘制与摄像头绑定的纹理,实际的绘制只是把参数传给着色器,
screenFilter.onDraw(texture[0]);
}
五,着色器shader.
到这一步,就可以通过texture[0]这纹理id,在opengl的上下文中操作图像了.
着色器shader是运行在GPU上的小程序,这些小程序作为图形渲染管线的某个特定部分而运行,编写着色器的语言时GLSL.
opengl渲染的过程,可以用一张图形象的表示,
任何形状的物体,在opengl看来都是一个个的三角形组成,只要给出三角形的顶点坐标,opengl就可以根据顶点组合成图元,然后进行光栅化,插值出图形区域的片元(可以理解为像素),接下来对片元着色.整个过程最主要的操作就是确定形状和颜色,这就是顶点着色器,片元着色器的职责,顶点着色器每个顶点都会调用一次程序,片元着色器每个片元都会调用一次程序.
着色器的代码就是一段字符串,所以通常定义在res/raw中,也可以定义在asset中.
先看下顶点着色器的代码:
camera_vert.glsl
//定点着色器决定形状,
//从java传变量到着色器,就用attribute修饰,attribute只在定点着色器中使用,
attribute vec4 vPosition; //从java传变量到着色器,就用attribute修饰,attribute只在定点着色器中使用,
attribute vec2 vCoord;//
varying vec2 aCoord;//
uniform mat4 vMatrix;
void main() {
//gl_Position = vec4(vec3(0.0), 1.0);
gl_Position = vPosition;
//直接这样给坐标值,图像会有镜像,旋转角度问题
//aCoord = vCoord;
//用matrix校正下摄像头数据的纹理坐标,
aCoord = (vMatrix * vec4(vCoord, 1.0, 1.0)).xy;
}
其中vPosition是要由java传值的世界坐标,也即是要画的物体的形状的顶点坐标,,
vCoord是要由java传值的纹理坐标,vCoord混合matrix后赋值给了aCoord,修饰符varying表示aCoord这是个out输出变量,具体就是这个值会从顶点着色器传给片元着色器去使用,所以片元着色器中也有一个varying vec2 aCoord; (声明类型,名字都一样的变量.)
vMatrix是来校正摄像头数据的旋转问题的.
在确定物体形状时,需要根据opengl的世界坐标系传递顶点坐标,以确定几何形状,在绘制几何表面进行贴图时,就要根据纹理坐标贴合几何顶点.
在看片元着色器,顶点着色器确定形状,片元着色器进行贴图.
camera_frag.glsl
//摄像头数据比较特殊的地方,
#extension GL_OES_EGL_image_external : require
//片元着色器决定颜色,或者说是贴图片,
//片元着色器要配置数据精度
precision mediump float;
//aCoord 纹理坐标,
varying vec2 aCoord;// 从定点着色器到片元着色器传递的变量,
//可以当做一张图片,实际是一个采样器,其实就是SurfaceTexture,相当于把摄像头的数据传过来赋值给他,
//它由java程序传过来,要加uniform,
//uniform,如果需要给片元着色器中的变量赋值,需要声明为uniform.
uniform samplerExternalOES vTexture;
void main() {
//gl_Position = vec4(vec3(0.0), 1.0);
//在vTexture这张图片,采样aCoord这个点的像素的RGBA值,给内置变量gl_FragColor.这就完成了对aCoord这个像素点的上色.
//正常图像,
gl_FragColor = texture2D(vTexture, aCoord);
vec4 rgba = texture2D(vTexture, aCoord);
//灰度图
// float gray = rgba.r * 0.30 + rgba.g * 0.59 + rgba.b * 0.11;
// gl_FragColor = vec4(gray, gray, gray, rgba.a);
//X光效果,
//gl_FragColor = vec4(1.0 -rgba.r, 1.0 - rgba.g, 1.0 -rgba.b, rgba.a);
}
其中的变量,vTexture是java层传递的图层的ID,
texture2D内置函数的作用: 在vTexture这张图片中,采样aCoord这个点的像素的RGBA值,赋值给内置变量gl_FragColor.这就完成了对aCoord这个像素点的上色.
最后,看下java代码,是怎么使用的着色器程序.
1,数据准备,顶点buffer,纹理buffer,
//顶点buffer
FloatBuffer vertexBuffer;
//纹理buffer
FloatBuffer textureBuffer;
vertexBuffer = ByteBuffer.allocateDirect(4*4*2).order(ByteOrder.nativeOrder()).asFloatBuffer();
float[] vertex= {
-1.0f, 1.0f,
-1.0f, -1.0f,
1.0f, -1.0f,
1.0f, 1.0f
};
vertexBuffer.clear();
vertexBuffer.put(vertex);
textureBuffer = ByteBuffer.allocateDirect(4*4*2).order(ByteOrder.nativeOrder()).asFloatBuffer();
float[] texture= {
0.0f, 1.0f,
0.0f, 0.0f,
1.0f, 0.0f,
1.0f, 1.0f
};
textureBuffer.clear();
textureBuffer.put(texture);
需要注意的是,这里的坐标对应的是上图中的世界坐标,纹理坐标,并且是有一定的顺序的,这个顺序分为两种,不同的坐标顺序,在绘图时需要制定不同的绘图方式GLES20.GL_TRIANGLE_FAN,GLES20.GL_TRIANGLE_STRIP
//GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);
//顶点坐标的顺序,左上,左下,右下,右上,
//GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
//顶点坐标的顺序,左下,右下,左上,右上,
GLES20.GL_TRIANGLE_FAN ,假设有N个顶点,第n个三角形的顶点是(1, n+1, n+2),总共是n-2个三角形,绘图模式:
GLES20.GL_TRIANGLE_STRIP ,假设有N个顶点,也有n-2个三角形,
当N为奇数:第n个三角形的顶点是(n, n+1, n+2),
当N是偶数,第n个三角形的顶点是(n+1,n , n+2),
绘图模式:
执行到这里,数据都准备好了,剩下就是在java代码中,通过opengl接口给着色器传参数就行了,传参数的操作在onDraw中完成,onDraw的调用是在onDrawFrame中,这样camera有一帧新的数据就会动态调用一次完成给着色器的传参,绘制效果的处理有着色器来完成.
//加载着色器代码
String vertexShader = OpenGLUtils.readRawGlslFile(context, R.raw.camera_vert);
String fragShader = OpenGLUtils.readRawGlslFile(context, R.raw.camera_frag);
//准备着色器程序
glesProgram = OpenGLUtils.loadGlslProgram(vertexShader, fragShader);
//获取程序中的变量,就是顶点着色器,片元着色器中的变量,也是索引
vPosition = GLES20.glGetAttribLocation(glesProgram, "vPosition");
vCoord = GLES20.glGetAttribLocation(glesProgram, "vCoord");
vTexture = GLES20.glGetUniformLocation(glesProgram, "vTexture");
vMatrix = GLES20.glGetUniformLocation(glesProgram, "vMatrix");
完成绘制,最后调用的函数:
//开始绘制,第一个参数时绘制的模式,GL_TRIANGLE_STRIP
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);
跟前面顶点坐标,纹理坐标的顺序要对应好.
public void onDraw(int textureId) {
//设置绘制区域
GLES20.glViewport(0, 0, mWidth, mHeight);
GLES20.glUseProgram(glesProgram);
vertexBuffer.position(0);
//为顶点,传入参数,第二个参数表示每个坐标是2个点,
GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
//从cpu传数据到gpu,默认情况下,着色器无法读到这个数据,需要启用一个这个数据才能读取到
GLES20.glEnableVertexAttribArray(vPosition);
textureBuffer.position(0);
GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
GLES20.glEnableVertexAttribArray(vCoord);
//为片元着色器变量传值,
//先要激活一个用来显示纹理的纹理单元,相当于激活一层用来显示图片的画框,跟view的叠加类似,画框可以有多个,
// 我们这里只需要一层,
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
//把摄像头的纹理,绑定到这激活的图层中,
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureId);
//给片元着色器中vTexture赋值,第二个参数0,可以认为是图层ID,因为前面激活纹理时,指定的时TEXTURE0,所以这里是0
GLES20.glUniform1i(vTexture, 0);
//传入校正图像角度的矩阵
GLES20.glUniformMatrix4fv(vMatrix, 1, false, mMatrix, 0);
//开始绘制,第一个参数时绘制的模式,GL_TRIANGLE_STRIP
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, 4);
}
使用opengl显示摄像头图像的整体流程:
1,核心工作时编写顶点着色器,片元着色器,
顶点着色器:传顶点数据给顶点着色器,这样opengl就知道要画的物体的几何形状.
片元着色器:为确定好的几何形状贴图,上色.
2,除去着色器的程序代码,java层的工作可以看作是一个客户端.
从java层调用opengl的接口,就相当于向服务器发出请求,可以认为顶点着色器,片元着色器就是运行在服务器上的,只是这个服务器也在手机中.
然后,客户端传递Attribute参数给顶点着色器,传递Uniform参数给顶点和片元着色器,
接着,数据经过顶点着色器确定好形状,经过图元装配,把顶点连成三角形,把图元进行光栅化得到图元中每个像素点的坐标,把每个像素点的坐标传给片元着色器.
最后,片元着色器,采集摄像头数据中每个像素点对应的像素值(rgba),赋值给内置变量,gl_FragColor,这就完成了像素点上色.
附录:着色器程序的加载: 准备顶点着色器,片元着色器,把顶点着色器,片元着色器组合成一个着色器程序.
public static int loadGlslProgram(String vertexSrc, String fragSrc) {
//创建顶点着色器
int vertexShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
//加载着色器程序
GLES20.glShaderSource(vertexShader, vertexSrc);
//编译配置
GLES20.glCompileShader(vertexShader);
//检查配置是否成功,检查着色器程序编写错误,
int[] status = new int[1];
GLES20.glGetShaderiv(vertexShader, GLES20.GL_COMPILE_STATUS, status, 0);
if (status[0] != GLES20.GL_TRUE) {
throw new IllegalStateException("load vertex shader :" +
GLES20.glGetShaderInfoLog(vertexShader));
}
//创建片元着色器,
int fragShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
//加载着色器代码,
GLES20.glShaderSource(fragShader, fragSrc);
//编译配置,
GLES20.glCompileShader(fragShader);
//查看配置是否成功,检查着色器程序编写错误,
GLES20.glGetShaderiv(fragShader, GLES20.GL_COMPILE_STATUS, status,0);
if (status[0] != GLES20.GL_TRUE) {
throw new IllegalStateException("load fragment shader :" +
GLES20.glGetShaderInfoLog(vertexShader));
}
//创建着色器程序
int program = GLES20.glCreateProgram();
//绑定顶点和片元
GLES20.glAttachShader(program, vertexShader);
GLES20.glAttachShader(program, fragShader);
//链接着色器程序
GLES20.glLinkProgram(program);
//检查链接状态
GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, status, 0);
if (status[0] != GLES20.GL_TRUE) {
throw new IllegalStateException("link program :" +
GLES20.glGetShaderInfoLog(vertexShader));
}
GLES20.glDeleteShader(vertexShader);
GLES20.glDeleteShader(fragShader);
return program;
}