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
都会回调,查看ARFrame
的capturedImage
是记录每次摄像机采集的画面,那我们猜测每次ARSession
处理完会传给ARSCNView
,ARSCNView
获取ARFrame
的capturedImage
来渲染,如果可以在系统获取capturedImage
之前修改为添加滤镜后的数据,那系统就会渲染添加滤镜后的结果。
验证
我们新建ARSession
的Extension
,在+(void)load
方法中交换系统的-(ARFrame *)currentFrame()
为-(ARFrame *)_currentFrame()
方法,在-(ARFrame *)_currentFrame()
中返回原始内容,运行后在这个方法中添加断点
查看调用的方法为-[ARSCNView _renderer:updateAtTime:]
,看方法名像是渲染的方法
在-(ARFrame *)_currentFrame()
中将capturedImage
的所有内存数据设置为0,重新运行,发现屏幕为黑屏,验证了我们的猜测
实践
我们使用GPUImage
来处理capturedImage
,建立一个GPUImageFilterPipeline
,输入为GPUImageMovie
,用来接收capturedImage
的CVPixelBuffer
数据,输出为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()
}
运行后发现崩溃了,查找原因发现capturedImage
的CVPixelBuffer
格式是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