OpenGL.ES在Android上的简单实践:20-水印录制(预览 gl_blend)
1、继续画出预览帧
紧接着上篇文章 <https://blog.csdn.net/a360940265a/article/details/80366073>
,既然是要画出预览帧,按照之前其他项目的架构组成。我们是通过模型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内部自带的函数变量请参阅
这里 <https://blog.csdn.net/a360940265a/article/details/79066787>
,搜索内置函数。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
<https://www.cnblogs.com/libertycode/p/6290497.html>
有无印象,从认知的角度来说其实是同一回事。在很多情况下,我们都需要在一个画布(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的混合知识不少,前辈巨人们已经在这里
<https://blog.csdn.net/aurora_mylove/article/details/1700540>作出了很详尽的介绍了。其中划重点:
1、混合就是在绘制时,不是直接把新的颜色覆盖在原来旧的颜色上,而是将新的颜色与旧的颜色经过一定的运算,从而产生新的颜色。新的颜色称为源颜色,原来旧的颜色称为目标颜色。传统意义上的混合,是将源颜色乘以源因子,目标颜色乘以目标因子,然后相加。
2、源因子和目标因子是可以设置的。源因子和目标因子设置的不同直接导致混合结果的不同。将源颜色的alpha值作为源因子,用1.0减去源颜色alpha值作
为目标因子,是一种常用的方式。这时候,源颜色的alpha值相当于“不透明度”的作用。利用这一特点可以绘制出一些半透明的物体。
3、在进行混合时,绘制的顺序十分重要。因为在绘制时,正要绘制上去的是源颜色,原来存在的是目标颜色,因此先绘制的物体就成为目标颜色,后来绘制的则成为源颜色。绘制的顺序要考虑清楚,将目标颜色和设置的目标因子相对应,源颜色和设置的源因子相对应。
前辈巨人们总结的OpenGL混合知识请一定要查阅一番(在这里点进去
<https://blog.csdn.net/aurora_mylove/article/details/1700540>
),一次读不懂不重要,到现在我也没完全熟练明白。(尴尬.jpg)从运用中理解会更能容易快速掌握。
总结:摄像头预览添加水印效果我们基本上已经完成了。通过这章我们复习了旧有的知识的同时学到了
1、同一渲染管线下,GL_TEXTURE_EXTERNAL_OES和GL_TEXTURE_2D的GLSL.mix线性混合(不推荐)
2、GL_BLEND的OpenGL混合模式(重点)
3、Android的surface颜色域(隐藏坑)
题外话:我们的水印签名图每帧都是固定位置的,能不能做成类似弹幕的平衡移动?嘿嘿嘿get到干货知识点了吧?
热门工具 换一换