上一篇实现了rc版本的CameraX的一些基本能力
但是那只是基本,因为很多时候我们想要在拿到预览的YUV
之前,就做一些事情,那只能通过拿到SurfaceTexture
交给OpenGL
去渲染,这篇文章就是主要说这个的,相关文章或者视频再网上挺少的,所以花费了比较多时间,后面细说。
前置知识
OpenGL(Open Graphics Library)是开放图形库。是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口API。简单来说就是一套画图的API。
OpenGL ES (OpenGL for Embedded Systems) 是 OpenGL 三维图形 API 的子集,针对手机、PDA和游戏主机等嵌入式设备而设计。
EGL OpenGL ES只是图形API,不负责管理(显示)窗口,窗口的管理交由各个设备自己来完成。OpenGL ES调用用于渲染纹理多边形,而 EGL 调用用于将渲染放到屏幕上。
Android 使用 EGL 库创建OpenGL ES上下文并为 OpenGL ES 渲染提供窗口系统。调用任何 OpenGL函数前,必须已经创建了 OpenGL 上下文。
上下文 在渲染过程中需要将顶点信息(形状)、纹理信息(图像)等渲染状态信息存储起来,而存储这些信息的数据结构就可以看作 OpenGL 的上下文。
GLSurfaceView 继承自SurfaceView。内部启动一个子线程(GL线程GLSurfaceView的静态内部类)来初始化ELG环境,并通过Render去完成绘制。使用GLSurfaceView,我们只能在这个EGL线程调用OpenGL函数
OpenGL可以做的事情很多,但是我现在只关心这个
//放到xml里面用来显示预览
public class CameraView extends GLSurfaceView {
private CameraRender renderer;
public CameraView(Context context) {
this(context, null);
}
public CameraView(Context context, AttributeSet attrs) {
super(context, attrs);
//使用OpenGL ES 2.0 context.
setEGLContextClientVersion(2);
renderer = new CameraRender(this);
setRenderer(renderer);
//注意必须在setRenderer 后面。
//这里我使用的是RENDERMODE_CONTINUOUSLY自动刷新,
//还有一种模式RENDERMODE_WHEN_DIRTY,如果使用这种的话
//需要每一帧自己去要求渲染cameraView.requestRender();
setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
super.surfaceDestroyed(holder);
renderer.onSurfaceDestroyed();
}
}
在OpenGL ES环境准备完成之后,会在GLThread
中回调 onSurfaceCreated
方法。在这个方法中我们需要准备我们需要绘制的图像。
让摄像头数据绑定到我们创建的纹理 textures[0]
之后,后续我们会将 textures[0] 交给OpenGL ES的Sharder着色器
去进行渲染,并且在渲染时也能去实现各种图像效果。
public class CameraRender implements GLSurfaceView.Renderer, Preview.OnPreviewOutputUpdateListener, SurfaceTexture.OnFrameAvailableListener {
private CameraView cameraView;
private CameraHelper cameraHelper;
// 摄像头的图像 用OpenGL ES 画出来
private SurfaceTexture mCameraTexure;
private int[] textures;
private ScreenFilter screenFilter;
float[] mtx = new float[16];
public CameraRender(CameraView cameraView) {
this.cameraView = cameraView;
LifecycleOwner lifecycleOwner = (LifecycleOwner) cameraView.getContext();
cameraHelper = new CameraHelper(lifecycleOwner, this);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
//创建OpenGL 纹理 ,把摄像头的数据与这个纹理关联
textures = new int[1]; //当做能在opengl用的一个图片的ID
mCameraTexure.attachToGLContext(textures[0]);
// 当摄像头数据有更新回调 onFrameAvailable
mCameraTexure.setOnFrameAvailableListener(this);
screenFilter = new ScreenFilter(cameraView.getContext());
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
screenFilter.setSize(width,height);
}
@Override
public void onDrawFrame(GL10 gl) {
//todo 更新纹理
mCameraTexure.updateTexImage();
mCameraTexure.getTransformMatrix(mtx);
screenFilter.setTransformMatrix(mtx);
screenFilter.onDraw(textures[0]);
}
public void onSurfaceDestroyed() {
}
/**
* 更新
* @param output 拿到里面的SurfaceTexture交给openGL
*/
@Override
public void onUpdated(Preview.PreviewOutput output) {
mCameraTexure = output.getSurfaceTexture();
}
@Override
public void onFrameAvailable(SurfaceTexture surfaceTexture) {
// 可以拿到每一帧的SurfaceTexture
//setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);则不必手动渲染
// cameraView.requestRender();
}
}
摄像头预览数据捕获
private Preview getPreView() {
// 分辨率并不是最终的分辨率,CameraX会自动根据设备的支持情况,结合你的参数,设置一个最为接近的分辨率
PreviewConfig previewConfig = new PreviewConfig.Builder()
.setLensFacing(currentFacing) //前置或者后置摄像头
.build();
Preview preview = new Preview(previewConfig);
//预览数据添加回调,老版本1.0.0alpha05版本有,而且思路很清晰,
// 我在用的1.0.0rc03已经没有了,虽然也能获取但是就贼费事了
preview.setOnPreviewOutputUpdateListener(listener);
return preview;
}
着色器与GLSL
我们现在已经可以通过 textures[0]
在OpenGL的世界中操作图像了,我们先来完成最基本的显示绘制。OpenGL其实就是在配置着色器,然后执行着色器即可。着色器(Shader)
是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。着色器是一种非常独立的程序,这些程序之间不能相互通信,它们之间唯一的沟通只有通过输入和输出。而编
写OpenGL着色器程序的语言为GLSL
。
上面这张图很形象的描述了OpenGL的渲染流程。任何形状的物体都可以看为由无数个或大或小的三角形组成,我们要做的就是给出这些三角形的顶点,然后OpenGL会根据你给的顶点组合成图元(比如图中的三角形)。然后进行光栅化,插值出那个图形区域的片元(可以理解为像素)。接下来就是对这些片元上色。
顶点着色器
//顶点坐标
attribute vec4 vPosition;
//传给片元进行采样的纹理坐标
attribute vec2 vCoord;
//易变变量 和片元写的一模一样 会传给片元
varying vec2 aCoord;
void main(){
//内置变量 顶点给它就行
gl_Position = vPosition;
aCoord = vCoord;
}
这段程序就是一个顶点着色器的代码(GPUIMage中就有很多,一般是字符串的形式存在)。与Java或者C一样,程序入口就是main
方法。在 main 方法中只要我们把要画的物体形状的点坐标给到 gl_position
即可。 vPosition
就是我们在CPU中确定的物体形状坐标点。现在我们需要绘制的是摄像头采集图像,也就是矩形,那就需要传递4个顶点坐标数据到着色器中。
//顶点坐标
attribute vec4 vPosition;
与我们熟悉的Java语法类似, vPosition
是变量名, vec4
是类型,而应用程序传递到顶点着色器中的变量值需要使用 attribute
修饰。可以看成是in
顶点着色器输入变量的修饰。
vec4 其实就是vector向量,4表示这个向量包含4个数据。需要注意的是vec4只能存储float数据,
如果需要存储整型则需要使用ivec4。
main 方法的第二行代码:
aCoord = vCoord;
aCoord
定义为存储2个float的vec2类型,被varying
修饰。如果说 attribute 是 in 输入变量的修饰,
那么 varying 就是 out 输出变量。我们会在片元着色器使用这个变量。
这个顶点着色器中没有任何逻辑,只进行了赋值的操作,那么作为 attribute 输入的值 vPosition 与 vCoord 就需要我们在Android中传递进来。这两个值分别为顶点坐标与纹理坐标。
我们需要根据OpenGL世界坐标系传递顶点坐标点来确定绘制的几何形状,需要在绘制的几何表面进行
贴图,就需要按照纹理坐标贴合几何顶点。顶点着色器执行4次, vPostion 接收的点坐标与 vCoord 分
别为:
-1.0,-1.0 与 0,0
1.0,-1.0 与 0,1.0
-1.0, 1.0 与 0,1.0
1.0, 1.0 与 1.0,1.0
片元着色器
顶点着色器确定形状,那片元着色器就是进行贴图。
#extension GL_OES_EGL_image_external : require
precision mediump float; //数据精度
varying vec2 aCoord;
uniform samplerExternalOES vTexture;
void main(){
gl_FragColor = texture2D(vTexture,aCoord);
}
片元着色器中,使用内置函数 texture2D
采集对应坐标点的像素,赋值给 gl_FragColor
即可。
#extension GL_OES_EGL_image_external : require :Android摄像头只能用samplerExternalOES 类型的纹理去接收摄像头的画面,而使用samplerExternalOES需要开GL_OES_EGL_image_external功能。
uniform 变量是Android中需要传递给着色器的变量,不能被着色器修改。可以用于修饰共享变量(在顶点和片元中声明方式完全一样)。
aCoord 的定义需要和顶点着色器一模一样,但是在片元着色器能读取之前会通过光栅化传递,光栅化程序在三角形的三个顶点之间进行插值处理,会访问三个顶点之间每一个像素,然后对每
个像素点执行片元着色器。所以在片元着色器中 aCoord 的值可以看成几何中每一个像素点的坐标。
texture2D(vTexture,aCoord) ,则是利用摄像头纹理的采样器 vTexture 获取 aCoord 坐标的像素RGBA值并赋值给 gl_FragColor ,OpenGL就知道当前处理的片元是什么颜色从而绘制。
GLSL
OpenGL着色语言(OpenGL Shading Language)稍微了解的信息如下
OpenGL整体结构流程
openGL绘制纹理
public class ScreenFilter {
private final int vPosition;
private final int vCoord;
private final int vTexture;
private final int vMatrix;
private int program;
FloatBuffer vertexBuffer; //顶点坐标缓存区
FloatBuffer textureBuffer; // 纹理坐标
private int mWidth;
private int mHeight;
private float[] mtx;
public ScreenFilter(Context context) {
//准备坐标数据
/*** 顶点坐标 * ================================================================ */
// 4个点 x,y = 4*2 float 4字节 所以 4*2*4
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, 0.0f,
1.0f, 0.0f,
0.0f, 1.0f,
1.0f, 1.0f
};
textureBuffer.clear();
textureBuffer.put(TEXTURE);
//读取写在raw下的着色器程序的字符串代码,当然也可以从服务器下载
String vertexSharder = OpenGLUtils.readRawTextFile(context, R.raw.camera_vert);
String fragSharder = OpenGLUtils.readRawTextFile(context, R.raw.camera_frag);
//着色器程序准备好
通过着色器代码准备好着色器程序,用int 表示这个程序的 id
program = OpenGLUtils.loadProgram(vertexSharder, fragSharder);
//获取程序中的变量 索引
//获得顶点着色器中的 attribute 变量的索引值
vPosition = GLES20.glGetAttribLocation(program, "vPosition");
vCoord = GLES20.glGetAttribLocation(program, "vCoord");
//获得片元着色器中的 Uniform vTexture变量的索引值
vTexture = GLES20.glGetUniformLocation(program, "vTexture");
vMatrix = GLES20.glGetUniformLocation(program, "vMatrix");
}
public void setSize(int width, int height) {
mWidth = width;
mHeight = height;
}
public void onDraw(int texture) {
//设置绘制区域
GLES20.glViewport(0, 0, mWidth, mHeight);
GLES20.glUseProgram(program);
vertexBuffer.position(0);
// ormalized [-1,1] . 把[2,2]转换为[-1,1]
GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
//CPU传数据到GPU,默认情况下着色器无法读取到这个数据。 需要我们启用一下才可以读取
GLES20.glEnableVertexAttribArray(vPosition);
textureBuffer.position(0);
//normalized [-1,1] . 把[2,2]转换为[-1,1]
GLES20.glVertexAttribPointer(vCoord, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
//CPU传数据到GPU,默认情况下着色器无法读取到这个数据。 需要我们启用一下才可以读取
GLES20.glEnableVertexAttribArray(vCoord);
//相当于激活一个用来显示图片的画框
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
// 0: 图层ID GL_TEXTURE0
// GL_TEXTURE1 , 1
GLES20.glUniform1i(vTexture, 0);
GLES20.glUniformMatrix4fv(vMatrix, 1, false, mtx, 0);
//通知画画,
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
}
public void setTransformMatrix(float[] mtx) {
this.mtx = mtx;
}
}
另一种解法
第一步
xml里面使用TextureView
textureView.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
private Surface mSurface;
@Override
public void onSurfaceTextureAvailable(@NonNull SurfaceTexture st, int width,
int height) {
Log.e(TAG, "onSurfaceTextureAvailable: " + st.toString());
mSurface = new Surface(st);
renderer.attachOutputSurface(mSurface, new Size(width, height),
Surfaces.toSurfaceRotationDegrees(textureView.getDisplay().getRotation()));
}
@Override
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture st, int width,
int height) {
renderer.attachOutputSurface(mSurface, new Size(width, height),
Surfaces.toSurfaceRotationDegrees(textureView.getDisplay().getRotation()));
}
@Override
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture st) {
Surface surface = mSurface;
mSurface = null;
renderer.detachOutputSurface().addListener(() -> {
surface.release();
st.release();
}, ContextCompat.getMainExecutor(textureView.getContext()));
return false;
}
@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture st) {
Log.e(TAG, "onSurfaceTextureUpdated: " + st.toString());
}
});
第二步
Preview preview = new Preview.Builder().setTargetAspectRatio(AspectRatio.RATIO_4_3).build();
mRenderer.attachInputPreview(preview).addListener(() -> {
//绑定preview到render
Log.d(TAG, "OpenGLRenderer get the new surface for the Preview");
}, ContextCompat.getMainExecutor(this));
CameraSelector cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA;
mCameraProvider.bindToLifecycle(this, cameraSelector, preview);
第三步
预览添加监听,通过JNI去初始化、释放GL环境、获取纹理的名字等,具体代码在这里,比较坑,我浪费了很长的时间去下载编译,但是跑不通完整项目,如果有大佬跑通了求赐教,最后我是把核心的类和cpp文件拷贝到我自己的Demo里才跑通!
preview.setSurfaceProvider()
mPreviewTexture = new SurfaceTexture(getTexName(mNativeContext));
mPreviewTexture.setDefaultBufferSize(size.getWidth(), size.getHeight());
mPreviewTexture.setOnFrameAvailableListener(
surfaceTexture -> {
if (surfaceTexture == mPreviewTexture && !mIsShutdown) {
Log.e(TAG, "setOnFrameAvailableListener: " + surfaceTexture);
surfaceTexture.updateTexImage();
renderLatest();
}
},
mExecutor.getHandler());
private static native int getTexName(long nativeContext);
参考
有错误的地方欢迎指出!
基础知识和老的cameraX对接OpenGL来自B站Lance老师
以及官网