OpenGL.ES在Android上的简单实践:20-水印录制(预览+透明水印 表情 弹幕 gl_blend)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/a360940265a/article/details/80403715

OpenGL.ES在Android上的简单实践:20-水印录制(预览 gl_blend)

1、继续画出预览帧

紧接着上篇文章,既然是要画出预览帧,按照之前其他项目的架构组成。我们是通过模型FrameRect.draw的方法画出预览帧,在定义这个draw方法之前我们从着色器出发,看看需要什么。

    private static final String VERTEX_SHADER =
                    "uniform mat4 uMVPMatrix;\n" +
                    "attribute vec4 aPosition;\n" +
                    "uniform mat4 uTexMatrix;\n" +
                    "attribute vec4 aTextureCoord;\n" +
                    "varying vec2 vTextureCoord;\n" +
                    "void main() {\n" +
                    "    gl_Position = uMVPMatrix * aPosition;\n" +
                    "    vTextureCoord = (uTexMatrix * aTextureCoord).xy;\n" +
                    "}\n";

    private static final String FRAGMENT_SHADER_EXT =
                    "#extension GL_OES_EGL_image_external : require\n" +
                    "precision mediump float;\n" +
                    "varying vec2 vTextureCoord;\n" +
                    "uniform samplerExternalOES sTextureOES;\n" +
                    "void main() {\n" +
                    "    gl_FragColor = texture2D(sTextureOES, vTextureCoord);\n" +
                    "}\n";

从顶点着色器入手,大部分属性都已经介绍,那么这个uTexMatrix从哪里来?这个又要说到Camera.SurfaceTexture的getTransformMatrix了,从方法名理解就是获取变换矩阵?怎么个变换法啊,我们可以这里理解这个变换矩阵,其实就是我们之前介绍OpenGL的三大矩阵MVP的核矩阵。Camera通过SurfaceTexture反馈一个纹理帧对象,而且附带了当前摄像头设置变换的矩阵。譬如我们在打开摄像头openCamera的时候就设置了显示角度,如下

private void openCamera(int desiredWidth, int desiredHeight, int desiredFps) {
        ... ...// System API Open Camera
        Camera.Parameters parms = mCamera.getParameters();
        CameraUtils.choosePreviewSize(parms, desiredWidth, desiredHeight);//设置合适的长宽
        mCameraPreviewThousandFps = CameraUtils.chooseFixedPreviewFps(parms, desiredFps * 1000);//设置合适的帧率
        parms.setRecordingHint(true);//设置可录制的索引
        mCamera.setParameters(parms);//生成设置的参数

        Camera.Size cameraPreviewSize = parms.getPreviewSize();
        // 调整界面的长宽比例
        AspectFrameLayout layout = (AspectFrameLayout) findViewById(R.id.continuousRecord_afl);
        //layout.setAspectRatio((double) cameraPreviewSize.width / cameraPreviewSize.height);
        // Portrait
        layout.setAspectRatio((double) cameraPreviewSize.height / cameraPreviewSize.width);
        mCamera.setDisplayOrientation(90); //设置显示角度,这里会影响作用纹理帧的变换矩阵
    }

所以,此时ContinuousRecordActivity的drawFrame方法修改成如下:

    private final float[] mTmpMatrix = new float[16];
    private void drawFrame() {
        if (mEglCore == null) {
            Log.d(TAG, "Skipping drawFrame after shutdown");
            return;
        }
        Log.d(TAG, " MSG_FRAME_AVAILABLE");
        mDisplaySurface.makeCurrent();
        GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        mCameraTexture.updateTexImage(); // 获取预览帧
        mCameraTexture.getTransformMatrix(mTmpMatrix); // 获取预览帧的变换矩阵
        int viewWidth = sv.getWidth();
        int viewHeight = sv.getHeight();
        GLES20.glViewport(0, 0, viewWidth, viewHeight); //设置视口为整个surface大小
        mFrameRect.drawFrame(mTextureId, mTmpMatrix); // 画图
        mDisplaySurface.swapBuffers();
    }

剩下的就是去完成这个FrameRect.drawFrame的方法实现了。我们继续show代码

    public void drawFrame(int mTextureId, float[] texMatrix) {
        GLES20.glUseProgram(mProgram.getShaderProgramId());
        // 设置纹理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
        GLES20.glUniform1i(mProgram.sTextureOESLoc, 0);
        GlUtil.checkGlError("TEXTURE_EXTERNAL_OES sTextureOES");
        // 设置 model / view / projection 矩阵
        GLES20.glUniformMatrix4fv(mProgram.uMVPMatrixLoc, 1, false, getFinalMatrix(), 0);
        GlUtil.checkGlError("glUniformMatrix4fv uMVPMatrixLoc");
        // 设置 纹理变换矩阵
        GLES20.glUniformMatrix4fv(mProgram.uTexMatrixLoc, 1, false, texMatrix, 0);
        GlUtil.checkGlError("glUniformMatrix4fv uTexMatrixLoc");
        // 使用简单的VAO 设置顶点坐标数据
        GLES20.glEnableVertexAttribArray(mProgram.aPositionLoc);
        GLES20.glVertexAttribPointer(mProgram.aPositionLoc, mCoordsPerVertex,
                GLES20.GL_FLOAT, false, mVertexStride, mVertexArray);
        GlUtil.checkGlError("VAO aPositionLoc");
        // 使用简单的VAO 设置纹理坐标数据
        GLES20.glEnableVertexAttribArray(mProgram.aTextureCoordLoc);
        GLES20.glVertexAttribPointer(mProgram.aTextureCoordLoc, mCoordsPerTexture,
                GLES20.GL_FLOAT, false, mTexCoordStride, mTexCoordArray);
        GlUtil.checkGlError("VAO aTextureCoordLoc");
        // GL_TRIANGLE_STRIP三角形带,这就为啥只需要指出4个坐标点,就能画出两个三角形了。
        GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, mVertexCount);
        // Done -- 解绑everything
        GLES20.glDisableVertexAttribArray(mProgram.aPositionLoc);
        GLES20.glDisableVertexAttribArray(mProgram.aTextureCoordLoc);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, 0);
        GLES20.glUseProgram(0);
    }

额 ... 感觉没啥好说的啊~注释都有了。坐标顶点和纹理顶点用了VAO的加载方法;第二个注意点就是glDrawArrays的绘制类型是GL_TRIANGLE_STRIP三角带,所以只需要四个顶点就可以画出两个连续的三角形了;其余那些m的变量都在FrameRect构造函数的时候准备好了;绑定的纹理对象就是Camera.SurfaceTexture的纹理对象,我们记住这是EXTERNAL_OES的类型,剩下的我们按照以前纹理的知识按部就班的去使用就没问题了。

现在我们运行demo看看情况如何?

2、添加水印

现在我们已经正常的在EGL环境下预览摄像头了!下一步就是添加水印签名。根据我们的直观认识,其实水印签名也就是一个透明背景的图而已嘛?!我们大胆猜测是否能这样?在片段着色器中增加一个普通的纹理对象sampler2D,然后在叠加显示它们? 事不宜迟,我们赶紧跳坑吧~(奸笑.jpg)

    private static final String VERTEX_SHADER = "... ... ...";

    private static final String FRAGMENT_SHADER_EXT =
                    "#extension GL_OES_EGL_image_external : require\n" +
                    "precision mediump float;\n" +
                    "varying vec2 vTextureCoord;\n" +
                    "uniform samplerExternalOES sTextureOES;\n" +
                    "uniform sampler2D sTexture;\n" +
                    "void main() {\n" +
                    "    //gl_FragColor = texture2D(sTextureOES, vTextureCoord);\n" +
                    "    vec4 texOES = texture2D(sTextureOES, vTextureCoord);\n"+
                    "    vec4 tex = texture2D(sTexture, vTextureCoord);\n"+
                    "    gl_FragColor = mix(texOES, tex, 0.5);\n"+
                    "}\n";

巴拉巴拉的,我们在片段着色器中增加多一个sampler2D普通纹理对象sTexture。然后我们先简单的运用GLSL自带的mix混合叠加两张纹理看看效果,关于更多的GLSL内部自带的函数变量请参阅这里,搜索内置函数。mix这个函数是GLSL中一个特殊的线性插值函数,他将前两个参数的值基于第三个参数按照以下公式进行插值:

genType mix (genType x, genType y, float a)
返回线性混合的x和y,如:x⋅(1−a)+y⋅a

重载我们FrameRect.drawFrame方法如下

    public void drawFrame(int mTextureId, float[] texMatrix, int mTextureSign) {
        GLES20.glUseProgram(mProgram.getShaderProgramId());
        // 设置预览纹理 使用单元0
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
        GLES20.glUniform1i(mProgram.sTextureOESLoc, 0);
        GlUtil.checkGlError("TEXTURE_EXTERNAL_OES sTextureOES");
        // 设置水印签名纹理 使用单元1
        GLES20.glActiveTexture(GLES20.GL_TEXTURE1);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureSign);
        GLES20.glUniform1i(mProgram.sTextureLoc, 1);
        GlUtil.checkGlError("GL_TEXTURE_2D sTexture");
        ... ... ... //以前的代码
        GLES20.glUseProgram(0);
    }

然后使用之前的模板工具代码,从mipmap加载我们的签名图片,并在传入新改造的drawFrame方法中。

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        ... ... ...
        mTextureId = GlUtil.createExternalTextureObject();
        ... ... ...
        mSignTexId = TextureHelper.loadTexture(ContinuousRecordActivity.this, R.mipmap.name);
        ... ... ...
    }

运行demo看看效果如何?   

因为我们使用的是同一组纹理坐标,所以占满了整个屏幕了。感觉还行吧,类似那种防盗版的视频不都是加了一个整屏幕的水印吗? (笑哭.jpg)好吧,在顶点着色器重新定义一个签名纹理的坐标attribute变量,然后视频纹理和签名纹理各自对应自己的纹理坐标。不过又有同学了,这太麻烦了不够灵活啊?!

3、更学科的添加水印

之前在介绍GL_TEXTURE_EXTERNAL_OES这个Android特有的纹理类型的时候,我就说到尽量把这个类型的纹理和普通纹理的分开在不同的GLSL渲染管线。这是更为科学优雅的做法。

我们依据FrameRect+FrameRectSProgram的模板代码,复制一份WaterSignature+WaterSignSProgram,然后我们需要注意以下几点的区别。

public class WaterSignSProgram extends ShaderProgram {

    private static final String VERTEX_SHADER =
                    "uniform mat4 uMVPMatrix;\n" +
                    "attribute vec4 aPosition;\n" +
                    "attribute vec4 aTextureCoord;\n" +
                    "varying vec2 vTextureCoord;\n" +
                    "void main() {\n" +
                    "    gl_Position = uMVPMatrix * aPosition;\n" +
                    "    vTextureCoord = aTextureCoord.xy;\n" +
                    "}\n";

    private static final String FRAGMENT_SHADER =
                    "precision mediump float;\n" +
                    "varying vec2 vTextureCoord;\n" +
                    "uniform sampler2D sTexture;\n" +
                    "void main() {\n" +
                    "    gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +
                    "}\n";

    public WaterSignSProgram() {
        ... ... ...
        //着色器程序的编译。属性、变量的获取等模板代码
        //详情follow github
    }
}

首先我们看着色器程序,普通纹理sampler2D,还有从顶点着色器传递过来的纹理坐标vTextureCoord是vec2二维的。而我们之前定义是vec4四维,我们只需要用前两个xy值就可以了。

public class WaterSignature {

    private static final float FULL_RECTANGLE_COORDS[] = {
            -1.0f, -1.0f,   // 0 bottom left
            1.0f, -1.0f,   // 1 bottom right
            -1.0f,  1.0f,   // 2 top left
            1.0f,  1.0f,   // 3 top right
    };
    private static final float FULL_RECTANGLE_TEX_COORDS[] = {
            0.0f, 1.0f,     //0 bottom left     //0.0f, 0.0f, // 0 bottom left
            1.0f, 1.0f,     //1 bottom right    //1.0f, 0.0f, // 1 bottom right
            0.0f, 0.0f,     //2 top left        //0.0f, 1.0f, // 2 top left
            1.0f, 0.0f,     //3 top right       //1.0f, 1.0f, // 3 top right
    };
    ... ... ...
    //构造函数初始化顶点坐标数据 纹理坐标数据 MVP三大矩阵
    //详情follow github
    public void drawFrame(int mTextureId) {
        GLES20.glUseProgram(mProgram.getShaderProgramId());
        // 设置纹理
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureId);
        GLES20.glUniform1i(mProgram.sTextureLoc, 0);
        GlUtil.checkGlError("GL_TEXTURE_2D sTexture");
        // 设置 model / view / projection 矩阵
        // 使用简单的VAO 设置顶点坐标数据
        // 使用简单的VAO 设置纹理坐标数据
        // Done -- 解绑~
    }

}

再到模型,因为这次是加载普通的纹理,我们的纹理坐标需要水平颠倒。参照之前SurfaceTexture的坐标就很好的转换过来了。drawFrame方法记得加载的目标纹理是GL_TEXTURE_2D。   东风准备好了,接着下来看看要怎么吹吧。

转到测试页面ContinuousRecordActivity,还是按照FrameRect的套路,在onCraete创建WaterSignature实例,在surfaceCreated的EGL环境下设置着色器程序。

下面我们来说说重点疑点。

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        ... ... ...
        mFrameRect.setShaderProgram(new FrameRectSProgram());
        mWaterSign.setShaderProgram(new WaterSignSProgram());
        mSignTexId = TextureHelper.loadTexture(ContinuousRecordActivity.this, R.mipmap.name);
        ... ... ...
    }
    private void drawFrame() {
        ... ... 
        mDisplaySurface.makeCurrent();
        ... ...
        GLES20.glViewport(0, 0, viewWidth, viewHeight);
        mFrameRect.drawFrame(mTextureId, mTmpMatrix);
        GLES20.glViewport(0, 0, 288, 144); // x, y, width, height. 设置绘制的视口位置/大小
        mWaterSign.drawFrame(mSignTexId);
        mDisplaySurface.swapBuffers();
    }

我们使用以前的工具代码,从res.mipmap加载透明背景的水印签名图片。然后就到drawFrame,我们在画出水印签名图前先更改视口的位置/大小,改为图尺寸大小or整数倍。这样我们的纹理坐标是填充整个视口的,不需要做很精密的计算,我们通过改动视口以达到更改签名的绘制的位置大小,就非常符合编程的思维。  现在我们跑起demo看看效果。(奸笑.jpg)

What the fxxk?这叫毛水印啊,透明效果呢?为啥会出现这样?首先要确保你的签名图是真的透明哦。好了,还是来说重点。其实这个是Surface的颜色域 和 GL的混合模式所造成的问题。有些低版本的Android的Surface默认颜色域是RGB_565的,所以我们要先申明其支持透明的RGBA_8888。申明方法如下:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.continuous_record);
        sv = (SurfaceView) findViewById(R.id.continuousRecord_surfaceView);
        SurfaceHolder sh = sv.getHolder();
        sh.setFormat(PixelFormat.RGBA_8888); // 申明其surface的颜色是RGBA_8888
        sh.addCallback(this);

        mHandler = new MainHandler(this);
        mFrameRect = new FrameRect();
        mWaterSign = new WaterSignature();
    }

然后就到GL的混合模式了,啥玩意?不知道同学们对Android.Paint的xfermode有无印象,从认知的角度来说其实是同一回事。在很多情况下,我们都需要在一个画布(Canvas/Surface)画一个以上的对象,多个对象间难免有重合叠加的情况,现实的艺术家通过颜色的混合来达到想要的效果,在程序上我们也是可以借用混合这个概念。  OpenGL打开混合模式的指令是如下代码:

private void drawFrame() {
        ... ... ...
        mDisplaySurface.makeCurrent();
        GLES20.glClear( GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glEnable(GLES20.GL_BLEND); //打开混合功能
        GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA); //指定混合模式
        GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        ... ... ...
        GLES20.glViewport(0, 0, viewWidth, viewHeight);
        mFrameRect.drawFrame(mTextureId, mTmpMatrix);
        GLES20.glViewport(0, 0, 288, 144);
        mWaterSign.drawFrame(mSignTexId);
        mDisplaySurface.swapBuffers();
    }

OpenGL的混合知识不少,前辈巨人们已经在这里作出了很详尽的介绍了。其中划重点:

1、混合就是在绘制时,不是直接把新的颜色覆盖在原来旧的颜色上,而是将新的颜色与旧的颜色经过一定的运算,从而产生新的颜色。新的颜色称为源颜色,原来旧的颜色称为目标颜色。传统意义上的混合,是将源颜色乘以源因子,目标颜色乘以目标因子,然后相加。
2、源因子和目标因子是可以设置的。源因子和目标因子设置的不同直接导致混合结果的不同。将源颜色的alpha值作为源因子,用1.0减去源颜色alpha值作 为目标因子,是一种常用的方式。这时候,源颜色的alpha值相当于“不透明度”的作用。利用这一特点可以绘制出一些半透明的物体。
3、在进行混合时,绘制的顺序十分重要。因为在绘制时,正要绘制上去的是源颜色,原来存在的是目标颜色,因此先绘制的物体就成为目标颜色,后来绘制的则成为源颜色。绘制的顺序要考虑清楚,将目标颜色和设置的目标因子相对应,源颜色和设置的源因子相对应。

前辈巨人们总结的OpenGL混合知识请一定要查阅一番(在这里点进去),一次读不懂不重要,到现在我也没完全熟练明白。(尴尬.jpg)从运用中理解会更能容易快速掌握。

总结:摄像头预览添加水印效果我们基本上已经完成了。通过这章我们复习了旧有的知识的同时学到了

1、同一渲染管线下,GL_TEXTURE_EXTERNAL_OES和GL_TEXTURE_2D的GLSL.mix线性混合(不推荐)

2、GL_BLEND的OpenGL混合模式(重点)

3、Android的surface颜色域(隐藏坑)

题外话:我们的水印签名图每帧都是固定位置的,能不能做成类似弹幕的平衡移动?嘿嘿嘿get到干货知识点了吧?

猜你喜欢

转载自blog.csdn.net/a360940265a/article/details/80403715