引言
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 {
// 告知用户追踪状态受限
...
}
}
此时可以获得追踪状态,如果受限的话还会告诉你原因。应该把原因告知用户。因为只有他们才能真正修复追踪状态,要么开灯,要么别面对白墙。还有一种可能是传感器数据不可用。对于这种情况,应通过 session interruptions 来处理。
Session Interruptions
如果摄像头输入不可用,主要原因是 app 进入后台或在 iPad 上做多任务,session 也就无法获得相机画面。在这种情况下,追踪会不可用或停止,session 也会被终止。为了应对这种情况,Apple 为我们提供了方便的 delegate 方法:
func sessionWasInterrupted(_ session: ARSession) {
showOverlay()
}
func sessionInterruptionEnded(_ session: ARSession) {
hideOverlay()
// 选择性重新开始整个体验
...
}
此时最好能将屏幕覆盖或模糊,以便告知用户当前体验已被暂停,也没有进行追踪。中断时一定要重点注意,由于没有进行追踪,设备的相对位置也就无法使用。如果用户移动了,当前的 anchor 或场景中的物理位置可能就无法像原来一样排布。对于这种情况,可能需要选择性重新开始整个体验。
以上就是追踪功能。下面讨论一下场景理解。
场景理解
场景理解的目标是找出环境中更多有关信息,以便在此环境中放置视觉对象,包括环境的 3D 拓扑以及光照情况等信息。
来看个例子,这是一张桌子。如果想在这张桌上放一个虚拟对象,首先要知道那儿有可以放东西的表面。
- 这一步可以通过平面检测实现。
- 下一步要找出放置虚拟对象的3D坐标系,这一步可以通过命中测试实现。也就是从设备发送光线,使之与现实世界相交,以便找出此坐标系。
- 为了以更真实的方式放置物体,需要估算光线以匹配环境中的光线。
下面依次讲解上面的三个步骤。
平面检测
- 平面检测可以提供相对于重力的水平面。包括地面以及类似桌子等平行平面。
- ARKit 会在后台聚合多个帧的信息,所以当用户绕着场景移动设备时,它会掌握更多有关平面的信息。
- 平面检测还能校准平面的边缘,即在平面所有检测到的部分四周套上一个矩形,并将其与主要区域对齐。所以从中也能得知物理平面的主要角度。
- 如果同一个物理平面检测到了多个虚拟平面,ARKit 会负责合并它们。组合后的平面会扩大至二者范围,因此后检测的那些平面就会从 session 中移除。
代码实现
// 启用 session 的平面检测
// 创建新的 world tracking configuration
let configuration = ARWorldTrackingSessionConfiguration()
// 启用平面检测
configuration.planeDetection = .horizontal
// 改变运行中 session 的 configuration
mySession.run(configuration)
首先创建一个 ARWorldTrackingSessionConfiguration。平面检测 planeDetection 是 ARWorldTrackingSessionConfiguration 的一个属性。要启用平面检测,只要设置 planeDetection 属性为 horizontal 即可。然后调用 ARSession 的 run 方法,用 configuration 作为参数,就会开始检测环境中的平面。如果想关掉平面检测,只要设置 plane detection 属性为 None,然后再次对 ARSession 调用 run 方法即可。session 中之前检测到的平面都会保留下来,也就是还存在于 ARFrames anchors 中。每当新的平面被检测到时,它们会以 ARPlaneAnchor 形式表示。
ARPlaneAnchor
ARAnchor 用于表示真实世界中的位置和角度,而 ARPlaneAnchor 是它的子类。
检测到新的 anchor 时,会调用 delegate 方法:
// 检测到新平面时调用
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
addPlaneGeometry(forAnchors: anchors)
}
此方法可以用于如视觉化平面。Extent 就是此平面的范围,以及一个相对的 center 属性。
如果用户绕着场景移动设备,对平面的了解会增多,所以范围 extent 可能也会相应改变。
此时会调用 delegate 方法:
// plane 的 transform 或 extent 变化时调用
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
updatePlaneGeometry(forAnchors: anchors)
}
可以使用此方法更新视觉效果。注意 center 也会相应发生改变,因为平面会向某个方向扩张。从 session 中移除 anchor 时,会调用 delegate 方法:
// 合并时移除 plane 会调用
func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
removePlaneGeometry(forAnchors: anchors)
}
如果 ARKit 合并了平面并移除了之前的小平面时会调用此方法,可以相应更新视觉效果。
现在我们知道了环境中都有哪些平面,下面看看如何实际地放点东西进去。这一步要使用命中测试。
命中测试
- 命中测试就是从设备发送一条光线,并与真实世界相交并找到相交点。
- ARKit 会使用所有能用的场景信息,包括所有检测到的平面以及3D特征点,这些信息都被 ARWorldTracking 用于确定位置。
- ARKit 然后发射光与所有能用的场景信息相交,然后用数组返回所有相交点,以距离升序排序。所以该数组的第一个元素就是离摄像头最近的相交点。
- 有不同的相交方式。可以通过 hit-test type 来定义。一共有四种方式,来具体看一看。
命中测试类型
-
Existing plane using extent: 如果在运行平面检测,并且 ARKit 已在环境中检测到了某个平面,就可以利用此平面。但你可以选择使用平面的范围或忽视它的范围。也就是说,例如你想让用户在某个平面上移动对象,就应该考虑范围,所以若光在范围内相交,就会产生一个相交点。如果这束光打到了范围的外面,就不会产生相交点。
Existing plane: 但如果你只检测到了地面的一小部分,但希望来回移动家具,就可以选择忽略范围,把当前平面当做无限平面。在这种情况下,你总是会获得相交点。
-
Estimated plane: 如果没有在运行平面检测或者还没有检测到某个平面,也可以根据目前的 3D 特征点来估算平面。在这种情况下,ARKit 会寻找环境中的处于共同平面的点并为它们安装一个平面。随后也返回与此平面的相交点。
-
Feature point: 如果你想在某个很小的表面上放东西,但此表面无法生成平面,或者是某个非常不规则的环境,也可以选择直接和特征点相交。也就是说光线会与特征点产生相交点,并将距离最近的特征点其作为结果返回。
代码实现
// 根据命中测试添加 ARAnchor
let point = CGPoint(x: 0.5, y: 0.5) // 画面中心
// 对帧执行命中测试
let results = frame.hitTest(point, types: [.existingPlane, .estimatedHorizontalPlane])
// 使用第一个结果
if let closestResult = results.first {
// 用它创建 ARAnchor
let anchor = ARAnchor(transform: closestResult.worldTransform)
// 添加到 session
session.add(anchor: anchor)
}
首先用 CGPoint 来定义从设备发射的光线,以标准画面空间坐标系表示。也就是说画面左上角是 (0,0),右下角是 (1,1)。所以如果我们想让光从屏幕中心发射,就用 (0.5,0.5) 来定义 CGPoints。对于 SceneKit 或 SpriteKit,Apple 提供了自定义 overlay,只要发送这些坐标系中的 CGPoint 即可。所以可以通过 touch gesture 使用 UI tap 的结果作为输入来定义此光线。将此点用作 histTest 方法的参数,同时还有一个参数是命中测试类型。本例里用的是 existingPlane,表示会与ARKit 目前检测到的所有平面相交,同时还有 estimatedHorizontalPlane 可用作没有检测到平面时的备选方案。然后 ARKit 会返回结果数组。访问第一个结果,也就是离相机最近的相交点。用命中测试结果 的 worldTransform 属性来创建一个新的 ARAnchor,并将其添加到 session 以便持续追踪。
如果将以上代码应用到下面的场景中,然后把手机对准桌子,就会返回屏幕中间与桌子的相交点。
然后在此位置放一个虚拟茶杯。
渲染引擎会默认背景画面有完美的光照条件。所以这增强现实看起来就像真的一样。但是如果你更暗的环境中,相机画面就会变暗,这是增强现实看上去就有一点出戏,好像在发光似的。
此时就需要调整虚拟对象的相对亮度。
因此就需要光线估算。
光线估算
- 光线估算需要使用摄像头画面,借助曝光信息来决定相对亮度。
- 对于光照良好的画面,默认为 1000 流明。对于更亮的环境,会得到更高的值。对于更暗的环境,会得到更低的值。如果是物理光源,也可以直接把这个值分配给 ambientIntensity 属性。
- 光线估算是默认启用的。可以通过 ARSessionConfiguration 的 isLightEstimationEnabled 属性进行配置:
configuration.isLightEstimationEnabled = true
光线估算的结果以 ARFrame 的 lightEstimate 属性的 ambientIntensity 值表示:
// 获取 ambient intensity 值
let intensity = frame.lightEstimate?.ambientIntensity
demo
下面来实际看一个 demo,了解如何使用 ARKit 中的场景理解。这个 demo 是 ARKit 示例应用,可以从 Apple 的开发者网站下载。它利用场景理解功能,以便在环境中放置对象。打开之后,在桌子上来回移动,就可以看到一个对焦矩形。
这一步就是在屏幕中心的位置做命中测试,以便找到相交点来放置物体。所以如果我对着桌子移动,这个矩形也会一起在桌子上滑动。
这一步还使用了平面检测,我们可以让平面检测的过程可视化,以便直观了解正在发生的事情。打开这里的 Debug 菜单并激活第二个选项,即 Debug Visualizations。
然后关闭这个菜单。就可以看到被检测到的平面了。
为了更好地理解新平面的检测过程,我们来重新检测一个平面。如果我指向一个新的平面,然后迅速指向这个平面的另一个位置,就会有两个平面被检测到:
但如果我顺着这个平面移动,ARKit 会发现其实这里只有一个平面,所以这两个平面最终会合并为一个平面。
下面,我们来实际放置一些物体。女朋友让我在办公桌上放一些鲜花,我不想让她失望,所以给这里来一个浪漫的花瓶。
点击对焦矩形,并选择添加“Vase”。
这里就是就是用屏幕中心进行命中测试并找到相交点,然后放置物体。重点要注意,这个花瓶是以真实世界的比例出现的,这是因为以下两点:1、世界追踪为我们提供了缩放比例;2、3D 模型是在真实世界坐标系中构建的。所以要为增强现实创建内容的话,一定要考虑第2点,花瓶不应该和一栋建筑物一样高,也不应该太小。
以上就是示例程序。可以从 Apple 的网站上下载并试着放入自己的内容。
渲染
下面我们来看看如何用 ARKit 进行渲染。渲染需要追踪、场景理解以及你的内容。要用 ARKit 渲染,需要处理 ARFrame 中提供的所有信息。
对于 SceneKit 和 SpriteKit,Apple 提供了可定制化的视图来为你处理 ARFrame 并进行渲染。而对于 Metal,可以创建自己的渲染引擎或将 ARKit 整合进目前的渲染引擎,Apple 提供了一张模板与它的使用介绍,使用此模板是一个很好的出发点。下面挨个讲解它们。
SceneKit
对于 SceneKit,Apple 提供了 ARSCNView,它是 SCNView 的子类,包含一个 ARSession,用于更新渲染。
ARSCNView
- ARSCNView 会绘制摄像头背景画面。
- ARSCNView 会根据 ARCamera 中的追踪 transform 更新 SCNCamera。场景保持不变,ARKit 只是控制 SCNCamera 在场景中移动,模拟用户在现实世界中来回移动设备。
- 使用光线估算时,ARSCNView 会自动在场景中放置一个 SCNLight 来更新光照。
- ARSCNView 会将 SCNNodees 映射到 ARAnchors,所以实际上不需要直接操作 ARAnchors,使用 SCNodees 即可。当一个新的 ARAnchor 被添加到 session 时,ARSCNView 也会创建一个 node。每次更新 ARAnchor 时,例如改变 transform,也会自动更新 nodes 的 transform。这一步是通过 ARSCNView delegate 来实现的。
ARSCNViewDelegate
session 每次添加新的 anchor 时,ARSCNView 都会创建一个新的 SCNNode。如果想用自定义 node,可以实现 renderer nodeFor anchor 方法并返回自定义 node。
然后 SCNNode 会被添加到场景中,此时会接收另一个 delegate 方法:
node 被更新时同样也会接收 delegate 方法,例如 ARAnchor 的 transform 改变时,DSCNNode 的 transform 也会自动改变,此时会收到两个回调。transform 更新前一个,更新后一个。
从 session 中移除 ARAnchor 时,也会自动从场景中移除对应的 SCNNode,并调用 renderer didRemove node for anchor:
以上就是 ARKit 中的 SceneKit。下面来看 SpriteKit。
SpriteKit
对于 SpriteKit,Apple 提供了 ARSKView,它是 SKView 的子类。
ARSKView
- ARSKView 包含 ARSession,用于更新渲染。
- ARSKView 会绘制摄像头背景画面。
- ARSKView 会将 SKNodes 映射到 ARAnchors。
ARSKView 提供的 delegate 方法与 SceneKit 很相似。主要的区别是 SpriteKit 是 2D 渲染引擎,这表示不能简单地移动摄像头,其实这里 ARKit 是将 ARAnchor 的位置投影到 SpriteKit 视图上,然后把 Sprites 渲染为投影位置上的广告牌(billboard),也就是说 Sprites 总是会面对摄像头。
如果想更多了解相关内容,可以去看来自 SpriteKit 团队的 session,“Going beyond 2-D in SpriteKit”,会讲如何整合 ARKit 和 SpriteKit。
下面来看看如何借助 Metal 在 ARKit 中实现自定义渲染。
自定义渲染
处理流程
ARKit 的渲染主要要做四件事。
- 绘制摄像头背景画面。通常需要创建纹理并绘制在背景。
- 根据 ARCamera 更新虚拟摄像头。需要设置视图矩阵和投影矩阵。
- 根据光线估算更新场景中的光线。
- 如果基于场景理解放置了几何体,使用 ARAnchor 来正确设置 transform。
这些所需的信息都被包含在 ARFrame 中。有两种方式获取 ARFrame。
访问 ARFrame
第一种方式是通过 ARSession 上的 currentFrame 属性。
if let frame = mySession.currentFrame {
if( frame.timestamp > _lastTimestamp ) {
updateRenderer(frame) // 用此帧更新渲染程序
_lastTimestamp = frame.timestamp
}
}
如果有自己的渲染循环,你可以使用此方法来访问 currentFrame。同时还要利用 ARFrame 的 timestamp 属性以避免多次渲染同一帧。
另一种方式是使用 Session Delegate,每次计算新的一帧时,都会调用 session didUpdate frame 方法:
func session(_ session: ARSession, didUpdate frame: ARFrame) {
// 用此帧更新渲染程序
updateRenderer(frame)
}
此时可以用它来更新渲染。此方法默认在主线程被调用,但你也可以提供自己的 dispatch queue 来调用它。下面来看看具体如何更新渲染。
func updateRenderer(_ frame: ARFrame) {
// 绘制摄像头背景画面
drawCameraImage(withPixelBuffer: frame.capturedImage)
// 更新虚拟摄像头
let viewMatrix = simd_inverse(frame.camera.transform)
let projectionMatrix = frame.camera.projectionMatrix
updateCamera(viewMatrix, projectionMatrix)
// 更新光线
updateLighting(frame.lightEstimate?.ambientIntensity)
// 根据 anchors 更新几何体
drawGeometry(forAnchors: frame.anchors)
}
首先要绘制摄像头背景画面。可以访问 AFFrame 上的 capturedImage 属性,这是 CVPixel Buffer。然后可以基于此 Pixel Buffer 生成 Metal texture,并在背景四边形中进行绘制。要注意由于这是通过 AV Foundation 暴露给我们的 Pixel Buffer,所以不应持有这些帧太多或太久,否则就会停止接收更新。下一步是根据 ARCamera 更新虚拟摄像头,因此需要确定视图矩阵以及投影矩阵。视图矩阵即 camera transform 的逆矩阵。Apple 在 ARCamera 上提供了一个 convenience 方法,以帮助我们生成投影矩阵。第三步是更新光线,访问 lightEstimate 属性并使用它的 ambientIntensity 值来更新光照模型。最后一步是遍历 anchors 和 anchors 的3D位置,并更新几何体的 transform,包括手动添加到 session 的 anchor 以及平面检测自动添加的 anchor。
绘制到 viewport
绘制摄像头画面时还有两点需要注意,一是 ARFrame 中的 captured iamge 总是以相同的角度提供。所以如果用户旋转了物理设备,画面可能对不上用户界面,此时需要应用 transform 来正确渲染。
二是摄像头画面的宽高比可能与设备不同。所以需要考虑这一点以便正确渲染屏幕中的的摄像头画面。
帮助方法
对于以上两点,Apple 提供了方便的帮助方法。ARFrame 有一个 displayTransform 方法:
// 给定 viewport 尺寸和角度并获得此帧的 display transform
let transform = frame.displayTransform(withViewportSize: viewportSize, orientation: .portrait)
此方法可以获得从帧空间到视图空间的 transform。只要提供 view port 的尺寸以及界面角度,就会得到对应的 transform。在 Metal 那个例子里,使用此 transform 的逆矩阵来调整相机画面的纹理坐标。
// 给定 viewport 尺寸和角度并获得摄像头的投影矩阵
let projectionMatrix = camera.projectionMatrix(withViewportSize: viewportSize, orientation: .portrait,
zNear: 0.001, zFar: 1000)
同时还有投影矩阵方差,给它用户界面角度以及 view port 尺寸,并告知平面裁剪(clipping planes)限制,然后就可以用得到的投影矩阵在相机画面上正确绘制虚拟内容。以上就是关于 ARKit 的介绍。
总结
ARKit 是 high-level API,为在 iOS 上创建增强现实应用而设计。ARKit 提供 WorldTracking 功能,能够得到设备相对于起始位置的相对位置。为了在现实世界中放置物体,ARKit 还提供了场景理解功能。场景理解可以检测平面,也能够对真实世界进行命中测试来找到3D坐标系并在那里放置物体。同时为了提升增强内容的真实性,ARKit 提供了基于摄像头画面的光线估算。ARKit 还提供了与 SceneKit 和 SpriteKit 的定制化整合,如果想自己开发渲染引擎的话,同时还有一张 Metal 模板供你采用。