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函数
CameraX和OpenGL的融合(cameraX预览数据openGL渲染)

OpenGL可以做的事情很多,但是我现在只关心这个

CameraX和OpenGL的融合(cameraX预览数据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
CameraX和OpenGL的融合(cameraX预览数据openGL渲染)
上面这张图很形象的描述了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中传递进来。这两个值分别为顶点坐标纹理坐标

CameraX和OpenGL的融合(cameraX预览数据openGL渲染)
我们需要根据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)稍微了解的信息如下
CameraX和OpenGL的融合(cameraX预览数据openGL渲染)
CameraX和OpenGL的融合(cameraX预览数据openGL渲染)

OpenGL整体结构流程

CameraX和OpenGL的融合(cameraX预览数据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的融合(cameraX预览数据openGL渲染)

参考

有错误的地方欢迎指出!
基础知识和老的cameraX对接OpenGL来自B站Lance老师
以及官网

上一篇:OpenGL代码学习(1)


下一篇:OpenGL导入模型全黑,纹理消失问题