在真实世界平面上放置虚拟物体,并通过手势让用户可以与虚拟内容交互。

代码下载


概述

AR体验的关键方面是能够混合虚拟和现实世界中物体的能力。一个平整的表面是放置虚拟物体的最好位置。为了辅助ARKit找到表面,应告知用户移动他们的设备来帮助ARKit准备该体验。ARKit提供了一个视图来为用户显示引导信息,引导他们移动手机视图到app需要的表面上。

为了让用户可以通过点击屏幕放置虚拟物体到真实世界表面上,ARKit整合了射线投射功能,该功能提供了一个与平面点击位置相对应的物理空间中的3D位置。当用户旋转或移动他们放置的虚拟物体时,你应该对每个手势进行相应并将该输入关联到物理环境中的虚拟内容的展示情况上。

笔记

ARKit要求iOS设备的处理器不低于A9。ARKit是无法在iOS模拟器中运行的

设置指导用户移动的目标

为了让你的用户可以检测真实世界的表面,你需要使用世界追踪的配置项。为了ARKit可以建立追踪流程,用户必须实体移动他们的设备来让ARKit获取透视的场景。为了与用户沟通该需求,可以通过ARKit提供的展示了教学图和语言指导的视图,即ARCoachingOverlayView 。例如,当你启动app的时候,用户看到的第一件事就是指导叠加视图的信息和动画来告知他们重复性的左右移动他们的设备,为开始做准备。

为了让用户可以在水平的表面放置虚拟物体,你需要对应的设定指导叠加视图的目标。

func setGoal() {
    coachingOverlay.goal = .horizontalPlane
}

指导叠加视图将会根据你选择的目标呈现出指导信息。ARKit一旦获取到场景的透视,指导叠加视图便会指导用户找到一个表面。

响应指导事件

为了保证指导叠加视图无论ARKit决定是否必要都为用户提供引导,你需要设置activatesAutomatically 的值为true。

func setActivatesAutomatically() {
    coachingOverlay.activatesAutomatically = true
}

指导叠加视图会在app启动或者追踪状态降级至某个阈值时自动启动。在这些情况下,ARKit会通过调用coachingOverlayViewWillActivate(_:) 方法来告知你的代理方法。在这个事件的相应中,你需要通过隐藏app的UI,使用户把注意力放在指导叠加视图提供的引导上面。

func coachingOverlayViewWillActivate(_ coachingOverlayView: ARCoachingOverlayView) {
    upperControlsView.isHidden = true
}

当指导叠加视图确定目标已经达成,它将会从用户视图中消失。ARKit将会告知你的代理方法引导流程已经结束了,这也是app的主用户界面应该展示的时机。

func coachingOverlayViewDidDeactivate(_ coachingOverlayView: ARCoachingOverlayView) {
    upperControlsView.isHidden = false
}

放置虚拟内容

为了能让用户知晓在什么地方可以防止虚拟内容,可以在环境中添加注释来为用户提供预览。示例代码绘制了一个正方形来给予用户视觉上的确认,确认ARKit识别到的表面的形状和对齐方向。

为了找出在真实世界中的哪个位置放置这个正方形,可以使用ARRaycastQuery 方法来询问ARKit真实世界中的平面都存在于哪里。首先,你创建一个射线投射查询,该查询定义了你再屏幕上感兴趣的一个2D点。由于焦点正方形是与屏幕中心对齐的,所以你应该为屏幕中心创建一个查询方法。

func getRaycastQuery(for alignment: ARRaycastQuery.TargetAlignment = .any) -> ARRaycastQuery? {
    return raycastQuery(from: screenCenter, allowing: .estimatedPlane, alignment: alignment)
}

接下来,通过会话来执行射线投射查询。

func castRay(for query: ARRaycastQuery) -> [ARRaycastResult] {
    return session.raycast(query)
}

ARKit通过结果参数返回一个位置,该位置包含了该点在真实世界中某个平面上所处位置的深度。为了给用户提供真实世界表面上哪个位置可以放置他们的虚拟内容的预览,使用射线投射查询的结果的worldTransform 来更新焦点方形。

func setPosition(with raycastResult: ARRaycastResult, _ camera: ARCamera?) {
    let position = raycastResult.worldTransform.translation
    recentFocusSquarePositions.append(position)
    updateTransform(for: raycastResult, camera: camera)
}

射线投射的结果表面是如何相对于重力形成角度的。为了预览用户的虚拟内容可以被放置在平面的哪个角度,可以使用结果的方向来更新焦点方形的simdWorldTransform

func updateOrientation(basedOn raycastResult: ARRaycastResult) {
    self.simdOrientation = raycastResult.worldTransform.orientation
}

如果你的app提供了多种不同类型的虚拟内容,你应该提供给用户一个表单来供用户选择。当用户点击加号按钮时,示例代码将会显示一个选择擦弹。当用户从列表中选择一个物体时,实例化对应的3D模型并将其锚定在焦点方形的当前位置。

func placeVirtualObject(_ virtualObject: VirtualObject) {
    guard focusSquare.state != .initializing, let query = virtualObject.raycastQuery else {
        self.statusViewController.showMessage("CANNOT PLACE OBJECT\nTry moving left or right.")
        if let controller = self.objectsViewController {
            self.virtualObjectSelectionViewController(controller, didDeselectObject: virtualObject)
        }
        return
    }

    let trackedRaycast = createTrackedRaycastAndSet3DPosition(of: virtualObject, from: query,
                                                              withInitialResult: virtualObject.mostRecentInitialPlacementResult)

    virtualObject.raycast = trackedRaycast
    virtualObjectInteraction.selectedObject = virtualObject
    virtualObject.isHidden = false
}

逐渐细化虚拟内容的位置

随着会话运行,ARKit分析每个摄像头图像并了解到更多的有关物理环境的层的信息。当ARKit更新了它对于真实世界表面的预估大小和位置后,你可能需要更新你的app的虚拟内容的位置来与之对应。为了让这个操作更简单,ARKit通过方法ARTrackedRaycast 来通知你什么时候它修正了对场景的理解。

func createTrackedRaycastAndSet3DPosition(of virtualObject: VirtualObject, from query: ARRaycastQuery,                                          withInitialResult initialResult: ARRaycastResult? = nil) -> ARTrackedRaycast? {
    if let initialResult = initialResult {
        self.setTransform(of: virtualObject, with: initialResult)
    }

    return session.trackedRaycast(query) { (results) in
        self.setVirtualObject3DPosition(results, with: virtualObject)
    }
}

ARKit会持续运行你提供给可追踪的射线投射的查询,它只有在当前结果与之前结果不同时才会调用你提供的闭包。你再闭包中提供的代码是为了响应ARKit对于更新后的场景的理解。这种情况下,你需要检查你的射线投射的对于更新后的平面的交点,并将其位置应用到你的app的虚拟内容上。

private func setVirtualObject3DPosition(_ results: [ARRaycastResult], with virtualObject: VirtualObject) {

    guard let result = results.first else {
        fatalError("Unexpected case: the update handler is always supposed to return at least one result.")
    }

    self.setTransform(of: virtualObject, with: result)

    // If the virtual object is not yet in the scene, add it.
    if virtualObject.parent == nil {
        self.sceneView.scene.rootNode.addChildNode(virtualObject)
        virtualObject.shouldUpdateAnchor = true
    }

    if virtualObject.shouldUpdateAnchor {
        virtualObject.shouldUpdateAnchor = false
        self.updateQueue.async {
            self.sceneView.addOrUpdateAnchor(for: virtualObject)
        }
    }
}

管理追踪的射线投射

由于ARKit持续调用,随着用户放置更多的虚拟内容,被追踪的射线投射将会持续增加资源的消耗。当你不再需要持续优化方向的时候记得停止追踪射线投射,例如,当一个虚拟的气球飞走了,或者当你从你的场景中移除一个虚拟物体时。

func removeVirtualObject(at index: Int) {
    guard loadedObjects.indices.contains(index) else { return }

    // Stop the object's tracked ray cast.
    loadedObjects[index].stopTrackedRaycast()

    // Remove the visual node from the scene graph.
    loadedObjects[index].removeFromParentNode()
    // Recoup resources allocated by the object.
    loadedObjects[index].unload()
    loadedObjects.remove(at: index)
}

为了停止被追踪的射线投射,你需要调用它的stopTracking() 方法:

func stopTrackedRaycast() {
    raycast?.stopTracking()
    raycast = nil
}

启用用户与虚拟内容的交互

为了允许用户在世界中移动已放置的虚拟内容,需要实现一个移动手势识别方法:

func createPanGestureRecognizer(_ sceneView: VirtualObjectARView) {
    let panGesture = ThresholdPanGesture(target: self, action: #selector(didPan(_:)))
    panGesture.delegate = self
    sceneView.addGestureRecognizer(panGesture)
}

当用户移动一个物体时,你需要询问其在平面上移动路径的位置。因为物体的位置是暂时的,需要使用raycast(_:) 代替被追踪的射线投射。在这情况下,一个一次性的撞击测试是合适的,因为你不需要持续优化对于这些请求的位置结果。

func translate(_ object: VirtualObject, basedOn screenPos: CGPoint) {
    object.stopTrackedRaycast()

    // Update the object by using a one-time position request.
    if let query = sceneView.raycastQuery(from: screenPos, allowing: .estimatedPlane, alignment: object.allowedAlignment) {
        viewController.createRaycastAndUpdate3DPosition(of: object, from: query)
    }
}

射线投射可以给你关于屏幕给定点的方向信息。当进行拖拽时,可以通过 从当前物体的旋转中减去手势的旋转来避免方向的过快变化。

@objc
func didRotate(_ gesture: UIRotationGestureRecognizer) {
    guard gesture.state == .changed else { return }

    trackedObject?.objectRotation -= Float(gesture.rotation)

    gesture.rotation = 0
}

处理追踪的中断

当出现追踪条件较差的情况时,ARKit会调用代理方法的sessionWasInterrupted(_:) 方法。在这种情况下,app的虚拟内容可能相对于摄像头信息流的位置是不准确的,所以这时应该隐藏虚拟内容。

func hideVirtualContent() {
    virtualObjectLoader.loadedObjects.forEach { $0.isHidden = true }
}

当追踪条件提高之后恢复应用的虚拟内容。为了将条件提升告知你,ARKit调用你的代理方法的session(_:cameraDidChangeTrackingState:) 方法,传入参数trackingState 等于 ARTrackingStateNormal

func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
    statusViewController.showTrackingQualityInfo(for: camera.trackingState, autoHide: true)
    switch camera.trackingState {
    case .notAvailable, .limited:
        statusViewController.escalateFeedback(for: camera.trackingState, inSeconds: 3.0)
    case .normal:
        statusViewController.cancelScheduledMessage(for: .trackingStateEscalation)
        showVirtualContent()
    }
}

恢复一个中断了的AR体验

当一个会话被中断之后,ARKit将会询问你是否想尝试恢复AR体验。你可以通过覆盖sessionShouldAttemptRelocalization(_:) 方法并返回true来实现重定位。

func sessionShouldAttemptRelocalization(_ session: ARSession) -> Bool {
    return true
}

在重定位过程中,指导叠加视图将会为用户展示引导内容。为了让用户将注意力集中在指导过程上,在指导启用后隐藏你的app的UI。

func coachingOverlayViewWillActivate(_ coachingOverlayView: ARCoachingOverlayView) {
    upperControlsView.isHidden = true
}

当ARKit成功的回复了体验之后,重新展示你的app的UI,因此所有的物体都展现在其中断前的位置。当指导叠加视图从用户视图中消失时,ARKit将会调用你的coachingOverlayViewDidDeactivate(_:) 回调,此时你便可以恢复你的app的UI。

func coachingOverlayViewDidDeactivate(_ coachingOverlayView: ARCoachingOverlayView) {
    upperControlsView.isHidden = false
}

允许用户重新开始而不是恢复

如果用户决定放弃恢复会话,可以通过代理方法的coachingOverlayViewDidRequestSessionReset(_:) 来重启体验。当用户点击知道叠加视图的"Start Over"按钮时ARKit会调用这个回调函数。

func coachingOverlayViewDidRequestSessionReset(_ coachingOverlayView: ARCoachingOverlayView) {
    restartExperience()
}