CameraX和OpenGL的融合(cameraX预览数据openGL渲染)

上一篇实现了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.00,0 
 1.0,-1.00,1.0 
-1.0, 1.00,1.0 
 1.0, 1.01.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老师
以及官网

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
好的,我可以为你提供一个简单的GLSurfaceView和CameraX结合的示例代码,供参考: ```kotlin class CameraXGLSurfaceView(context: Context, attrs: AttributeSet?) : GLSurfaceView(context, attrs) { private val renderer: CameraXGLRenderer init { // 设置OpenGL版本 setEGLContextClientVersion(2) // 创建渲染器 renderer = CameraXGLRenderer(context) // 设置渲染器 setRenderer(renderer) // 设置渲染模式为连续模式 renderMode = RENDERMODE_CONTINUOUSLY } fun startCamera() { // 启动相机 renderer.startCamera() } fun stopCamera() { // 停止相机 renderer.stopCamera() } } class CameraXGLRenderer(private val context: Context) : GLSurfaceView.Renderer { private val cameraExecutor = Executors.newSingleThreadExecutor() private lateinit var cameraProvider: ProcessCameraProvider private lateinit var preview: Preview override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { // 初始化OpenGL环境 GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f) } override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) { // 设置视口大小 GLES20.glViewport(0, 0, width, height) } override fun onDrawFrame(gl: GL10?) { // 清空屏幕 GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT) // 绘制相机预览数据 preview.surfaceTexture?.let { // 更新纹理图像 it.updateTexImage() // 绘制纹理 GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4) } } fun startCamera() { // 启动相机 val cameraProviderFuture = ProcessCameraProvider.getInstance(context) cameraProviderFuture.addListener({ // 获取相机提供者 cameraProvider = cameraProviderFuture.get() // 创建预览用例 preview = Preview.Builder().build() // 绑定预览用例到相机 val cameraSelector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK) .build() val camera = cameraProvider.bindToLifecycle( context as LifecycleOwner, cameraSelector, preview ) // 创建纹理和SurfaceTexture val surfaceTexture = SurfaceTexture(0) surfaceTexture.setDefaultBufferSize(preview.width, preview.height) // 设置预览用例的SurfaceTexture preview.setSurfaceProvider { val surface = Surface(surfaceTexture) it.provideSurface(surface, cameraExecutor, { surface.release() }) } }, ContextCompat.getMainExecutor(context)) } fun stopCamera() { // 停止相机 cameraProvider.unbindAll() } } ``` 以上是一个简单的GLSurfaceView和CameraX的结合示例,其中GLSurfaceView用于显示相机预览数据CameraX用于获取相机数据。你可以根据自己的需求来进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alex_ChuTT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值