OpenGL着色器语言 —— GLSL

OpenGL(Open Graphics Library)定义了一个跨编程语言、跨平台编程的专业图形程序接口规范,所以说OpenGL只是一套规范,不要理解成FFmpeg那种开源库了。本篇文章会涉及OpenGL的着色语言GLSL,以及如何使用OpenGL来渲染摄像头图像到屏幕上,并且通过着色器可以实现灰度化,底片等效果。

OpenGL简介

OpenGL可用于二维或三维图像的处理和渲染,它是一个功能强大、调用方便的底层图形库规范。OpenGL ES是OpenGL的子集,专门针对嵌入式设备的版本。需要注意的是:OpenGL 是个状态机,面向过程编程。

很多人认为OpenGL是一套API,包含了一系列可以操作图形、图像的函数。事实上,OpenGL仅仅是一个由 Khronos组织 制定并维护的一套规范。OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现的,将由OpenGL库的开发者自行决定,通常OpenGL库的实现者是显卡厂商。因为OpenGL规范并没有规定实现的细节,具体的OpenGL库允许使用不同的实现,只要其功能和结果与规范相匹配,所以作为用户不会感受到功能上的差异。

Android支持的OpenGL ES对应的版本如下:

OpenGL ES 1.0 和 1.1 - 此 API 规范受 Android 1.0 及更高版本的支持。

OpenGL ES 2.0 - 此 API 规范受 Android 2.2(API 级别 8)及更高版本的支持。

OpenGL ES 3.0 - 此 API 规范受 Android 4.3(API 级别 18)及更高版本的支持。

OpenGL ES 3.1 - 此 API 规范受 Android 5.0(API 级别 21)及更高版本的支持。

GlSurfaceView

调用任何 OpenGL 函数前,必须已经创建了 OpenGL 上下文。Android中GLSurfaceView中会为我们初始化OpenGL上下文。GlSurfaceView继承至SurfaceView,它内嵌的Surface专门负责OpenGL渲染。

GlSurfaceView用于管理Surface与EGL;

GlSurfaceView允许自定义渲染器(render);

GlSurfaceView支持按需渲染和连续渲染;

public void setRenderer(Renderer renderer) {
    checkRenderThreadState();
    if (mEGLConfigChooser == null) {
        mEGLConfigChooser = new SimpleEGLConfigChooser(true);
    }
    if (mEGLContextFactory == null) {
        mEGLContextFactory = new DefaultContextFactory();
    }
    if (mEGLWindowSurfaceFactory == null) {
        mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
    }
    mRenderer = renderer;
    mGLThread = new GLThread(mThisWeakRef);
    mGLThread.start();
}

可以看到GlSurfaceView在设置渲染器的时候通过GLThread线程去执行渲染任务。

// 手动渲染,也就是按需渲染
public final static int RENDERMODE_WHEN_DIRTY = 0;

// 自动渲染,大概16ms刷新一次
public final static int RENDERMODE_CONTINUOUSLY = 1;

public void setRenderMode(int renderMode) {
    mGLThread.setRenderMode(renderMode);
}

可以看到GlSurfaceView支持手动渲染和自动渲染两种模式,注意必须在setRenderer 之后才能指定渲染模式。

GLSL —— OpenGL着色语言

在介绍OpenGL运行过程的文章 《OpenGL渲染管线》 中,提到了其中需要手动编写的就是顶点着色器程序和片元着色器程序,那么该怎么编写呢?这个时候就会用到OpenGL的着色语言——GLSL。

OpenGL着色语言(OpenGL Shading Language) ,开发人员利用这种语言编写程序运行在GPU(Graphic Processor Unit,图形图像处理单元,可以理解为是一种高并发的运算器)上以进行图像的处理或渲染。GLSL着色器代码分为两个部分,即Vertex Shader(顶点着色器)与Fragment Shader(片元着色器)两部分,分别完成各自在OpenGL渲染管线中的功能。

下面来看看GLSL的语法:

GLSL的语法与C语言非常类似,首先要看它的数据类型表示,然后再学习具体的运行流程。对于GLSL,其数 据类型表示具体如下:

变量修饰符

修饰符 用途 & 作用范围
const 用于声明只读的编译时常量变量。
attribute 属性变量。用于经常更改的信息,只能用于顶点着色器输入数据。如顶点坐标、纹理坐标、颜色等。
uniform 一致变量。在着色器执行期间一致变量的值是不变的。类似常量,但不同的是,这个值在编译时期是未知的,由着色器外部初始化。可用于顶点着色器和片元着色器。
varying 易变变量。用于修饰从顶点着色器向片元着色器传递的变量。

数据类型

基本数据类型与float修饰符

基本数据类型,int、float、bool,这些与C语言都是一致的,但是float是有一个修饰符的,即可以指定精度。三种修饰符的范围(范围一般视显卡而定)和应用情况具体如下:

float的修饰符 大小 应用场景
highp 32bit 一般用于顶点坐标(vertex Coordinate)
medium 16bit 一般用于纹理坐标(texture Coordinate)
lowp 8bit 一般用于颜色表示(color)

向量类型

向量类型是Shader中非常重要的一个数据类型,因为在做数据传递的时候需要经常传递多个参数,相较于写多个基本数据类型,使用向量类型是非常好的选择。列举一个最经典的例子,要将物体坐标和纹理坐标传递到Vertex Shader中,每一个顶点都是一个四维向量(x、y、 z、w,其中w是缩放因子),在Vertex Shader中利用这两个四维向量即可完成自己的纹理坐标映射操作。声明方式如下(GLSL代码):

attribute vec4 position;

矩阵类型

比如需要做卷积运算,需要传入卷积核,那么就可以使用矩阵类型。声明方式如下(GLSL代码):

uniform lowp mat4 colorMatrix; // 4×4的浮点矩阵

uniform medium mat2 colorMatrix; // 2×2的浮点矩阵

纹理类型

一般仅在Fragment Shader中使用这个类型,二维纹理的声明方式如下(GLSL代码):

uniform sampler2D texSampler

内置函数与内置变量

Vertex Shader的内置变量(GLSL代码):

vec4 gl_position;

上述代码用来设置顶点转换到屏幕坐标的位置,Vertex Shader一定要去更新这个数值。另外还有一个内置变量,代码如下(GLSL代码):

float gl_pointSize;

在粒子效果的场景下,需要为粒子设置大小,改变该内置变量的值就是为了设置每一个粒子矩形的大小。

Fragment Shader的内置变量(GLSL代码):

vec4 gl_FragColor;

用于指定当前纹理坐标所代表的像素点的最终颜色值。

内置函数,具体的函数可以去官方文档中查询,这里仅介绍几个常用的函数:

函数 用途
abs(genType x) 绝对值函数
floor(genType x) 向下取整函数
ceil(genType x) 向上取整函数
mod(genType x,genType y) 取模函数
min(genType x,genType y) 取得最小值函数
max(genType x,genType y) 取得最大值函数
clamp(genType x,genType y,genType z) 取得中间值函数
step(genType edge,genType x) 如果x < edge,则返回0.0,否则返回1.0
smoothstep(genType edge0,genType edge1,genType x) 如果x≤edge0,则返回0.0;如果x≥edge1,则返回1.0;如果edge0<x<edge1,则执行0~1之间的平滑差值.
mix(genType x,genType y,genType a) 返回线性混合的x和y,用公式表示为:x*(1-a)+y*a,这个函数在mix两个纹理图像的时候非常有用。

其他的角度函数、指数函数、几何函数可以去官方文档进行查询。

流程控制语句

GLSL的控制流与C语言非常类似,既可以使用for、while以及do-while实现循环,也可以使用if和if-else进行条件分支的操作,在后面的实践过程中及GLSL代码中都会用到这些控制流,在这里将不再讲解这些枯燥的语法。这里是他的官网文档 https://www.khronos.org/registry/OpenGL-Refpages/gl4/html/index.php

需要注意的是GLSL没有switch语句,但是过多的if或if-else语句会降低着色器的执行速度 。

结合上图我们发现,其实对于顶点着色器和片元着色器来讲,其重要的几个内置变量就是:

gl_Positiongl_Position内置变量主要和顶点相关,出现的位置是顶点着色器语言的main函数中。gl_Position内置变量表示最终传入片元着色器片元化要使用的顶点位置坐标)、gl_PointSize(用来设置顶点渲染出来的方形点像素大小)、gl_FragColor(用于指定当前纹理坐标所代表的像素点的最终颜色值)。其他的内置变量等需要用的时候再查阅官方文档即可。

创建显卡执行程序

其实,根据OpenGL渲染管线的流程,我们需要做的就是两件事情:

1、编写顶点着色器

2、编写片元着色器

编写了GLSL代码,那么如何让显卡来运行写好的着色器代码呢?下面是创建并启动显卡执行程序的过程中要用到的函数。总体上看就是创建程序,创建(顶点+片元)着色器,绑定并链接着色器,启动显卡执行程序,如下图:

OpenGL坐标系统

为了简单方便理解,OpenGL坐标系可分为:世界坐标系和纹理坐标系。

世界坐标系:在OpenGL中,世界坐标系是以屏幕中心为原点(0, 0, 0),且是始终不变的。面对屏幕,你的右边是x正轴,上面是y正轴,屏幕指向你的为z正轴。OpenGL标准化设备坐标规定了一切坐标点都在-1到1之间。

纹理坐标系:是绘制物体时的坐标系。OpenGL使用右手坐标,从左到右,x递增,从下到上,y递增,从远到近,z递增。

实现渲染摄像头画面

CameraView extends GLSurfaceView,最终渲染的结果将会展示在这个View上。

CameraRender implements GLSurfaceView.Renderer, Preview.OnPreviewOutputUpdateListener, SurfaceTexture.OnFrameAvailableListener,CameraRender作为渲染器,从其实现的接口来看,实现了GLSurface的Renderer接口,提供具体渲染实现。还有预览输出更新的回调,比如横竖屏切换等等。还有第一帧可用的回调。

准备CameraX 与渲染视图

CameraView.java 渲染视图组件

package cn.tim.opengl.widget;

import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
import android.view.SurfaceHolder;

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);

        // 设置刷新方式
        // RENDERMODE_WHEN_DIRTY 手动刷新,调用requestRender();
        // RENDERMODE_CONTINUOUSLY 自动刷新,大概16ms自动回调一次onDraw方法
        setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        super.surfaceDestroyed(holder);
        renderer.onSurfaceDestroyed();
    }
}

CameraRender.java 渲染器:

package cn.tim.opengl.widget;

import android.graphics.SurfaceTexture;
import android.opengl.GLSurfaceView;

import androidx.camera.core.Preview;
import androidx.lifecycle.LifecycleOwner;


import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

import cn.tim.opengl.filter.ScreenFilter;
import cn.tim.opengl.utils.CameraHelper;

public class CameraRender implements GLSurfaceView.Renderer,
        Preview.OnPreviewOutputUpdateListener,
        SurfaceTexture.OnFrameAvailableListener {

    private CameraView cameraView;
    private CameraHelper cameraHelper;

    // 摄像头的图像,用OpenGL ES画出来
    private SurfaceTexture mCameraTexture;

    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
        mCameraTexture.attachToGLContext(textures[0]);
        // 当摄像头数据有更新回调 onFrameAvailable
        mCameraTexture.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) {
        // 更新纹理
        mCameraTexture.updateTexImage();
        // 获取CameraX中给定的校准参考矩阵
        mCameraTexture.getTransformMatrix(mtx);

        screenFilter.setTransformMatrix(mtx);
        screenFilter.onDraw(textures[0]);
    }

    public void onSurfaceDestroyed() {

    }

    /**
     * 更新
     * @param output 预览输出
     */
    @Override
    public void onUpdated(Preview.PreviewOutput output) {
        mCameraTexture = output.getSurfaceTexture();
    }

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        // 请求执行一次 onDrawFrame
        cameraView.requestRender();
    }
}

CameraHelper.java CamerX的帮助工具类,生命周期与Activity绑定。

package cn.tim.opengl.utils;

import android.os.HandlerThread;
import android.util.Size;

import androidx.camera.core.CameraX;
import androidx.camera.core.Preview;
import androidx.camera.core.PreviewConfig;
import androidx.lifecycle.LifecycleOwner;

public class CameraHelper {
    private HandlerThread handlerThread;
    private CameraX.LensFacing currentFacing = CameraX.LensFacing.BACK;
    private Preview.OnPreviewOutputUpdateListener listener;

    public CameraHelper(LifecycleOwner lifecycleOwner, Preview.OnPreviewOutputUpdateListener listener) {
        this.listener = listener;
        handlerThread = new HandlerThread("Analyze-thread");
        handlerThread.start();
        CameraX.bindToLifecycle(lifecycleOwner, getPreView());
    }

    private Preview getPreView() {
        // 分辨率并不是最终的分辨率,CameraX会自动根据设备的支持情况
        // 结合你的参数,设置一个最为接近的分辨率
        PreviewConfig previewConfig = new PreviewConfig.Builder()
                .setTargetResolution(new Size(640, 480))
                .setLensFacing(currentFacing) // 前置或者后置摄像头
                .build();
        Preview preview = new Preview(previewConfig);
        preview.setOnPreviewOutputUpdateListener(listener);
        return preview;
    }
}

编写顶点&片元着色器

下面是重点,编写顶点着色器与片元着色器,在raw目录下新建两个文件存放顶点着色器与片元着色器的代码,

顶点着色器 camera_vert.vert

attribute vec4 vPosition; // 变量 float[4] 一个顶点坐标,Java传过来的

attribute vec2 vCoord;  // 纹理坐标,也是Java传过来的

varying vec2 aCoord; // aCoord是片元着色器传递过去的参数

uniform mat4 vMatrix; // Java传过来的CameraX坐标校正参数

void main(){
    // 内置变量: 把坐标点赋值给gl_position
    gl_Position = vPosition;
    // aCoord是片元着色器传递过去的参数
    aCoord = (vMatrix * vec4(vCoord,1.0,1.0)).xy;
}

片元着色器 camera_frag.frag

#extension GL_OES_EGL_image_external : require

// 摄像头数据比较特殊的一个地方
precision mediump float; // 数据精度
varying vec2 aCoord;

uniform samplerExternalOES  vTexture;  // samplerExternalOES: 图片,采样器

void main(){
    //  texture2D: vTexture采样器,采样  aCoord 这个像素点的RGBA值
    vec4 rgba = texture2D(vTexture,aCoord);  //rgba
    gl_FragColor = vec4(rgba.r, rgba.g, rgba.b, rgba.a); // 原画
    //gl_FragColor = vec4(1.-rgba.r,1.-rgba.g,1.-rgba.b,rgba.a); // 底片效果

    // 公式灰度化
    //float r = rgba.r * 0.299 + rgba.g * 0.587 + rgba.b * 0.114; // 灰度效果
    //gl_FragColor = vec4(r, r, r, rgba.a);
}

写成两个文件是因为方便读取和编写,其实这些着色器的代码写在哪里都是可以的,无论是硬编码到Java字符串中,还是从网络请求,结果都是一样的,都要读取成字符串,方便编译得到着色器程序即可。

如果要拿到摄像头的原始数据并展示在屏幕上,那么必须使用ExternalOES这个采样器。另外要开启ExternalOES:#extension GL_OES_EGL_image_external : require 这句

编译&链接着色器程序

现在开始生成着色器程序,并且把着色器连接到显卡执行程序中:

OpenGLUtils.java

package cn.tim.opengl.utils;

import android.content.Context;
import android.opengl.GLES20;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

public class OpenGLUtils {

    private static final String TAG = "OpenGLUtils";

    // 顶点坐标
    public static final float[] VERTEX = {
            -1.0f, -1.0f,
            1.0f, -1.0f,
            -1.0f, 1.0f,
            1.0f, 1.0f
    };
    
	// 纹理坐标
    public static final float[] TEXURE = {
			0.0f, 0.0f,
            1.0f, 0.0f,
            0.0f, 1.0f,
            1.0f, 1.0f
    };
    
    // 读取RAW目录下的资源文件
    public static String readRawTextFile(Context context, int rawId) {
        InputStream is = context.getResources().openRawResource(rawId);
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line;
        StringBuilder sb = new StringBuilder();
        try {
            while ((line = br.readLine()) != null) {
                sb.append(line);
                sb.append("\n");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        try {
            br.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sb.toString();
    }


    // 创建显卡执行程序
    public static int loadProgram(String vSource, String fSource) {
        // =============== 创建顶点着色器 & 片元着色器 ======================
        // 创建顶点着色器
        int vShader = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
        // 加载着色器代码
        GLES20.glShaderSource(vShader, vSource);
        // 编译(配置)
        GLES20.glCompileShader(vShader);

        // 查看配置是否成功
        int[] status = new int[1];
        GLES20.glGetShaderiv(vShader, GLES20.GL_COMPILE_STATUS, status, 0);
        if (status[0] != GLES20.GL_TRUE) {
            //失败
            throw new IllegalStateException("load vertex shader:" + GLES20.glGetShaderInfoLog
                    (vShader));
        }

        // 创建片元着色器,流程和上面一样
        int fShader = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
        // 加载片元着色器代码
        GLES20.glShaderSource(fShader, fSource);
        // 编译(配置)
        GLES20.glCompileShader(fShader);

        // 查看配置是否成功
        GLES20.glGetShaderiv(fShader, GLES20.GL_COMPILE_STATUS, status, 0);
        if (status[0] != GLES20.GL_TRUE) {
            //失败
            throw new IllegalStateException("load fragment shader:" + GLES20.glGetShaderInfoLog
                    (vShader));
        }
        
        // =============== 创建显卡执行程序 ======================
        int program = GLES20.glCreateProgram();
        // 绑定顶点和片元到显卡执行程序中
        GLES20.glAttachShader(program, vShader);
        GLES20.glAttachShader(program, fShader);
        // 链接显卡执行程序
        GLES20.glLinkProgram(program);
        
        // 获得状态
        GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, status, 0);
        if (status[0] != GLES20.GL_TRUE) {
            throw new IllegalStateException("link program:" + GLES20.glGetProgramInfoLog(program));
        }
        GLES20.glDeleteShader(vShader);
        GLES20.glDeleteShader(fShader);
        return program;
    }
}

启动显卡执行程序

ScreenFilter.java

package cn.tim.opengl.filter;

import android.content.Context;
import android.opengl.GLES20;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;

import cn.tim.opengl.R;
import cn.tim.opengl.utils.OpenGLUtils;

public class ScreenFilter {
    private final int vPosition;
    private final int vCoord;
    private final int vTexture;
    private final int vMatrix;
    private final int program;
    FloatBuffer vertexBuffer; // 顶点坐标缓存区
    FloatBuffer textureBuffer; // 纹理坐标
    private int mWidth;
    private int mHeight;
    private float[] mtx; // CameraX的校正矩阵

    public ScreenFilter(Context context) {
        // 准备数据
        vertexBuffer = ByteBuffer.allocateDirect(4 * 4 * 2)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        vertexBuffer.clear();
        vertexBuffer.put(OpenGLUtils.VERTEX);

        textureBuffer = ByteBuffer.allocateDirect(4 * 4 * 2)
                .order(ByteOrder.nativeOrder())
                .asFloatBuffer();
        
        textureBuffer.clear();
        textureBuffer.put(OpenGLUtils.TEXURE);

        String vertexSharder = OpenGLUtils.readRawTextFile(context, R.raw.camera_vert);
        String fragSharder = OpenGLUtils.readRawTextFile(context, R.raw.camera_frag);
        // 着色器程序准备好
        program = OpenGLUtils.loadProgram(vertexSharder, fragSharder);

        // 获取程序中的变量索引
        vPosition = GLES20.glGetAttribLocation(program, "vPosition");
        vCoord = GLES20.glGetAttribLocation(program, "vCoord");
        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);
        // 归一化 normalized 把[2,2]转换为[-1,1],目前不启用归一化处理
        GLES20.glVertexAttribPointer(vPosition, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer);
        // CPU传数据到GPU,默认情况下着色器无法读取到这个数据,需要我们启用一下才可以读取
        GLES20.glEnableVertexAttribArray(vPosition);
        
        textureBuffer.position(0);
        // 归一化 normalized 把[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,因为激活的是图层0
        // GL_TEXTURE1,1
        GLES20.glUniform1i(vTexture,0);

        // 通过一致变量(uniform修饰的变量)引用将一致变量值传入渲染管线
        GLES20.glUniformMatrix4fv(vMatrix, 1, false, mtx, 0);

        // 通知画画
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP,0,4);
        // 与图层0进行绑定
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,0);
    }

    // 接收CameraX的校正矩阵
    public void setTransformMatrix(float[] mtx) {
        this.mtx = mtx;
    }
}

MainActivity.java

package cn.tim.opengl;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {
    // 申请权限请求码
    private static final int REQUEST_CAMERA = 1001;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        verifyStoragePermissions(this);
    }

    // 检查摄像头权限
    public static void verifyStoragePermissions(Activity activity) {
        int cameraPermission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.CAMERA);

        if (cameraPermission != PackageManager.PERMISSION_GRANTED) {
            // 如果没有权限需要动态地去申请权限
            ActivityCompat.requestPermissions(
                    activity,
                    // 权限数组
                    new String[]{
                            Manifest.permission.CAMERA
                    },
                    // 权限请求码
                    REQUEST_CAMERA
            );
        }
    }
}

main_activity.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <cn.tim.opengl.widget.CameraView
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
</RelativeLayout>

CameraX的依赖:

implementation "androidx.camera:camera-core:1.0.0-alpha05"
implementation "androidx.camera:camera-camera2:1.0.0-alpha05"

声明摄像头权限:

<uses-permission android:name="android.permission.CAMERA"/>

这个是分步骤介绍的程序源码,完整程序代码请见: https://github.com/zouchanglin/render-camera