紧接着上篇文章,既然是要画出预览帧,按照之前其他项目的架构组成。我们是通过模型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看看情况如何?
现在我们已经正常的在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变量,然后视频纹理和签名纹理各自对应自己的纹理坐标。不过又有同学了,这太麻烦了不够灵活啊?!
之前在介绍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到干货知识点了吧?
github项目地址:https://github.com/MrZhaozhirong/BlogApp