对特效相机的重构方案

615 阅读6分钟

存在的问题:

  • 1.使用GPUImage第三方框架,对图片进行渲染, 底层是基于OpenGL实现的
  • 2.链式渲染的方式,多张图片或者多个结果需要依赖的时候,需要自己编写大量着色器代码, GPUImage只适用于单张图片或者渲染操作数很少的需求
  • 3.后续复杂的功能比如特效,小视频等 GPUImage无法满足我们的业务需求
  • 4.使用过程中手机发热,耗电量也增加(GPU和CPU负载过重)

探索重构方案:

使用OpenGL和metal来做,但是两个哪个最好呢?

WWDC2018大会上面 苹果指明了OpenGL将在Mac OS10.14弃用, OpenGL ES将在iOS上弃用,虽然现在API还能访问,但是已经被标记了弃用的API很有可能在未来的某一刻被抹去
OpenGL: 不支持多线程操作, 不支持异步处理, OpenGL 本身设计上存在的问题已经影响了 GPU 真正性能的发挥
Metal 简化了 CPU 参与渲染的步骤,尽可能地让 GPU 去控制资源。与此同时,拥有更现代的设计,使操作处于可控,结果可预测的状态。在优化设计的同时,它仍然是一个直接访问硬件的框架。与 OpenGL 相比,它更加接近于 GPU,以获得更好的性能。
在 MacOS 10.14 的文档中,苹果表示使用 OpenGL 和 OpenCL 构建的应用还可以继续在 macOS 10.14 中运行(但是即使 macOS 支持 OpenGL ,其内置版本依然是 8 年前发布的 OpenGL 3.3 ,而不是去年发布的 4.6)。苹果表示,这些“遗留技术”并不推荐使用。 很明显,苹果此举是想要大力推广 Metal 图形技术,来替换掉“古老”的 OpenGL 接口和 OpenCL 接口。

大家复盘了一下,发现我们团队的图像处理方面的知识储备基本上是0,基于这个考虑我们选择了metal, 因为metal比OpenGL有优势

针对GPUImage存在的弊端以及限制, 制定了metal替代的方案以及需要达到目标

1.metal实现基本的图片渲染和处理 2.metal实现复杂的动效以及特效处理 3.封装一个完整的组件,使得渲染业务和UI业务解耦 4.为了更好的扩展,制定了一套渲染规则和参数,行程json文件,方便为了渲染方式的扩充, 可以服务器下发也支持本地配置

针对耗电发热问题,通过review代码发现了2个问题

1.渲染的过程效果需要保存到本地, 沿用了之前的模式, 通过使用CoreGraphics对渲染结果显示的view多次截图的方式,最后将一张纸图片合成MP4文件
2.渲染效果没有做缓存,也就是每次点击的时候需要重复进行渲染

解决方案:

1.用渲染生成的纹理替代截图的方式
2.对渲染的效果进行缓存

这里有一个问题, 怎么将纹理转为渲染过程的mp4? 怎么做缓存?

纹理转CVPixelBuffer写入本地的方案研究:

ReplayKit:

API:提供开始录制方法和结束录制
问题:
1.当开始录制方法被调用时用户会收到权限请求警告,这个警告每次会在开始录制时出现。然而,一旦用户选择了其中一种偏好设置,系统会在接下来的8分钟记住这个选择。
2.会录制整个屏幕,而我们的渲染效果显示的只是屏幕中间的一小个view, 会导致多余的内容被录制上

那怎么办呢?能不能从视频播放哪里找到一点启发呢?

想到之前做播放器的时候用了MPMoviePlayerController,但是因为他是苹果高度封装的类,对我们使用很方便,可自定义性越受限制,所以就做一个属于我们的播放器吧,AVAssetReader可以从原始数据里获取解码后的音视频数据,结合AVAssetReaderTrackOutput ,能读取一帧帧的CMSampleBufferRef 。CMSampleBufferRef 可以转化成CGImageRef 。 这是视频的读取流程,  那是不是应该有一个将这个流程反过来的类呢?

带着这些疑问, 又重新找了找苹果的API, 最后找到了AVAssetWriter这个类, 可以将CVPixelBuffer类型的数据,直接写入到指定的文件, 怎么将metal渲染的纹理数据MTLTexture 转换为CVPixelBuffer? 苹果是提供了两者相互转换的方法

显示方式:

渲染完再输出显示, 还是边渲染边显示?

1.渲染完再显示,因为一整个渲染过程已经结束

   1.优点:

  • 渲染流程只用一次, 结果显示的时候可以取本地保存的过程视频直接播放
  • 可以减少GPU的资源消耗,节省电量,改善发热的问题
  • 可以不用限制用户对保存按钮的点击, 用户可以直接点击保存, 然后转存到相册    2.缺点需要耗费少量时间等待渲染完成再显示

2.边渲染边写入:

   1.用户可以不用等待

   2.缺点是如果用户一直停留在同一个页面上,会重复进行渲染,因为有些特效是循环的效果

两种方案都支持了, 做了线上的灰度测试, 发现渲染完在显示的方案, 需要等待的时间,95%以上线上用户在0.5-1.5s时间,所以我们考虑采用渲染完再显示的方案, 这样做的优点是可以节省更多电量

重构具体实现:
1.因为顶点函数和片元函数与渲染的结果绑定很紧密,所以我们近把目前支持的.metal文件保留, 特定功能需要自己实现顶点函数和片元函数
2.使用类簇的设计模式,将对外的接口封装在一个父类上面,接受一个mtkView(也就是我们渲染效果需要显示的view),返回一个实例对象

  • 1.初始化方法
  • 2.设置顶点坐标和纹理坐标的方法
  • 3.设置纹理数据的方法
  • 4.设置渲染管道的方法(内部依赖设置顶点函数和片元函数)
  • 5.通过提前订好的协议json文件, 来决定片元函数和顶点函数需要的其他参数(用于生成不同的图像处理算法)
  • 6.创建命令缓冲区, 并将需要的命令添加到缓冲区
  • 7.通过一个block将命令返回给客户端commondBuffer
  • 8.客户端执行commondBuffer就可以执行渲染操作了
  • 9.里面还实现了一些其他方法:比如支持直接将纹理写入本地沙盒路径,纹理转图片等操作

3.每一种渲染操作都会提供一个子类出去,如果需要定制的话,客户端只需要在子类里面重写对应的方法,加入自己的操作就行
4.如果需要新的渲染特效,只需要重新定义一个子类,重写方法实现