Android 系统下 OpenGL 三种离屏渲染技术
经常有同学问我如何将 OpenGL 渲染出的结果保存成mp4文件,今天我们就来聊聊这个话题。
离屏渲染 OpenGL
要想将OpenGL渲染出的结果保存成mp4文件,我们需要使用一种称为离屏渲染的技术。
什么是离屏渲染呢?顾名思意,就是让OpenGL不将渲染的结果直接输出到屏幕上,而是输出到一个中间缓冲区(一块GPU空间),然后再将中间缓冲区的内容输出到屏幕或编码器等目标上,这就称为离屏渲染。
使用离屏渲染有什么好处呢? 比如我们可以让OpenGL将渲染的结果先输出到一个帧缓冲区中,然后再把帧缓冲区的内容送给编码器进行编码,最后写到mp4文件中,这样就达到我们的目标了。 当然在将结果保存成mp4文件的同时,还能让它在屏幕上显示出来就更好了,那么如何做到这一点呢?
在Android系统下,有三种方法可以实现,下面我们就来一一介绍一下。
Android下离屏渲染的三种方式
在Android系统下可以使用三种方法实现同时将OpenGL的内容输出给多个目标(屏幕和编码器)。第一种方法是二次渲染法;第二种方法是使用FBO;第三种是使用BlitFramebuffer。
首先我们来看看如何通过二次渲染法实现将OpenGL渲染结果送给屏幕和编码器。
二次渲染法
想通过二次渲染法实现OpenGL渲染结果送屏幕和编码器,我们必须使用SurfaceView,而不能使用GLSurfaceView。
之所以如此,是因为GLSurfaceView有自己的专有渲染线程,这虽然减少了开发者使用OpenGL的复杂度,但也同时降低了开发者对EGL的控制力,显得不够灵活,从而无法将OpenGL的渲染结果输出给多个目标。
为了解决这个问题,我们需要使用 SurfaceView+自己创建渲染线程 这个组合,在自己创建的渲染线程中使用EGL API,通过多次渲染并将结果输送给多个目标Surface来实现二次渲染,其架构如下图所示:
在上图中,我们可以看到有 SurfaceView, MediaCodec, Camera, OpenGL/EGL等组件。
其中SurfaceView用于展示OpenGL的渲染结果,而且它还实现了两个重要的事件处理方法,surfaceCreated和surfaceChanged。
surfaceCreated方法是在SurfaceView中的Surface创建好后被调用。在该方法中,首先创建一个渲染线程,并在渲染线程中创建EGL环境。
而surfaceChanged方法是在Surface发生变化时被调用,在该方法中我们会创建FBO环境,关于这方面的内容我在讲解FBO时再向你做详细介绍。
MediaCodec 用于编码,它也有一个Surface用于接收需要编码的数据 。
Camera 用于采集视频数据,当采集到视频数据后,它会通知渲染线程,渲染线程通过SurfaceTexture从BufferQueue中取走数据,并交由OpenGL处理。
OpenGL/EGL用于渲染,它收到视频帧后调用Shader程序进行渲染,之后将渲染后的结果输出给SurfaceView的Surface ,让其在屏幕上显示;接下来,调用EGL的eglMakeCurrent方法,将默认Surface从SurfaceView的Surface改为MediaCodec的Surface,然后再次调用Shader程序进行渲染,并将渲染后的结果输出给MediaCodec的Surface进行编码。
也就是说,二次渲染就是调用两次Shader程序进行渲染,每次渲染后的结果输送给不同的目标Surface,因此称为二次渲染法。
当然,如果你需要将渲染结果输出给多个目标也是可以的,那就需要调用多次Shader程序,每次将渲染结果输送给不同的目标Surface就好了。
FBO法
上面的方法虽然能够将同一个源输送给不同的目标,但每次都要调用OpenGL进行重绘,效率上确实不高。尤其是当我们要渲染的模型特别复杂的时候,会严重影响效率,有没有更好的解决办法呢?
OpenGL为我们提供了一种高效的办法,即FBO(FrameBufferObject)。下面我们来看一下如何通过FBO高效的向多个目标输送渲染结果,其架构图如下所示:
其架构图与我们上面展示的二次渲染架构图很类似,只不过通过OpenGL渲染的结果不再直接送给不同的Surface,而是将结果输出到FBO中,这里你可以先将FBO想像为一块特殊的空间(关于FBO的细节,我会单独写一篇文章进行讲解)。
然后通过另外一种Shader程序(这种Shader程序是专门用于处理FBO纹理的)再次进行渲染。当然,这种Shader程序由于处理的是纹理,所以会比第一次使用的Shader程序效率高很多。
渲染成功后将结果输出给屏幕,然后切换Surface再次调用Shader(第二个Shader),这次渲染的结果将会输送给MediaCodec的Surface进行编码。
因此,通过FBO方法我们只需要对原模型渲染一次,将结果保存到FBO。之后再对FBO中的内容进行多次渲染,通过这种方式来提高效率。
注意,之所以通过这种方式可以提高效率,原因就在于OpenGL对FBO纹理的渲染效率远远高于对原模型的渲染效率。
BlitFramebuffer法
通过上面的描述,我们仍然觉得不够高效。有没有更好的方法,只渲染一次就可以将结果输送给多个目标呢?到了OpenGL3.0出现了一种更高效的方法,即BlitFramebuffer。
它是如何工作的呢?其工作架构图所下所示:
该方法不再使用FBO做缓存,而是像二次渲染法一样,先将渲染的内容输出到当前Surface中,但并不展示到屏幕上。
相当于把当前的Surface当作一个缓冲区,然后切换Surface,此时MediaCodec的Surface变成了当前Surface,接下来利用OpenGL3.0提供的API BlitFramebuffer从原来的Surface拷贝数据到当前Surface中,再调用EGL的eglSwapBuffers将Surface中的内容送编码器编码。
之后再将当前Surface切回原来的Surface,也就是SurfaceView的Surface,同样调用EGL的eglSwapBuffers方法,将其内容显示到屏幕上。
至此,实现了OpenGL仅渲染一次,却可以输出给多个目标,这种方法是最高效的。
小结
本文向你介绍了三种离屏渲染的方法,其中最高效的是BlitFramebuffer方法,但它是在OpenGL3.0才有的API,因此只有OpenGL3.0才可以使用该方法;其次是FBO方法,但其复杂度要比二次渲染法高很多,并且效率是有提高与原模型的复杂高有很大关系,如果原模型复杂度并不高,那么它与二次渲染法的效率并不多。
此外,本篇文章中还有很多细节并没有介绍到,比如如何使用FBO,如何在自己创建的渲染线程中构建EGL环境,如何使用BlitFramebuffer等,这些内容我将在后面的文章中再向你做详细讲解。
参考资料
《系统玩转OpenGL+AI,实现各种酷炫视频特效》
原文链接:https://zhuanlan.zhihu.com/p/676879474
-- END --
进技术交流群,扫码添加我的微信:Byte-Flow
获取相关资料和源码
推荐:
Android FFmpeg 实现带滤镜的微信小视频录制功能
全网最全的 Android 音视频和 OpenGL ES 干货,都在这了
面试官:如何利用 Shader 实现 RGBA 到 NV21 图像格式转换?
项目疑难问题解答、大厂内部推荐、面试指导、简历指导、代码指导、offer 选择建议、学习路线规划,可以点击找我一对一解答。