OpenGL.ES在Android上的简单实践:23-水印录制(FBO离屏录制,解决透明冲突)
1、水印签名罢工了?
不知道大家有没注意到,之前我们使用MediaCodec录制的视频,水印签名那部分区域还是黑黑的啊(笑哭.jpg)。道理还是之前说过的,原生的Surface默认格式是RGB565,不支持透明通道。我也在 20-水印录制 提供了解决方案。 第一种就是在shader层使用内置函数Mix进行混合(不推荐);第二种解决方案就是利用系统API,SurfaceHolder.setFormat(PixelFormat.RGBA_8888)(推荐); 但是我们这次的Surface是引用MediaCodec.createInputSurface。这下就没能setFormat了?(起码我是不知道Surface->SurfaceHolder,有知道方法的兄弟请指引一下) 难不成我们录制视频的水印就这样黑黑不处理?不要怂,我还有法宝!
2、离屏渲染->FrameBufferObject
今天我为大家介绍离屏渲染的概念。百度下来很多都是在说iOS的,其实这个概念不止是iOS独有,这个是所有渲染系统都应该具备的。在OpenGL中,GPU屏幕渲染有以下两种方式:
1.On-Screen Rendering
意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。(例如我们显示/录制的Surface)
2.Off-Screen Rendering
意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。(区别于Surface的另外一种渲染区)
你可能会说,我懂啊,录制视频就是离屏渲染啊!不不不,概念可不一样哦,请参照下图并说明。
我们用于预览/录制的Surface都是由Android系统提供给OpenGL渲染的帧缓冲区,OpenGL的所有渲染结果都是直接到达到帧缓冲区,这是on-screen的渲染方式;
除此方式以外,OpenGL还扩展提供了一种方式来创建额外的帧缓冲区对象(FBO)。使用帧缓冲区对象,OpenGL可以将原先绘制到窗口提供的帧缓冲区重定向到FBO之中。和窗口提供的帧缓冲区类似,FBO提供了一系列的缓冲区,包括颜色缓冲区、深度缓冲区和模板缓冲区。这些逻辑的缓冲区在FBO中被称为 framebuffer-attachable说明它们是可以绑定到FBO的对象数组上。
FBO中有两类绑定的对象:纹理图像(texture images)和渲染图像(renderbuffer images)。如果纹理对象绑定到FBO,那么OpenGL就会执行渲染到纹理(render to texture)
的操作,如果渲染对象绑定到FBO,那么OpenGL会执行离屏渲染(offscreen rendering)
FBO可以理解为包含了许多挂接点的一个对象,它自身并不存储图像相关的数据,他提供了一种可以快速切换外部纹理对象和渲染对象挂接点的方式,在FBO中必然包含一个深度缓冲区挂接点和一个模板缓冲区挂接点,同时还包含许多颜色缓冲区挂节点(具体多少个受OpenGL实现的影响,可以通过GL_MAX_COLOR_ATTACHMENTS使用glGet查询),FBO的这些挂接点用来挂接纹理对象和渲染对象,这两类对象中才真正存储了需要被显示的数据。FBO提供了一种快速有效的方法挂接或者解绑这些外部的对象,对于纹理对象使用 glFramebufferTexture2D
,对于渲染对象使用glFramebufferRenderbuffer
。具体描述参考下图:
3、创建FBO
既然我们已经认识了什么是FBO,接下来我们就认识认识关于FBO的OpenGL.API
和其他VAO,VBO,IBO一样,通过调用Gen****,创建FBO,代码如下:
final int frameBuffers[] = new int[1]; GLES20.glGenFramebuffers(1, frameBuffers, 0); if (frameBuffers[0] == 0) { int i = GLES20.glGetError(); throw new RuntimeException("Could not create a new frame buffer object, glErrorString : "+ GLES20.glGetString(i)); } int frameBufferId = frameBuffers[0];
接着,我们开始使用FBO的时候,需要通过绑定纹理对象来锁定挂接渲染区。
int textureId = createFBOTexture(width, height, format); //创建指定format的纹理对象 GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBufferId); //绑定fbo进行操作 GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, //说明FBO挂接操作 GLES20.GL_COLOR_ATTACHMENT0, //指定挂接区是单元0 GLES20.GL_TEXTURE_2D, //说明挂接的是纹理对象 textureId, //具体挂接的纹理对象 0);
接着我们就可以渲染了(draw),当渲染结束后,我们就需要解绑FBO,告诉OpenGL我们已经不需要在FBO上创作了。
GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
最后,展览结束关门了,我们需要收拾东西。
GLES20.glDeleteFramebuffers(1, new int[]{frameBufferId}, 0); GLES20.glDeleteTextures(1, new int[]{textureId}, 0);以上,so easy。妈妈再也不用担心我们学不会啦。
4、使用FBO
可能有同学会吐槽,这么简单的API还要你教怎么使用哦。别紧张嘛,会用 和 用得native是两码事嘛。所以接下来我给自己的FBO封装,方便往后代码的编写调用。
public class FrameBuffer { private int mWidth; private int mHeight; private int frameBufferId; private int textureId; public FrameBuffer() { mWidth=0; mHeight=0; frameBufferId=0; textureId=0; } public int getTextureId() { return textureId; } public boolean isInstantiation() { return mWidth!=0||mHeight!=0; } public boolean setup(int width, int height){ this.mWidth = width; this.mHeight = height; final int frameBuffers[] = new int[1]; GLES20.glGenFramebuffers(1, frameBuffers, 0); if (frameBuffers[0] == 0) { int i = GLES20.glGetError(); throw new RuntimeException("Could not create a new frame buffer object, glErrorString : "+ GLES20.glGetString(i)); } frameBufferId = frameBuffers[0]; GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); return true; } public boolean begin(){ if(textureId==0 ){ textureId = createFBOTexture(mWidth, mHeight, GLES20.GL_RGBA); } GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, frameBufferId); GLES20.glFramebufferTexture2D(GLES20.GL_FRAMEBUFFER, GLES20.GL_COLOR_ATTACHMENT0, GLES20.GL_TEXTURE_2D, textureId, 0); return true; } private int createFBOTexture(int width, int height, int format) { final int[] textureIds = new int[1]; GLES20.glGenTextures(1, textureIds, 0); if(textureIds[0] == 0){ int i = GLES20.glGetError(); throw new RuntimeException("Could not create a new texture buffer object, glErrorString : "+ GLES20.glGetString(i)); } GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureIds[0]); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, format, width, height, 0, format, GLES20.GL_UNSIGNED_BYTE, null); return textureIds[0]; } public void end(){ GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); } public void release(){ mWidth = 0; mHeight = 0; GLES20.glDeleteFramebuffers(1, new int[]{frameBufferId}, 0); GLES20.glDeleteTextures(1, new int[]{textureId}, 0); frameBufferId = 0; textureId = 0; } }
然后我们回到测试页面ContinuousRecordActivity,开始使用我们的FBO修正录像签名的透明问题。概述如下图:
不知道大家有没看懂,这个就是利用离屏渲染 和 实时渲染的区别所在。离屏渲染充当一些幕后的准备操作,当我们向fbo挂载一个纹理对象的时候,在fbo所渲染的任何指令,都是输出到这个挂载的纹理对象当中。而且这个纹理的格式、大小、类型都是由我们自己定义,非常方便灵活。
利用这个方法,我们可以声明一个支持透明格式(RGBA四通道)的纹理对象,随后把这个纹理各自传到实时预览 和 录制器当中渲染,以到达效果。事不宜迟,show code!
private FrameBuffer fbo; private FBOFrameRect mFBOFrameRect; @Override public void surfaceCreated(SurfaceHolder surfaceHolder) { ... ... ... mFrameRect.setShaderProgram(new FrameRectSProgram()); mWaterSign.setShaderProgram(new WaterSignSProgram()); mFBOFrameRect.setShaderProgram(new WaterSignSProgram()); ... ... ... fbo = new FrameBuffer(); fbo.setup(VIDEO_HEIGHT, VIDEO_WIDTH); }
我们新增一个FBOFrameRect,这个类和FrameRect区别在于内部引用的ShaderProgram,因为之前我的摄像头帧纹理是samplerExternalOES的类型;现在我们使用离屏渲染的方式,使用普通的纹理方式(纹理y坐标上下颠倒),所以我们直接引用水印签名的WaterSignSProgram就可以了;之后我们创建FBO,注意屏幕是竖屏的方式,和摄像头默认横屏是宽高互换。
private void drawFrame() { mDisplaySurface.makeCurrent(); fbo.begin(); 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); mCameraTexture.updateTexImage(); mCameraTexture.getTransformMatrix(mTmpMatrix); int viewWidth = sv.getWidth(); int viewHeight = sv.getHeight(); GLES20.glViewport(0, 0, viewWidth, viewHeight); mFrameRect.drawFrame(mTextureId, mTmpMatrix); GLES20.glViewport(0, 0, 288, 144); mWaterSign.drawFrame(mSignTexId); fbo.end(); GLES20.glViewport(0, 0, viewWidth, viewHeight); mFBOFrameRect.drawFrame(fbo.getTextureId()); mDisplaySurface.swapBuffers(); // 水印录制 状态设置 if(mRequestRecord) { if(!recording) { mRecordEncoder.startRecording(new CameraRecordEncoder.EncoderConfig( outputFile, VIDEO_HEIGHT, VIDEO_WIDTH, 1000000, EGL14.eglGetCurrentContext(), ContinuousRecordActivity.this)); mRecordEncoder.setTextureId(fbo.getTextureId()); recording = mRecordEncoder.isRecording(); } mRecordEncoder.frameAvailable(mCameraTexture); } else { if(recording) { mRecordEncoder.stopRecording(); recording = false; } } }
之后我们再看看drawFrame方法,我们把原来之前直接渲染的指令(mFrameRect.drawFrame 和 mWaterSign.drawFrame)都包裹在fbo(begin~end)的操作当中,这样渲染指令的结果就流入到fbo挂载的纹理对象。
正常的渲染流程结束后,我们就可以取出fbo.texture一整张的纹理对象,渲染到显示区域(mFBOFrameRect.drawFrame)。接着我们还要进行录制,把fbo.texture设置到编码录制器,编码录制器的渲染操作修改思路也是一样的。
private void handleFrameAvailable(float[] transform, long timestampNanos) { //先推动一次编码器工作,把编码后的数据写入Muxer mRecordEncoder.drainEncoder(false); mRecorderInputSurface.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, mConfig.mWidth, mConfig.mHeight ); //mFrameRect.drawFrame(mFrameTextureId, transform); //GLES20.glViewport(0, 0, 288, 144); //mWaterSign.drawFrame(mSignTexId); GLES20.glViewport(0, 0, mConfig.mWidth, mConfig.mHeight ); mFBOFrameRect.drawFrame(mFrameTextureId); // mRecorderInputSurface是 获取编码器的输入Surface 创建的EGLSurface, // 以上的draw直接渲染到mRecorderInputSurface,喂养数据到编码器当中,非常方便。 mRecorderInputSurface.setPresentationTime(timestampNanos); mRecorderInputSurface.swapBuffers(); }
我们可以看到,修改后的编码录制器不在需要独自的重新绘制FrameRect/WaterSign,我们只需要绘制显示FBO.texture就大功告成了,so easy。
而且因为FBO.texture是我们自己独立创建的,格式GLES20.GL_RGBA支持四通道的颜色区,水印签名不再变成黑色的了!
5、延伸思考。
这章我们学习了FBO,解决了MediaCodec.inputSurface不支持透明通道的问题。 其实FBO用途还有很多,比如微信视频通话中的显示对方的画中画效果,我们就可以利用FBO离屏渲染显示出来了;更有一些视频图像应用双击放大,右下角还保留显示着原图;再譬如一些短视频应用的 镜像 九宫格(不同色调) 等等效果。 相信同学们一定能自己扩展延伸。
至此,OpenGL.ES在Android上的简单实践:水印录制 结束。
The End .