41c3b150baf000db10f487417b96bfe5
ARKit 介绍:iOS 上的增强现实

引言

ARKit 为开发 iPhone 和 iPad 增强现实(AR)app 提供了一个前沿平台。本文为你介绍 ARKit 框架,学习如何利用其强大的位置追踪和场景理解能力。ARKit 可以和 SceneKit 与 SpriteKit 无缝结合,或是与 Metal 2 配合直接控制渲染。

下面会为你讲解如何在 iOS 上创建完全自定义的增强现实体验,包括概念和实际的代码。很多开发者都迫不及待想拥抱增强现实,现在有了 ARKit,一切都变得相当简单 :)

增强现实

什么是增强现实?

增强现实就是创建一种在物理世界中放置虚拟物体的错觉。从 iPhone 或 iPad 的相机中看进虚拟世界,就像一面魔法透镜。

例子

先来看几个例子。Apple 已经让一部分开发者早先接触了 ARKit,这些都是他们的作品。让我们试着见微知著,看看不久的将来都会发生什么。

这是一家专注于“沉浸式讲故事”体验的公司,他们用 AR 讲述了《金发姑娘与三只熊》的故事。把一间卧室变成了一本虚拟的故事书,可以通过娓娓道来的文字推动故事进行,但更更重要的是,可以让孩子从任意视角来探索故事场景。

这种级别的交互真的可以把虚拟场景变得更加活灵活现。

下一个例子是宜家,宜家使用 ARKit 来重新设计你的客厅。可以在物理物体旁边放上虚拟内容,为用户打开了一个充满无限可能性的新世界。

最后一个例子是游戏,Pokemon Go。

大名鼎鼎的 Pokemon Go 借助 ARKit 把小精灵的捕捉提升到了全新的 level。可以把虚拟内容固定在现实世界中,的确获得了比之前更加身临其境的体验。

小结

以上就是增强现实的四个例子,但还远远不止于此。有许许多多方法可以借助增强现实来提升用户体验。但增强现实需要很多领域的知识。从计算机视觉、传感器数据混合处理,到与硬件对话以获得相机校准和相机内部功能。Apple 想让这一切变得容易。所以 WWDC 2017 发布了 ARKit。

ARKit

  • ARKit 是一个移动端 AR 平台,用于在 iOS 上开发增强现实 app。
  • ARKit 提供了接口简单的高级 API,有一系列强大的功能。
  • 但更重要的是,它也会在目前的数千万台 iOS 设备上推出。为了获得 ARKit 的完整功能,需要 A9 及以上芯片。其实也就是大部分运行 iOS 11 的设备,包括 iPhone 6S。

功能

那么 ARKit 都有哪些功能呢?其实 ARKit 可以被明确分为三层,第一层是追踪。

追踪(Tracking)

追踪是 ARKit 的核心功能,也就是可以实时追踪设备。

  • 世界追踪(world tracking)可以提供设备在物理环境中的相对位置。
  • 借助视觉惯性里程计Visual–Inertial Odometry(VIO),可以提供设备所在位置的精确视图以及设备朝向,视觉惯性里程计使用了相机图像和设备的运动数据。
  • 更重要的是不需要外设,不需要提前了解所处的环境,也不需要另外的传感器。

场景理解(Scene Understanding)

追踪上面一层是场景理解,即确定设备周围环境的属性或特征。它会提供诸如平面检测(plane detection)等功能。

  • 平面检测能够确定物理环境中的表面或平面。例如地板或桌子。
  • 为了放置虚拟物体,Apple 还提供了命中测试功能。此功能可获得与真实世界拓扑的相交点,以便在物理世界中放置虚拟物体。
  • 最后,场景理解可以进行光线估算。光线估算用于正确光照你的虚拟几何体,使其与物理世界相匹配。
    结合使用上述功能,可以将虚拟内容无缝整合进物理环境。所以 ARKit 的最后一层就是渲染

渲染(Rendering)

  • Apple 让我们可以轻易整合任意渲染程序。他们提供的持续相机图像流、追踪信息以及场景理解都可以被导入任意渲染程序中。
  • 对于使用 SceneKit 或 SpriteKit 的人,Apple 提供了自定义 AR view,替你完成了大部分的渲染。所以真的很容易上手。
  • 同时对于做自定义渲染的人,Apple 通过 Xcode 提供了一个 metal 模板,可以把 ARKit 整合进你的自定义渲染器。

one more thing

Unity 和 UNREAL 会支持 ARKit 的全部功能。

使用 ARKit

创建增强现实体验需要的所有处理都由 ARKit 框架负责。

选好处理程序后,只要使用 ARKit 来完场处理的部分即可。渲染增强现实场景所需的所有信息都由 ARKit 来提供。

除了处理,ARKit 还负责捕捉信息,这些信息用于构建增强现实。ARKit 会在幕后使用 AVFoundation 和 CoreMotion,从设备捕捉图像和运动数据以进行追踪,并为渲染程序提供相机图像。

所以如何使用 ARKit 呢?

ARKit 是基于 session 的 API。所以首先你要做创建一个简单的 ARSession。ARSession 对象用于控制所有处理流程,这些流程用于创建增强现实 app。

但首先需要确定增强现实 app 将会做哪种类型的追踪。所以,还要创建一个 ARSessionConfiguration。

ARSessionConfiguration 及其子类用于确定 session 将会运行什么样的追踪。只要把对应的属性设置为 enable 或 disable,就可以获得不同类型的场景理解,并让 ARSession 做不同的处理。

要运行 session,只要对 ARSession 调用 run 方法即可,带上所需的 configuration。

run(_ configuration)

然后处理流程就会立刻开始。同时底层也会开始捕捉信息。

所以幕后会自动创建 AVCaptureSession 和 CMMotionManager。它们用于获取图像数据和运动数据,这些数据会被用于追踪。

处理完成后,ARSession 会输出 ARFrames。

ARFrame 就是当前时刻的快照,包括 session 的所有状态,所有渲染增强现实场景所需的信息。要访问 ARFrame,只要获取 ARSession 的 currentFrame 属性。或者也可以把自己设置为 delegate,接收新的 ARFrame。

ARSessionConfiguration

下面详细讲解一下 ARSessionConfiguration。ARSessionConfiguration 用于确定 session 上将会运行哪种类型的追踪。所以它提供了不同的 configuration 类。基类是 ARSessionConfiguration,提供了三个追踪自由度,也就是设备角度。其子类 ARWorldTrackingSessionConfiguration 提供六个追踪自由度。

这个 World tracking 世界追踪是核心功能,不仅可以获得设备角度,还能获得设备的相对位置,此外还能获得有关场景的信息。因为有它,才能够进行场景理解,例如获得特征点以及在世界中的物理位置。要打开或关闭功能,只要设置 session configuration 类的属性即可。

session configuration 还可以告诉你可用性(availability)。如果你想知道当前设备是否支持直接追踪,只要检查 ARWorldTrackingSessionConfiguration 类的属性 isSupported 即可。

if ARWorldTrackingSessionConfiguration.isSupported {
    configuration = ARWorldTrackingSessionConfiguration()
}
else {
    configuration = ARSessionConfiguration()
}

如果支持的话就可以用 WorldTrackingSessionConfiguration,否则就降回只提供三个自由度的基类 ARSessionConfiguration。

这里要重点注意,由于基类没有如何场景理解功能,例如命中测试在某些设备上就不可用。所以 Apple 还提供了 UI required device capability,可以在 app 里设置,这样 app 就只会出现在受支持设备的 App Store 里。

ARSession

管理 AR 处理流程

刚刚说过,ARSession 是管理增强现实 app 所有处理流程的类。除了带 configuration 参数调用 run 之外,还可以调用 pause。pause 可以暂停 session 上所有处理流程。例如 view 不在前台了,就可以停止处理,以停止使用 CPU,暂停时追踪不会进行。要在暂停后恢复追踪,只要再次对 session 调用 run,参数即它自己的 configuration。最后,你可以多次调用 run 以在不同的 configuration 间切换。假设我想启用平面检测,就可以更改 configuration,再次对 session 调用 run,从而打开平面检测。session 会自动在两个 configuration 之间无缝转换,而不会丢失任何相机图像。

// 运行 session
session.run(configuration)

// 暂停 session
session.pause()

// 恢复 session
session.run(session.configuration)

// 改变 configuration
session.run(otherConfiguration)

重置追踪

除了 run 命令,还可以重置追踪。运行 run 命令时带上 options 参数即可重置追踪。

// 重置追踪
session.run(configuration, options: .resetTracking)

这样会重新初始化目前的所有追踪。相机位置也会再次从 0,0,0 开始。所以如果你想将应用重置为某个初始点,这个方法会很有用。

Session 更新

所以如何使用 ARSession 的处理结果呢?把自己设置为 delegate 就可以接收 session 更新。要获取最近一帧,就可以实现 session didUpdate Frame。要进行错误处理,就可以实现 session didFailWithError,此方法用于处理 fatal 错误,例如设备不支持世界追踪就会出现这样的错误,session 则会被暂停。

// 访问最近一帧
func session(_: ARSession, didUpdate: ARFrame)

// 处理 session 错误
func session(_: ARSession, didFailWithError: Error)

currentFrame

使用 ARSession 处理结果的另一种方式是通过 currentFrame 属性。

ARFrame

那么 ARFrame 都包含什么东西呢?渲染增强现实场景所需的所有信息,ARFrame 都有。

  • ARFrame 首先会提供相机图像,用于渲染场景背景。
  • 其次提供了追踪信息,如设备角度和位置,甚至是追踪状态。
  • 最后它提供了场景理解,例如特征点、空间中的物理位置以及光线估算。ARKit 使用 ARAnchor 来表示空间中的物理位置。

ARAnchor

  • ARAnchor 是空间中相对真实世界的位置和角度。
  • ARAnchor 可以添加到场景中,或是从场景中移除。基本上来说,它们用于表示虚拟内容在物理环境中的锚定。所以如果要添加自定义 anchor,添加到 session 里就可以了。它会在 session 生命周期中一直存在。但如果你在运行诸如平面检测功能,ARAnchor 则会被自动添加到 session 中。
  • 要响应被添加的 anchor,可以从 current ARFrame 中获得完整列表,此列表包含 session 正在追踪的所有 anchor。
  • 或者也可以响应 delegate 方法,例如 add、update 以及 remove,session 中的 anchor 被添加、更新或移除时会通知。

小结

以上是四个主要类,用于创建增强现实体验。下面专门讨论一下追踪。

追踪

追踪就是要实时确定空间中的物理位置。这并不是一件简单的事。但增强现实必须要找到设备的位置和角度,这样才能正确渲染事物。下面看一个例子。

我在物理环境中放了一把虚拟椅子和一张虚拟桌子。如果我把设备转个角度,它们依然固定在空间中。但更重要的是,如果我在场景中走来走去,它们仍被固定在那里。

这是因为我们在不断更新投影的角度,也就用于渲染这个虚拟内容的投影矩阵,使其从任何角度看上去都是正确的。那具体要怎么做呢?

世界追踪

ARKit 提供了世界追踪功能。此技术使用了视觉惯性里程计以及相机图像和运动数据。

  • 提供设备的旋转度以及相对位置。但更重要的是,它提供了真实世界比例。所以虚拟内容实际上会被缩放,然后渲染到物理场景中。
  • 设备的运动数据计算出了物理移动距离,计算单位为米。
  • 追踪给定的所有位置都是相对于 session 的起始位置的。
  • 提供 3D 特征点。

世界追踪的工作原理

特征点就是相机图像中的一块块信息碎片,需要检测这些特征点。可以看到,坐标轴表示设备的位置和角度。当用户在世界中移动时,它会画出一条轨迹。这里的小点点就表示场景中已检测到的 3D 特征点。在场景中移动时可以对它们作三角测量,然后用它们去匹配特征,如果匹配之前的特征点则会画出一条线。使用所有这些信息以及运动数据,能够精确提供设备的角度和位置。

这看起来可能很难。但下面我们来看看如何用代码运行世界追踪。

世界追踪的代码实现
// 创建 session
let mySession = ARSession()

// 把自己设为 session delegate
mySession.delegate = self

// 创建 world tracking configuration
let configuration = ARWorldTrackingSessionConfiguration()

// 运行 session
mySession.run(configuration) 

首先要创建一个 ARSession。之前说过,它会管理世界追踪中所有的处理流程。接下来,把自己设置为 session delegate,这样就可以接收帧的更新。然后创建 WorldTrackingSessionConfiguration,这一步就是在说,“我要用世界追踪。我希望 session 运行这个功能。”然后只要调用 run,处理流程就会立即开始。同时也会开始捕捉信息。

session 在幕后创建了一个 AVCaptureSession 以及一个 CMMotionManager,通过它们获得图像和运动数据。使用图像来检测场景中的特征点。在更高的频率下使用运动数据,随着时间推移计算其积分以获得设备的运动数据。同时使用两者,就能够进行传感器数据混合处理,从而提供精确的角度和位置,并以 ARFrame 形式返回。

ARCamera

每个 ARFrame 都会包含一个 ARCamera。ARCamera 对象表示虚拟摄像头。虚拟摄像头就代表了设备的角度和位置。

  • ARCamera 提供了一个 transform。transform 是一个 4x4 矩阵。提供了物理设备相对于初始位置的变换。
  • ARCamera 提供了追踪状态(tracking state),通知你如何使用 transform,这个在后面会讲。
  • ARCamera 提供了相机内部功能(camera intrinsics)。包括焦距和主焦点,用于寻找投影矩阵。投影矩阵是 ARCamera 上的一个 convenience 方法,可用于渲染虚拟你的几何体。

小结

以上就是 ARKit 提供的追踪功能。

创建第一个 ARKit 应用

下面我们来看一个使用世界追踪的 demo,并创建第一个 ARKit 应用。

打开 Xcode 9 时会注意到有一张新的模板,用于创建增强现实 app。选择它,然后点 Next。

给定项目名 MyARApp,语言可以选择 Swift 或 Objective-C。这儿还有 Content Technology 选项。Content Technology 是用来渲染增强现实场景的。可以选择 SenceKit、SpriteKit 或 Metal。本例使用 SceneKit。

点击 Next 并创建 workspace。

这儿有一个 view controller。它有一个 ARSCNView。这个 ARSCNView 是一个自定义 AR 子类,替我们实现了大部分渲染工作。也就是说它会基于返回的 ARFrame 更新虚拟摄像头。ARSCNView 有一个 session 属性。可以看到给 sceneView 设置了一个 scene,这个 scene 将会是一艘飞船,会处于世界原点处 z 轴往前一点的位置。最重要的部分是对 session 调用 run,带有 WorldTrackingSessionConfiguration 参数。这样就会运行世界追踪,同时 view 会为我们更新虚拟摄像头。

尝试在设备上运行。安装后,会先弹出相机授权,必须使用相机进行追踪并渲染场景背景。

授权后就可以看到摄像头画面。正前方有一艘飞船。

如果改变设备的角度,你会发现它被固定在空间中。

但更重要的是,如果你绕着飞船移动,就会发现它真的被固定在物理世界中了。

实现的原理就是同时使用设备的角度以及相对位置,更新虚拟摄像头,让它看在飞船上。

还不够好玩?来再给它加点料,尝试在点击屏幕时,为场景添加点东西。首先写一个 tap gesture recognizer,然后添加到 view 上,每次点击屏幕时,都会调用 handleTap 方法。

    override func viewDidLoad() {
        ...

        // Set the scene to the view
        sceneView.scene = scene

        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(ViewController.handleTap(gestureRecognizer:)))
        view.addGestureRecognizer(tapGesture)
    }

下面实现 handleTap 方法。

    @objc
    func handleTap(gestureRecognizer: UITapGestureRecognizer) {
        //使用 view 的快照来创建图片平面
        let imagePlane = SCNPlane(width: sceneView.bounds.width / 6000, height: sceneView.bounds.height / 6000)
        imagePlane.firstMaterial?.diffuse.contents = sceneView.snapshot()
        imagePlane.firstMaterial?.lightingModel = .constant
    }

首先创建一个 SCNPlane,参数是 width 和 height。然后将 material 的 contents 设为 view 的快照(snapshot),这一步可能不是很直观。你猜会怎么样?其实就是把渲染后的 view 截图,包括摄像头画面背景以及前面放的虚拟几何体。然后将光线模型设为 constant,这样 ARKit 提供的光线估算就不会应用此图片上,因为它已经与环境匹配了。下一步要把它添加到场景中。

    @objc
    func handleTap(gestureRecognizer: UITapGestureRecognizer) {
        //使用 view 的快照来创建图片平面
        let imagePlane = SCNPlane(width: sceneView.bounds.width / 6000, height: sceneView.bounds.height / 6000)
        imagePlane.firstMaterial?.diffuse.contents = sceneView.snapshot()
        imagePlane.firstMaterial?.lightingModel = .constant

        //创建 plane node 并添加到场景
        let planeNode = SCNNode(geometry: imagePlane)
        sceneView.scene.rootNode.addChildNode(planeNode)
    }

先创建一个 plane node,这个 SCNNode 封装了添加到场景中的几何体。每次触摸屏幕时,就会向场景中添加一个 image plane。但问题是,它总是会在 0, 0, 0 处。 所以怎么变得更好玩呢?我们有一个 current frame,其中包含了一个 ARCamera。我可以借助 camera 的 transform 来更新 plane node 的 transform,这样 plane node 就会处于摄像头当前在空间中的位置了。

    @objc
    func handleTap(gestureRecognizer: UITapGestureRecognizer) {
        guard let currentFrame = sceneView.session.currentFrame else {
            return
        }
        //使用 view 的快照来创建图片平面
        let imagePlane = SCNPlane(width: sceneView.bounds.width / 6000, height: sceneView.bounds.height / 6000)
        imagePlane.firstMaterial?.diffuse.contents = sceneView.snapshot()
        imagePlane.firstMaterial?.lightingModel = .constant

        //创建 plane node 并添加到场景
        let planeNode = SCNNode(geometry: imagePlane)
        sceneView.scene.rootNode.addChildNode(planeNode)

        //将 node 的 transform 设为摄像头前 10cm
        var translation = matrix_identity_float4x4
        translation.columns.3.z = -0.1
        planeNode.simdTransform = matrix_multiply(currentFrame.camera.transform, translation)
    }

首先从 sceneView session 中获得 current frame。下一步,用摄像头的 transform 更新 plane node 的 transform。这一步我先创建了转换矩阵,因为我不想把 image plane 就放在相机的位置,这样会挡住我的视线,所以要把它放在相机前面。所以这里的转换我用了 负z轴。缩放的单位都是米,所以使用 .1 来表示相机前方 10 厘米。将此矩阵和摄像头的 transform 相乘,并将结果应用到 plane node 上,这个 plane node 将会是一个 image plane,位于相机前方 10 厘米处。

现在来试试看会是什么样子。

摄像头场景运行后,可以看到依然有一艘飞船浮在空中。可以试着在任意地方点击屏幕,可以看到快照图片就浮在了空间里你点击的位置。

这只是 ARKit 的万千可能性之一,但的确是非常酷炫的体验。以上就是 ARKit 的使用。

追踪质量

刚刚的 demo 使用了 ARKit 的追踪功能,现在来讨论如何获得最佳质量的追踪结果。

  • 追踪依赖于源源不断的传感器数据。这表示如果不再提供相机画面,追踪就会停止。
  • 追踪在良好纹理的环境中会获得最佳工作状态。这表示场景从视觉上来说需要足够复杂,以便从相机画面中找到特征点。所以如果你对着一张白墙,或房间里光线不足,可能就无法找到特征点了,追踪功能就会受限。
  • 追踪在静止场景中会获得最佳工作状态。所以如果相机里的大部分东西都在移动,视觉数据无法对应运动数据,就会导致漂移,这同样也会限制追踪状态。

为了应对这些情况,ARCamera 提供了 tracking state 属性。

tracking state 有三个可能值:Not Avaiable 不可用,Normal 正常,以及 Limited 受限。新的 session 会从 Not Avaiable 开始,表示摄像头的 transform 为空,即身份矩阵(identity matrix)。一般很快就会找到第一个追踪姿态(tracking pose),状态会从 Not Avaiable 变为 Normal,表示现在可以用摄像头的 transform 了。如果后面追踪受限,追踪状态会从 Normal 变为 Limited,而且会告诉你原因。例如用户面对一面白墙,或没有足够的光线,也就是特征不足。这时应该告知用户。所以,Apple 提供了一个 session delegate 方法供我们实现:

```
func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
if case .limited(let reason) = camera.trackingState {
// 告知用户追踪状态受限
...
}
}

top Created with Sketch.