跨平台渲染引擎之路:框架与核心模块
前言
说好的保持一月一更再次食言了,再也不敢随便立Flag了,这段时间只能在业余时间进行 GPUImage-X 的设计和第一版的落地,除了时间因素之外,还有就是想要把第一版的整体框架和基础能力都能以更编码规范、考虑更充分地落地下来,不仅仅是一个小玩具更是一个可以被使用的渲染库,因此在过程中就会出现返工重复调整等情况。
目前 GPUImage-X 的整体框架中各个层级的主要模块已基本落地,整体设计上在个人来看符合我们上一篇中的部分目标:以游戏引擎的效果和特性,支持图片/视频等场景下的跨平台3D渲染库。在功能上,该库落地了图层、滤镜链、变换、包括转场、遮罩、叠加在内的混合模式等基本功能,同时不管是更多样的图层元素还是粒子、光照等更广泛的效果都也已具备了对应扩展性。在项目里配套了一个可调节上述能力参数效果的Demo,不过目前只实现了Mac上的版本,其他平台的在下一步规划里会优先安排上,确保跨平台可用这一点。
那么接下来这篇文章主要会介绍这个过程中自己的一些想法还有踩过的一些坑,有存在问题的地方期待看的大佬们交流纠正~
站在bgfx的肩膀上
首先简单说一下为什么一开始先基于bgfx来搭建这个项目,一方面是在于时间成本上,自己从头开始撸一个渲染API封装的框架的话是很耗时的,会导致看到效果的周期被不断拉长,难以快速得到反馈和验证,另一方面就是对于各个平台的渲染API目前还并未完全掌握,即使去写也肯定是需要借鉴甚至照搬bgfx的设计,很难有所超越。因此在目前的阶段先选择基于bgfx之上来进行开发和学习,重点去理解bgfx的设计理念和实现思路,记录下踩过的坑和自己感觉使用不便、可以完善优化的一些地方,作为后续自己开发渲染框架的基础,这部分后续在开始落地时也会再有文章来进行记录和分享。
要依赖bgfx来进行二次开发还是有一定难度的,光配置依赖关系就折腾了一会。目前是借鉴于 bgfx.cmake 的思路,将其配置好的CMakeList 以及 .cmake 文件迁移到了 GPUImage-X 的项目中并且做了一些定制化修改(当然会不断同步更新的),比如 bgfx.cmake 中是自己搞了一个App类来作为Demo运行基类, GPUImage-X 则直接使用了 bgfx 的entry以及自带的工具,避免重复的编码工作。具体的依赖配置可以在运行项目还有根据各个层级下的 cmake 配置内容查看。
解决完依赖问题后,在开发过程中也是踩了bgfx的各种坑,当然其中很大部分原因是因为自己对bgfx的使用方式、内部逻辑原理等不够熟悉导致的,比如view的265最大数限制(这个View的机制导致了目前渲染还需要有一个全局的计数器来控制每次submit的index),同一个uniform在一轮渲染中不得重复设置,多纹理的设置等等。
设计思路
接下来介绍一下目前 GPUImage-X 项目中的整体设计思路以及各个核心模块。
整体设计
首先贴一下一个简要的层级和模块设计图:
首先访问层就没什么好说的,展示层指的是比如Android上的GLSurfaceView之类的承载我们渲染结果的视图,目前bgfx有一套创建跨平台渲染环境和Surface的代码,但是看网上还是挺多人在询问移动端如何使用和接入的,因此对这套代码在移动端上的应用情况还是存在顾虑(可用但是希望能够易用),因此如果下一步在做Android/iOS双端的接入时发现的确存在问题,那么就会先封装对应平台原生的展示控件,然后等功能验证完毕后进一步自己实现跨平台的渲染环境和Surface创建(感觉又是一个可以学习分享的大坑-_-)。
使用层是使用者可直接感知的一些模块,目前主要的就是图层和效果这两个模块。处理层是根据用户设置的图层以及其上的叠加效果等内容,内部自行封装的各个业务逻辑模块,因为现在项目整体的架构和代码量都还是很轻量的,因此将和效果处理相关的内容都统一放到这一层,后续再根据情况考虑是否进一步拆分。
除了架构设计,在项目结构上也划分了以下几个目录结构:
- thirdparty:存放第三方依赖项目以及相关配置,包括bgfx、glfw等
- examples:存放各种Demo
- shaders:存放着色器源代码
- source:源代码存放路径
- include:对外头文件存放路径
- tools:存放一些如快速编译着色器等工具
在外部接入使用上,还有封装了一个 XImage 来作为对外统一添加图层、控制渲染流程的门面,后续在进一步降低使用成本与风险里面,应该会将其作用范围进一步扩大。
帧 XFrameBuffer
在 GPUImage-X 项目里有一个 XFrameBuffer 类,该类表示一帧的渲染结果,被设计用于封装所有的输入和输出,比如图片/视频纹理除了加载成Texture之外还会被进一步加载到一个FBO中封装成 XFrameBuffer ,而我们每一个效果的渲染结果也都必将渲染到一个自身的 XFrameBuffer 里,这样做的好处是统一了各个模块的入口和出口,同时在我们做缓存池的时候也会更简单一些。
目前项目里有一个 XFrameBufferPool 的缓存池,项目中所有 XFrameBuffer 的获取和回收都需要经过该缓存池,目前的逻辑比较简单,只具备简单的缓存和回收能力,后续会进一步完善。
图层 XLayer
首先回到我们之前说的该项目主要面向的场景是图片/视频下的,因此需要针对该类型场景有一个总结性的提炼,这样更能有所针对性地进行设计。
在视频/图片类型场景下,类比PS/AE之类的设计工具会发现,里面如图片、文字等元素都是以图层的概念存在,所有图层有其公共和特有属性,在图层之上我们可以再去额外叠加一系列的效果,图层和图层之间也会产生相互影响。借鉴于此,我们抽象出图层 XLayer 来作为外部操作控制的对象,该基类负责如渲染区域、渲染层级以及效果叠加等通用属性和业务逻辑,而如图片/视频帧数据则可以封装成 XFrameLayer 这样的子类,持有其特有属性如图片路径、像素数据等,还有其特有逻辑如加载图片到 XFrameBuffer 中等,再进一步预想比如后面还有其他类型图层,文字、矢量等也可以通过这样策略模式的方式来进行实现。
在 XLayer 里,每一个图层都首先必须有一个自身的输出,如 XFrameLayer 就是图片/视频的加载结果,在这些源输出的基础上才会去叠加各类效果,进行各项处理。而图层和图层之间是可以互相影响的,这里借鉴于AE,图层可以设置自身的图层内遮罩 Matte ,Matte 也是一个个的图层,只是这些图层只能用作对应图层的遮罩,不参与全局的图层混合,并且 Matte 目前出于简化逻辑考虑,暂时不可再 Matte 图层上叠加 Matte 也不可添加额外的效果,只能进行基本的绘制和动画(下一阶段实现的内容之一)。除了图层内遮罩 Matte 之外,还可以对图层设置图层间遮罩 Mask ,Mask 除了与对应图层进行抠图混合之外,还可以选择是否参与最终的全局混合。
效果 XEffect
在项目里我们将各类效果都封装成是基类 XEffect 的各个子类,XEffect 就代表了可叠加在各类图层上的各种效果,如滤镜、变换、粒子、混合等等,XEffect 除了提供公共接口外目前没有太多实现内容,主要的逻辑都由不同的效果子类来实现,如 XFilter 代表了所有的滤镜效果,XMixer 继承自 XFilter 但是主要负责混合相关的多输入效果,后续如粒子效果则会相比这两者更复杂一些,因为它需要考虑将自身的粒子效果与上一次的渲染结果进行叠加之类等额外逻辑。
而各个效果都会有专用或者共用的处理器,目前暂时都为公用的,一个是 XEffectProcessor ,负责处理滤镜等单输入着色器,一个是 XTwoInputEffectProcessor ,则是负责处理混合等双输入着色器,当然这一层对于使用者来说是不可感知的。
输入/输出 XInput/XOutput
输入输出模块对于使用层来说也是不可感知的模块,相比效果处理器还要更底下一层,该模块是所有链式结构的基础。输出的基类 XOuput ,继承自该类的所有子类都需要确保自身可以产出一个存储在XFrameBuffer中的输出结果,而输入的基类为 XInput ,继承自该类的所有子类都可以接收一个外部传入的 XFrameBuffer 作为输入,同时自身的处理逻辑是基于这个输入来进行的。
输入输出模块是借鉴与 GPUImage 中的输入输出模块的,输出是整个效果链条的开端,它可以是一个图片输出也可以是一个视频输出(这在后面可以让我们在视频输出中加入 FFmpeg 解码相关的内容),而输入是效果链条的结束,它对外部传入的输入帧进行效果处理,而当我们同时继承自XInput/XOuput时,那么如滤镜等效果就可以既处理外部传入的帧数据(图片、上一次渲染结果等),也可以将自身的渲染结果存储下来作为下一轮渲染的输入。
在 GPUImage 的设计中,每一个 Output 可以通过 addTarget(Input) 来添加一系列的处理该 Output 输出结果的输入对象,但是通过研究其内部实现,会发现这些Target都是处于并行状态的,也就是说不断通过 addTarget 添加的输入对象,他们都是在处理同一个输入(即 Output 的输出)而不是处理前一个被 add 的 Input 的结果。这个也是很容易理解的,因为毕竟这个接口声明的是 Input 对象,而 Input 对象是没有 addTarget 这样的接口的。因此如果需要形成链式结果需要外部自己进行额外的 addTarget 等操作,但是这种方式的扩展性其实是更强的,一个输出可以产生多个处理链条。这里提一下是因为目前项目中也是借鉴这样的设计思路的,避免产生理解上的歧义(至少本人在一开始研究GPUImage的时候就理解错了的-_-)。
范例 Examples
目前项目里已经有一个 X-Image 的Demo,该Demo主要是提供项目中已有的各项能力的展示以及参数调节,参数调节界面是借助于 imgui 实现的,目前用下来感觉还比较顺手,在Demo目录下面也针对各个能力的参数设置、界面展示都做了封装,可以通过这些封装后的类的实现来参考各个效果如何设置和参数规则。
后续还会针对一些效果如粒子、动画系统等搭建额外的Demo进行展示,避免一个Demo里承载太多内容,失去参考意义。
内存管理
目前在内存管理上秉承的原则就是谁创建谁管理,比如 XLayer、XEffect 这些是由使用者创建和设置的,那么就由使用者来进行管理,而 XMixer 等对象创建是内部进行,则由内部来统一管理,涉及到内存管理相关的都会在对应的地方添加注释和声明由谁负责其生命周期等内容,后续会考虑如何通过只设置一些参数,所有对象创建和回收都由内部进行,降低使用成本和风险。
下一步
目前的规划主要分三个方面:能力的不断扩充,框架的不断完善以及文档的补充,现在主要还是自己给自己提需求,如果大家有不同的诉求的话,欢迎在评论或者项目issue里提出。
能力扩充:
- GPUImage 中以及目前流行滤镜效果
- 粒子系统
- 光照
- 文字图层
- 多边形图层
- 更多3D效果
框架完善:
- Android/iOS 平台支持(aar/pod)
- 进一步降低使用成本与风险
- 动画系统
- 手势处理
文档补充:
- 使用手册
- 其他
最近发现还有很多放着积了很久灰的书要看,但是还是会尽量保证算上周末平均每天2-3小时的时间来进行编码(这算Flag吗-_-),并且在一个阶段一个阶段比如某些较复杂效果、较大的框架落地等节点上更新自己的一些经验和踩坑记录,至少避免出现长时间的断更,目标是希望能在今年内把上面的规划都落地下来。