AR实践:基于ARKit实现电影中的全息视频会议

作者简介:龚宇华,声网Agora.io 首席iOS研发工程师,负责iOS端移动应用产品设计和技术架构。

去年中旬,苹果在 WWDC2017 推出了 ARKit。通过它,开发者可以更加快速地在 iOS 平台开发 AR 应用,利用镜头将虚拟照进现实。最近苹果还增强了 iOS 系统对 ARKit的支持,并将加大对 AR 应用的推广力度。

在本篇中,我们将会把 ARKit 融入视频会议场景中。本文将会介绍视频中两种场景的实现:

  • 将 ARKit 融入直播中

  • 将直播连麦的对方画面渲染到 AR 场景中

我们将一起在直播场景中利用 ARKit 实现平面检测,还将应用到 Agora SDK 2.1 的新功能“自定义视频源与渲染器”。如果你在此之前还未了解过 ARKit 的基本类及其原理,可以先阅读《上篇:ARKit 基础知识》。

不多说,先上效果图。尽管距离电影中看到的全息视频会议效果还有距离,但大家可以试着对后期效果优化无限接近电影场景(文末有源码)。我们在这里仅分享利用 AR 在视频会议中的实现技巧。

准备工作1:基础的AR功能

我们首先使用ARKit创建一个简单的识别平面的应用做为开发基础。

在Xcode中使用 Augmented Reality App 模版创建一个新项目,其中 Content Technology 选择 SceneKit.

启动平面检测

在 ViewController 中设置 ARConfiguration 为平面检测。

override func viewDidLoad() {
    super.viewDidLoad()

    sceneView.delegate = self
    sceneView.session.delegate = self

    sceneView.showsStatistics = true
}

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    let configuration = ARWorldTrackingConfiguration()
    configuration.planeDetection = .horizontal
    sceneView.session.run(configuration)
}

显示识别出的平面

实现 ARSCNViewDelegate 的回调方法 renderer:didAddNode:forAnchor: ,在识别出的平面上添加一个红色的面。

func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
    guard let planeAnchor = anchor as? ARPlaneAnchor else {
        return
    }

    // 创建红色平面模型
    let plane = SCNBox(width: CGFloat(planeAnchor.extent.x),
                       height: CGFloat(planeAnchor.extent.y),
                       length: CGFloat(planeAnchor.extent.z),
                       chamferRadius: 0)
    plane.firstMaterial?.diffuse.contents = UIColor.red

    // 用模型生成 Node 对象并添加到识别出的平面上
    let planeNode = SCNNode(geometry: plane)
    node.addChildNode(planeNode)

    // 渐隐消失
    planeNode.runAction(SCNAction.fadeOut(duration: 1))
}

这样就完成了一个最简单的AR应用,当识别出环境中的平面时,会在上面添加一个红色的矩形,并渐隐消失。

准备工作2:基础的直播功能

接下来我们需要使用 Agora SDK 在应用中添加直播功能。

首先在官网下载最新的 SDK 包并添加到我们的 Demo 中。接着在 ViewController 中添加 AgoraRtcEngineKit 的实例,并且进行直播相关的设置。

let agoraKit: AgoraRtcEngineKit = {
    let engine = AgoraRtcEngineKit.sharedEngine(withAppId: <#Your AppId#>, delegate: nil)
    engine.setChannelProfile(.liveBroadcasting)
    engine.setClientRole(.broadcaster)
    engine.enableVideo()
    return engine
}()

最后在 viewDidLoad 方法中加入频道。

agoraKit.delegate = self
agoraKit.joinChannel(byToken: nil, channelId: "agoraar", info: nil, uid: 0, joinSuccess: nil)

至此,所有的准备工作都已经完成,我们有了一个可以识别平面的AR应用,同时又可以进行音视频通话,接下来要做的就是把这两个功能结合起来。

将 ARKit 的画面直播出去

因为 ARKit 已经占用了设备摄像头,我们无法自己启动 AVCaptureSession 进行采集。幸好 ARFramecapturedImage 接口提供了摄像头采集到的数据可以供我们直接使用。

添加自定义视频源

为了发送视频数据,我们需要构造一个实现了 AgoraVideoSourceProtocol 协议的类 ARVideoSource 。其中 bufferType 返回 AgoraVideoBufferTypePixelBuffer 类型。

class ARVideoSource: NSObject, AgoraVideoSourceProtocol {
    var consumer: AgoraVideoFrameConsumer?

    func shouldInitialize() -> Bool { return true }

    func shouldStart() { }

    func shouldStop() { }

    func shouldDispose() { }

    func bufferType() -> AgoraVideoBufferType {
        return .pixelBuffer
    }
}

给这个 ARVideoSource 类添加一个发送视频帧的方法:

func sendBuffer(_ buffer: CVPixelBuffer, timestamp: TimeInterval) {
    let time = CMTime(seconds: timestamp, preferredTimescale: 10000)
    consumer?.consumePixelBuffer(buffer, withTimestamp: time, rotation: .rotationNone)
}

接着在 ViewController 中实例化一个 ARVideoSource, 并在 viewDidLoad 中通过 setVideoSource 接口设置给 Agora SDK

let videoSource = ARVideoSource()

override func viewDidLoad() {
    ……
    agoraKit.setVideoSource(videoSource)
    ……
}

这样在我们需要的时候,只要调用 videoSource 的 sendBuffer:timestamp: 方法,就可以把视频帧传给 Agora SDK 了。

发送摄像头数据

我们可以通过 ARSession 的回调拿到每一帧 ARFrame ,从中读出摄像头的数据,并使用 videoSource 发送出去。

viewDidLoad 中设置 ARSession 的回调

sceneView.session.delegate = self

实现 ARSessionDelegate 回调,读取每一帧的摄像头数据,并传给 Agora SDK 。

extension ViewController: ARSessionDelegate {
    func session(_ session: ARSession, didUpdate frame: ARFrame) {
        videoSource.sendBuffer(frame.capturedImage, timestamp: frame.timestamp)
    }
}

发送 ARSCNView 数据

ARFramecapturedImage 是摄像头采集到的原始数据,如果我们想发送的是已经添加好虚拟物体的画面,那就只能自己获取 ARSCNView 的数据了。这里提供一种简单的思路:设定一个定时器,定时去将 SCNView 转为 UIImage,接着转换为CVPixelBuffer,然后提供给 videoSource。下面只提供了示例逻辑代码。

func startCaptureView() {
    // 0.1秒间隔的定时器
    timer.schedule(deadline: .now(), repeating: .milliseconds(100))

    timer.setEventHandler { [unowned self] in
        // 将 sceneView 数据变成 UIImage
        let sceneImage: UIImage = self.image(ofView: self.sceneView)

        // 转化为 CVPixelBuffer 后提供给 Agora SDK
        self.videoSourceQueue.async { [unowned self] in
            let buffer: CVPixelBuffer = self.pixelBuffer(ofImage: sceneImage)
            self.videoSource.sendBuffer(buffer, timestamp: Double(mach_absolute_time()))
        }
    }

    timer.resume()
}

将直播连麦的对方画面渲染到 AR 场景中

我们可以先在 AR 场景中添加一个 SCNNode, 接着通过 Metal 把连麦对方的视频数据渲染到 SCNNode 上。这样即可实现在 AR 环境中显示连麦端的画面。

添加虚拟显示屏

首先我们需要创建用来渲染远端视频的虚拟显示屏,并通过用户的点击添加到 AR 场景中。

在 Storyboard 中给 ARSCNView 添加一个 UITapGestureRecognizer,当用户点击屏幕后,通过 ARSCNViewhitTest 方法得到在平面上的位置,并把一个虚拟显示屏放在点击的位置上。

@IBAction func doSceneViewTapped(_ recognizer: UITapGestureRecognizer) {
    let location = recognizer.location(in: sceneView)

    guard let result = sceneView.hitTest(location, types: .existingPlane).first else {
        return
    }

    let scene = SCNScene(named: "art.scnassets/displayer.scn")!
    let rootNode = scene.rootNode
    rootNode.simdTransform = result.worldTransform
    sceneView.scene.rootNode.addChildNode(rootNode)

    let displayer = rootNode.childNode(withName: "displayer", recursively: false)!
    let screen = displayer.childNode(withName: "screen", recursively: false)!

    unusedScreenNodes.append(screen)
}

用户通过点击可以添加多个显示屏,并被存在 unusedScreenNodes 数组中待用。

添加自定义视频渲染器

为了从 Agora SDK 获取到远端的视频数据,我们需要构造一个实现了 AgoraVideoSinkProtocol 协议的类型 ARVideoRenderer

class ARVideoRenderer: NSObject {
    var renderNode: SCNNode?
}

extension ARVideoRenderer: AgoraVideoSinkProtocol {
    func shouldInitialize() -> Bool { return true }

    func shouldStart() { }

    func shouldStop() { }

    func shouldDispose() { }

    func bufferType() -> AgoraVideoBufferType {
        return .rawData
    }

    func pixelFormat() -> AgoraVideoPixelFormat {
        return .I420
    }

    func renderRawData(_ rawData: UnsafeMutableRawPointer, size: CGSize, rotation: AgoraVideoRotation) {
        ……
    }
}

通过 renderRawData:size:rotation: 方法可以拿到远端的视频数据,然后就可以使用 Metal 渲染到 SCNNode 上。具体的 Metal 渲染代码可以参考文末的完整版 Demo.

将自定义渲染器设置给 Agora SDK

通过实现 AgoraRtcEngineDelegate 协议的 rtcEngine:didJoinedOfUid:elapsed: 回调,可以得到连麦者加入频道的事件。在回调中创建 ARVideoRenderer 的实例,把前面用户通过点击屏幕创建的虚拟显示屏 Node 设置给 ARVideoRenderer,最后通过 setRemoteVideoRenderer:forUserId: 接口把自定义渲染器设置给 Agora SDK。

func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
    guard !unusedScreenNodes.isEmpty else {
        return
    }

    let screenNode = unusedScreenNodes.removeFirst()
    let renderer = ARVideoRenderer()
    renderer.renderNode = screenNode

    agoraKit.setRemoteVideoRenderer(renderer, forUserId: uid)
}

这样当连麦端加入频道后,就会在虚拟显示屏上显示对方的视频,得到一个虚拟会议室的效果,正如我们在文章开头所看到的。

总结

用最新 2.1 版 Agora SDK 的自定义视频源和自定义视频渲染器接口,可以轻松地把 AR 和直播场景结合起来。Demo 基于 Agora SDK 以及 SD-RTN™ 运行,可以支持17人的同时视频连麦。可以预见,AR 技术会为实时视频连麦带来全新的体验。

完整 Demo 请见 Github:https://github.com/AgoraIO/Agora-Video-With-ARKit

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值