Camera + opengl es 使用opengls绘制摄像头数据(二)

实现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渲染的过程,可以用一张图形象的表示,

Camera + opengl es 使用opengls绘制摄像头数据(二)_第1张图片

任何形状的物体,在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是来校正摄像头数据的旋转问题的.

Camera + opengl es 使用opengls绘制摄像头数据(二)_第2张图片

在确定物体形状时,需要根据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个三角形,绘图模式:

Camera + opengl es 使用opengls绘制摄像头数据(二)_第3张图片

GLES20.GL_TRIANGLE_STRIP ,假设有N个顶点,也有n-2个三角形,

当N为奇数:第n个三角形的顶点是(n, n+1, n+2),

当N是偶数,第n个三角形的顶点是(n+1,n , n+2),

绘图模式:

Camera + opengl es 使用opengls绘制摄像头数据(二)_第4张图片

执行到这里,数据都准备好了,剩下就是在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;
    }

 

你可能感兴趣的:(音视频,android开发)