ARKit是苹果在2017年推出的AR开发平台,开发人员可以使用这套工具iPhone和iPad创建增强现实应用程序。

本文以ARKit提供的ARSCNView为例实现给AR背景添加滤镜。

先放代码 Add Filter To ARKit

平常开发主要使用ARKit中的ARSCNView,或者使用iOS13新推出的RealityKit,当然也可以使用Metal或者OpenGL自己渲染。如果是自己渲染,整个的渲染过程都是自己控制的,可以方便的添加滤镜,但是需要繁琐的渲染代码和3D数学知识。使用系统提供的控件,好处是我们不需要关注渲染的过程和控制摄像机的运动,只需要关注业务逻辑,但是系统并没有给我们暴露自定义渲染的方法。

思路

我们知道ARSession这个类是ARKit的核心,他负责摄像头数据的采集,分析,和处理,从硬件中读取手机姿态等信息,综合所有结果,在你的现实世界和创建的虚拟世界之间创建一个对应的关系,从而构建出一个AR世界。

我们从ARSession入手查找发现ARSessionDelegate有一个方法

/**
 This is called when a new frame has been updated.
 
 @param session The session being run.
 @param frame The frame that has been updated.
 */
optional func session(_ session: ARSession, didUpdate frame: ARFrame)

这个方法会在每次有新ARFrame都会回调,查看ARFramecapturedImage是记录每次摄像机采集的画面,那我们猜测每次ARSession处理完会传给ARSCNViewARSCNView获取ARFramecapturedImage来渲染,如果可以在系统获取capturedImage之前修改为添加滤镜后的数据,那系统就会渲染添加滤镜后的结果。

验证

我们新建ARSessionExtension,在+(void)load方法中交换系统的-(ARFrame *)currentFrame()-(ARFrame *)_currentFrame()方法,在-(ARFrame *)_currentFrame()中返回原始内容,运行后在这个方法中添加断点

查看调用的方法为-[ARSCNView _renderer:updateAtTime:],看方法名像是渲染的方法
-(ARFrame *)_currentFrame()中将capturedImage的所有内存数据设置为0,重新运行,发现屏幕为黑屏,验证了我们的猜测

实践

我们使用GPUImage来处理capturedImage,建立一个GPUImageFilterPipeline,输入为GPUImageMovie,用来接收capturedImageCVPixelBuffer数据,输出为GPUImageFilter,用来输出处理完的数据,因为是异步处理的,所以我们添加一个DispatchSemaphore锁,等待GPUImage处理完,处理完后将数据复制回capturedImage

 let output = GPUImageFilter()
 
 pipeline = GPUImageFilterPipeline(orderedFilters: [], input: input, output: output)
 
 output.frameProcessingCompletionBlock = { [weak self] (output, time) -> Void  in
     let frameBuffer = output?.framebufferForOutput()
     
     guard let buffer = frameBuffer else {
         return;
     }
     
     glFinish()
     
     self?.rgbBuffer = buffer.getRenderTarget()?.takeUnretainedValue()
     
     self?.semaphore.signal()
 }

运行后发现崩溃了,查找原因发现capturedImageCVPixelBuffer格式是yuv的,而GPUImage处理完的数据为rgb的,所以我们添加libyuv库,把GPUImage处理完的rgb数据转成yuv,在复制回capturedImage

CVPixelBufferLockBaseAddress(pixelBuffer, [])
let final_y_buffer = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0)?.assumingMemoryBound(to: uint8.self);
let final_uv_buffer = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1)?.assumingMemoryBound(to: uint8.self);
input.processMovieFrame(pixelBuffer, withSampleTime: .zero)
_ = semaphore.wait(timeout: .distantFuture)
CVPixelBufferLockBaseAddress(rgbBuffer!, [])
let width = CVPixelBufferGetWidth(rgbBuffer!)
let height = CVPixelBufferGetHeight(rgbBuffer!)
let rgbAddress = CVPixelBufferGetBaseAddress(rgbBuffer!)?.assumingMemoryBound(to: uint8.self)
ARGBToNV12(rgbAddress, Int32(width*4), final_y_buffer, Int32(width), final_uv_buffer, Int32(width), Int32(width), Int32(height))
CVPixelBufferUnlockBaseAddress(rgbBuffer!, [])
CVPixelBufferUnlockBaseAddress(pixelBuffer, [])

这样我们就可以随意在GPUImageFilterPipeline添加或删除滤镜了

测试后发现如果滤镜切换特别快会有随机崩溃,猜测是切换过程影响到最后的数据,修改为等待上一帧处理完再切换滤镜

总结

本文通过比较取巧的方式实现了这个需求,有点不走寻常路。采用Unity等现成的渲染引擎会是一个相对稳妥的方案,集成也很简单(参考之前的文章 Use Unity as Library),直接给摄像机添加滤镜脚本就行了

查看全部代码请点击 Add Filter To ARKit