引言
ARKit 為開發(fā) iPhone 和 iPad 增強(qiáng)現(xiàn)實(AR)app 提供了一個前沿平臺。本文為你介紹 ARKit 框架,學(xué)習(xí)如何利用其強(qiáng)大的位置追蹤和場景理解能力。ARKit 可以和 SceneKit 與 SpriteKit 無縫結(jié)合,或是與 Metal 2 配合直接控制渲染。
下面會為你講解如何在 iOS 上創(chuàng)建完全自定義的增強(qiáng)現(xiàn)實體驗,包括概念和實際的代碼。很多開發(fā)者都迫不及待想擁抱增強(qiáng)現(xiàn)實,現(xiàn)在有了 ARKit,一切都變得相當(dāng)簡單 :)
增強(qiáng)現(xiàn)實
什么是增強(qiáng)現(xiàn)實?
增強(qiáng)現(xiàn)實就是創(chuàng)建一種在物理世界中放置虛擬物體的錯覺。從 iPhone 或 iPad 的相機(jī)中看進(jìn)虛擬世界,就像一面魔法透鏡。
例子
先來看幾個例子。Apple 已經(jīng)讓一部分開發(fā)者早先接觸了 ARKit,這些都是他們的作品。讓我們試著見微知著,看看不久的將來都會發(fā)生什么。

這是一家專注于“沉浸式講故事”體驗的公司,他們用 AR 講述了《金發(fā)姑娘與三只熊》的故事。把一間臥室變成了一本虛擬的故事書,可以通過娓娓道來的文字推動故事進(jìn)行,但更更重要的是,可以讓孩子從任意視角來探索故事場景。

這種級別的交互真的可以把虛擬場景變得更加活靈活現(xiàn)。
下一個例子是宜家,宜家使用 ARKit 來重新設(shè)計你的客廳??梢栽谖锢砦矬w旁邊放上虛擬內(nèi)容,為用戶打開了一個充滿無限可能性的新世界。

最后一個例子是游戲,Pokemon Go。

大名鼎鼎的 Pokemon Go 借助 ARKit 把小精靈的捕捉提升到了全新的 level??梢园烟摂M內(nèi)容固定在現(xiàn)實世界中,的確獲得了比之前更加身臨其境的體驗。
小結(jié)
以上就是增強(qiáng)現(xiàn)實的四個例子,但還遠(yuǎn)遠(yuǎn)不止于此。有許許多多方法可以借助增強(qiáng)現(xiàn)實來提升用戶體驗。但增強(qiáng)現(xiàn)實需要很多領(lǐng)域的知識。從計算機(jī)視覺、傳感器數(shù)據(jù)混合處理,到與硬件對話以獲得相機(jī)校準(zhǔn)和相機(jī)內(nèi)部功能。Apple 想讓這一切變得容易。所以 WWDC 2017 發(fā)布了 ARKit。
ARKit

- ARKit 是一個移動端 AR 平臺,用于在 iOS 上開發(fā)增強(qiáng)現(xiàn)實 app。
- ARKit 提供了接口簡單的高級 API,有一系列強(qiáng)大的功能。
- 但更重要的是,它也會在目前的數(shù)千萬臺 iOS 設(shè)備上推出。為了獲得 ARKit 的完整功能,需要 A9 及以上芯片。其實也就是大部分運行 iOS 11 的設(shè)備,包括 iPhone 6S。
功能
那么 ARKit 都有哪些功能呢?其實 ARKit 可以被明確分為三層,第一層是追蹤。
追蹤(Tracking)
追蹤是 ARKit 的核心功能,也就是可以實時追蹤設(shè)備。
- 世界追蹤(world tracking)可以提供設(shè)備在物理環(huán)境中的相對位置。
- 借助視覺慣性里程計Visual–Inertial Odometry(VIO),可以提供設(shè)備所在位置的精確視圖以及設(shè)備朝向,視覺慣性里程計使用了相機(jī)圖像和設(shè)備的運動數(shù)據(jù)。
- 更重要的是不需要外設(shè),不需要提前了解所處的環(huán)境,也不需要另外的傳感器。
場景理解(Scene Understanding)
追蹤上面一層是場景理解,即確定設(shè)備周圍環(huán)境的屬性或特征。它會提供諸如平面檢測(plane detection)等功能。
- 平面檢測能夠確定物理環(huán)境中的表面或平面。例如地板或桌子。
- 為了放置虛擬物體,Apple 還提供了命中測試功能。此功能可獲得與真實世界拓?fù)涞南嘟稽c,以便在物理世界中放置虛擬物體。
- 最后,場景理解可以進(jìn)行光線估算。光線估算用于正確光照你的虛擬幾何體,使其與物理世界相匹配。
結(jié)合使用上述功能,可以將虛擬內(nèi)容無縫整合進(jìn)物理環(huán)境。所以 ARKit 的最后一層就是渲染。
渲染(Rendering)
- Apple 讓我們可以輕易整合任意渲染程序。他們提供的持續(xù)相機(jī)圖像流、追蹤信息以及場景理解都可以被導(dǎo)入任意渲染程序中。
- 對于使用 SceneKit 或 SpriteKit 的人,Apple 提供了自定義 AR view,替你完成了大部分的渲染。所以真的很容易上手。
- 同時對于做自定義渲染的人,Apple 通過 Xcode 提供了一個 metal 模板,可以把 ARKit 整合進(jìn)你的自定義渲染器。
one more thing
Unity 和 UNREAL 會支持 ARKit 的全部功能。
使用 ARKit
創(chuàng)建增強(qiáng)現(xiàn)實體驗需要的所有處理都由 ARKit 框架負(fù)責(zé)。

選好處理程序后,只要使用 ARKit 來完場處理的部分即可。渲染增強(qiáng)現(xiàn)實場景所需的所有信息都由 ARKit 來提供。

除了處理,ARKit 還負(fù)責(zé)捕捉信息,這些信息用于構(gòu)建增強(qiáng)現(xiàn)實。ARKit 會在幕后使用 AVFoundation 和 CoreMotion,從設(shè)備捕捉圖像和運動數(shù)據(jù)以進(jìn)行追蹤,并為渲染程序提供相機(jī)圖像。
所以如何使用 ARKit 呢?

ARKit 是基于 session 的 API。所以首先你要做創(chuàng)建一個簡單的 ARSession。ARSession 對象用于控制所有處理流程,這些流程用于創(chuàng)建增強(qiáng)現(xiàn)實 app。
但首先需要確定增強(qiáng)現(xiàn)實 app 將會做哪種類型的追蹤。所以,還要創(chuàng)建一個 ARSessionConfiguration。

ARSessionConfiguration 及其子類用于確定 session 將會運行什么樣的追蹤。只要把對應(yīng)的屬性設(shè)置為 enable 或 disable,就可以獲得不同類型的場景理解,并讓 ARSession 做不同的處理。
要運行 session,只要對 ARSession 調(diào)用 run 方法即可,帶上所需的 configuration。
run(_ configuration)
然后處理流程就會立刻開始。同時底層也會開始捕捉信息。

所以幕后會自動創(chuàng)建 AVCaptureSession 和 CMMotionManager。它們用于獲取圖像數(shù)據(jù)和運動數(shù)據(jù),這些數(shù)據(jù)會被用于追蹤。
處理完成后,ARSession 會輸出 ARFrames。

ARFrame 就是當(dāng)前時刻的快照,包括 session 的所有狀態(tài),所有渲染增強(qiáng)現(xiàn)實場景所需的信息。要訪問 ARFrame,只要獲取 ARSession 的 currentFrame 屬性?;蛘咭部梢园炎约涸O(shè)置為 delegate,接收新的 ARFrame。
ARSessionConfiguration
下面詳細(xì)講解一下 ARSessionConfiguration。ARSessionConfiguration 用于確定 session 上將會運行哪種類型的追蹤。所以它提供了不同的 configuration 類?;愂?ARSessionConfiguration,提供了三個追蹤自由度,也就是設(shè)備角度。其子類 ARWorldTrackingSessionConfiguration 提供六個追蹤自由度。

這個 World tracking 世界追蹤是核心功能,不僅可以獲得設(shè)備角度,還能獲得設(shè)備的相對位置,此外還能獲得有關(guān)場景的信息。因為有它,才能夠進(jìn)行場景理解,例如獲得特征點以及在世界中的物理位置。要打開或關(guān)閉功能,只要設(shè)置 session configuration 類的屬性即可。
session configuration 還可以告訴你可用性(availability)。如果你想知道當(dāng)前設(shè)備是否支持直接追蹤,只要檢查 ARWorldTrackingSessionConfiguration 類的屬性 isSupported 即可。
if ARWorldTrackingSessionConfiguration.isSupported {
configuration = ARWorldTrackingSessionConfiguration()
}
else {
configuration = ARSessionConfiguration()
}
如果支持的話就可以用 WorldTrackingSessionConfiguration,否則就降回只提供三個自由度的基類 ARSessionConfiguration。
這里要重點注意,由于基類沒有如何場景理解功能,例如命中測試在某些設(shè)備上就不可用。所以 Apple 還提供了 UI required device capability,可以在 app 里設(shè)置,這樣 app 就只會出現(xiàn)在受支持設(shè)備的 App Store 里。
ARSession
管理 AR 處理流程
剛剛說過,ARSession 是管理增強(qiáng)現(xiàn)實 app 所有處理流程的類。除了帶 configuration 參數(shù)調(diào)用 run 之外,還可以調(diào)用 pause。pause 可以暫停 session 上所有處理流程。例如 view 不在前臺了,就可以停止處理,以停止使用 CPU,暫停時追蹤不會進(jìn)行。要在暫停后恢復(fù)追蹤,只要再次對 session 調(diào)用 run,參數(shù)即它自己的 configuration。最后,你可以多次調(diào)用 run 以在不同的 configuration 間切換。假設(shè)我想啟用平面檢測,就可以更改 configuration,再次對 session 調(diào)用 run,從而打開平面檢測。session 會自動在兩個 configuration 之間無縫轉(zhuǎn)換,而不會丟失任何相機(jī)圖像。
// 運行 session
session.run(configuration)
// 暫停 session
session.pause()
// 恢復(fù) session
session.run(session.configuration)
// 改變 configuration
session.run(otherConfiguration)
重置追蹤
除了 run 命令,還可以重置追蹤。運行 run 命令時帶上 options 參數(shù)即可重置追蹤。
// 重置追蹤
session.run(configuration, options: .resetTracking)
這樣會重新初始化目前的所有追蹤。相機(jī)位置也會再次從 0,0,0 開始。所以如果你想將應(yīng)用重置為某個初始點,這個方法會很有用。
Session 更新
所以如何使用 ARSession 的處理結(jié)果呢?把自己設(shè)置為 delegate 就可以接收 session 更新。要獲取最近一幀,就可以實現(xiàn) session didUpdate Frame。要進(jìn)行錯誤處理,就可以實現(xiàn) session didFailWithError,此方法用于處理 fatal 錯誤,例如設(shè)備不支持世界追蹤就會出現(xiàn)這樣的錯誤,session 則會被暫停。
// 訪問最近一幀
func session(_: ARSession, didUpdate: ARFrame)
// 處理 session 錯誤
func session(_: ARSession, didFailWithError: Error)
currentFrame
使用 ARSession 處理結(jié)果的另一種方式是通過 currentFrame 屬性。
ARFrame
那么 ARFrame 都包含什么東西呢?渲染增強(qiáng)現(xiàn)實場景所需的所有信息,ARFrame 都有。
-
ARFrame 首先會提供相機(jī)圖像,用于渲染場景背景。
image -
其次提供了追蹤信息,如設(shè)備角度和位置,甚至是追蹤狀態(tài)。
image -
最后它提供了場景理解,例如特征點、空間中的物理位置以及光線估算。ARKit 使用 ARAnchor 來表示空間中的物理位置。
image
ARAnchor

- ARAnchor 是空間中相對真實世界的位置和角度。
- ARAnchor 可以添加到場景中,或是從場景中移除?;旧蟻碚f,它們用于表示虛擬內(nèi)容在物理環(huán)境中的錨定。所以如果要添加自定義 anchor,添加到 session 里就可以了。它會在 session 生命周期中一直存在。但如果你在運行諸如平面檢測功能,ARAnchor 則會被自動添加到 session 中。
- 要響應(yīng)被添加的 anchor,可以從 current ARFrame 中獲得完整列表,此列表包含 session 正在追蹤的所有 anchor。
- 或者也可以響應(yīng) delegate 方法,例如 add、update 以及 remove,session 中的 anchor 被添加、更新或移除時會通知。
小結(jié)
以上是四個主要類,用于創(chuàng)建增強(qiáng)現(xiàn)實體驗。下面專門討論一下追蹤。
追蹤
追蹤就是要實時確定空間中的物理位置。這并不是一件簡單的事。但增強(qiáng)現(xiàn)實必須要找到設(shè)備的位置和角度,這樣才能正確渲染事物。下面看一個例子。

我在物理環(huán)境中放了一把虛擬椅子和一張?zhí)摂M桌子。如果我把設(shè)備轉(zhuǎn)個角度,它們依然固定在空間中。但更重要的是,如果我在場景中走來走去,它們?nèi)员还潭ㄔ谀抢铩?/p>

這是因為我們在不斷更新投影的角度,也就用于渲染這個虛擬內(nèi)容的投影矩陣,使其從任何角度看上去都是正確的。那具體要怎么做呢?
世界追蹤
ARKit 提供了世界追蹤功能。此技術(shù)使用了視覺慣性里程計以及相機(jī)圖像和運動數(shù)據(jù)。
- 提供設(shè)備的旋轉(zhuǎn)度以及相對位置。但更重要的是,它提供了真實世界比例。所以虛擬內(nèi)容實際上會被縮放,然后渲染到物理場景中。
- 設(shè)備的運動數(shù)據(jù)計算出了物理移動距離,計算單位為米。
- 追蹤給定的所有位置都是相對于 session 的起始位置的。
- 提供 3D 特征點。

世界追蹤的工作原理

特征點就是相機(jī)圖像中的一塊塊信息碎片,需要檢測這些特征點??梢钥吹?,坐標(biāo)軸表示設(shè)備的位置和角度。當(dāng)用戶在世界中移動時,它會畫出一條軌跡。這里的小點點就表示場景中已檢測到的 3D 特征點。在場景中移動時可以對它們作三角測量,然后用它們?nèi)テヅ涮卣鳎绻ヅ渲暗奶卣鼽c則會畫出一條線。使用所有這些信息以及運動數(shù)據(jù),能夠精確提供設(shè)備的角度和位置。
這看起來可能很難。但下面我們來看看如何用代碼運行世界追蹤。
世界追蹤的代碼實現(xiàn)
// 創(chuàng)建 session
let mySession = ARSession()
// 把自己設(shè)為 session delegate
mySession.delegate = self
// 創(chuàng)建 world tracking configuration
let configuration = ARWorldTrackingSessionConfiguration()
// 運行 session
mySession.run(configuration)
首先要創(chuàng)建一個 ARSession。之前說過,它會管理世界追蹤中所有的處理流程。接下來,把自己設(shè)置為 session delegate,這樣就可以接收幀的更新。然后創(chuàng)建 WorldTrackingSessionConfiguration,這一步就是在說,“我要用世界追蹤。我希望 session 運行這個功能?!比缓笾灰{(diào)用 run,處理流程就會立即開始。同時也會開始捕捉信息。
session 在幕后創(chuàng)建了一個 AVCaptureSession 以及一個 CMMotionManager,通過它們獲得圖像和運動數(shù)據(jù)。使用圖像來檢測場景中的特征點。在更高的頻率下使用運動數(shù)據(jù),隨著時間推移計算其積分以獲得設(shè)備的運動數(shù)據(jù)。同時使用兩者,就能夠進(jìn)行傳感器數(shù)據(jù)混合處理,從而提供精確的角度和位置,并以 ARFrame 形式返回。

ARCamera
每個 ARFrame 都會包含一個 ARCamera。ARCamera 對象表示虛擬攝像頭。虛擬攝像頭就代表了設(shè)備的角度和位置。
- ARCamera 提供了一個 transform。transform 是一個 4x4 矩陣。提供了物理設(shè)備相對于初始位置的變換。
- ARCamera 提供了追蹤狀態(tài)(tracking state),通知你如何使用 transform,這個在后面會講。
- ARCamera 提供了相機(jī)內(nèi)部功能(camera intrinsics)。包括焦距和主焦點,用于尋找投影矩陣。投影矩陣是 ARCamera 上的一個 convenience 方法,可用于渲染虛擬你的幾何體。

小結(jié)
以上就是 ARKit 提供的追蹤功能。
創(chuàng)建第一個 ARKit 應(yīng)用
下面我們來看一個使用世界追蹤的 demo,并創(chuàng)建第一個 ARKit 應(yīng)用。
打開 Xcode 9 時會注意到有一張新的模板,用于創(chuàng)建增強(qiáng)現(xiàn)實 app。選擇它,然后點 Next。

給定項目名 MyARApp,語言可以選擇 Swift 或 Objective-C。這兒還有 Content Technology 選項。Content Technology 是用來渲染增強(qiáng)現(xiàn)實場景的。可以選擇 SenceKit、SpriteKit 或 Metal。本例使用 SceneKit。

點擊 Next 并創(chuàng)建 workspace。

這兒有一個 view controller。它有一個 ARSCNView。這個 ARSCNView 是一個自定義 AR 子類,替我們實現(xiàn)了大部分渲染工作。也就是說它會基于返回的 ARFrame 更新虛擬攝像頭。ARSCNView 有一個 session 屬性??梢钥吹浇o sceneView 設(shè)置了一個 scene,這個 scene 將會是一艘飛船,會處于世界原點處 z 軸往前一點的位置。最重要的部分是對 session 調(diào)用 run,帶有 WorldTrackingSessionConfiguration 參數(shù)。這樣就會運行世界追蹤,同時 view 會為我們更新虛擬攝像頭。
嘗試在設(shè)備上運行。安裝后,會先彈出相機(jī)授權(quán),必須使用相機(jī)進(jìn)行追蹤并渲染場景背景。

授權(quán)后就可以看到攝像頭畫面。正前方有一艘飛船。

如果改變設(shè)備的角度,你會發(fā)現(xiàn)它被固定在空間中。

但更重要的是,如果你繞著飛船移動,就會發(fā)現(xiàn)它真的被固定在物理世界中了。


實現(xiàn)的原理就是同時使用設(shè)備的角度以及相對位置,更新虛擬攝像頭,讓它看在飛船上。
還不夠好玩?來再給它加點料,嘗試在點擊屏幕時,為場景添加點東西。首先寫一個 tap gesture recognizer,然后添加到 view 上,每次點擊屏幕時,都會調(diào)用 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)
}
下面實現(xiàn) handleTap 方法。
@objc
func handleTap(gestureRecognizer: UITapGestureRecognizer) {
//使用 view 的快照來創(chuàng)建圖片平面
let imagePlane = SCNPlane(width: sceneView.bounds.width / 6000, height: sceneView.bounds.height / 6000)
imagePlane.firstMaterial?.diffuse.contents = sceneView.snapshot()
imagePlane.firstMaterial?.lightingModel = .constant
}
首先創(chuàng)建一個 SCNPlane,參數(shù)是 width 和 height。然后將 material 的 contents 設(shè)為 view 的快照(snapshot),這一步可能不是很直觀。你猜會怎么樣?其實就是把渲染后的 view 截圖,包括攝像頭畫面背景以及前面放的虛擬幾何體。然后將光線模型設(shè)為 constant,這樣 ARKit 提供的光線估算就不會應(yīng)用此圖片上,因為它已經(jīng)與環(huán)境匹配了。下一步要把它添加到場景中。
@objc
func handleTap(gestureRecognizer: UITapGestureRecognizer) {
//使用 view 的快照來創(chuàng)建圖片平面
let imagePlane = SCNPlane(width: sceneView.bounds.width / 6000, height: sceneView.bounds.height / 6000)
imagePlane.firstMaterial?.diffuse.contents = sceneView.snapshot()
imagePlane.firstMaterial?.lightingModel = .constant
//創(chuàng)建 plane node 并添加到場景
let planeNode = SCNNode(geometry: imagePlane)
sceneView.scene.rootNode.addChildNode(planeNode)
}?
先創(chuàng)建一個 plane node,這個 SCNNode 封裝了添加到場景中的幾何體。每次觸摸屏幕時,就會向場景中添加一個 image plane。但問題是,它總是會在 0, 0, 0 處。 所以怎么變得更好玩呢?我們有一個 current frame,其中包含了一個 ARCamera。我可以借助 camera 的 transform 來更新 plane node 的 transform,這樣 plane node 就會處于攝像頭當(dāng)前在空間中的位置了。
@objc
func handleTap(gestureRecognizer: UITapGestureRecognizer) {
guard let currentFrame = sceneView.session.currentFrame else {
return
}
//使用 view 的快照來創(chuàng)建圖片平面
let imagePlane = SCNPlane(width: sceneView.bounds.width / 6000, height: sceneView.bounds.height / 6000)
imagePlane.firstMaterial?.diffuse.contents = sceneView.snapshot()
imagePlane.firstMaterial?.lightingModel = .constant
//創(chuàng)建 plane node 并添加到場景
let planeNode = SCNNode(geometry: imagePlane)
sceneView.scene.rootNode.addChildNode(planeNode)
//將 node 的 transform 設(shè)為攝像頭前 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。這一步我先創(chuàng)建了轉(zhuǎn)換矩陣,因為我不想把 image plane 就放在相機(jī)的位置,這樣會擋住我的視線,所以要把它放在相機(jī)前面。所以這里的轉(zhuǎn)換我用了 負(fù)z軸。縮放的單位都是米,所以使用 .1 來表示相機(jī)前方 10 厘米。將此矩陣和攝像頭的 transform 相乘,并將結(jié)果應(yīng)用到 plane node 上,這個 plane node 將會是一個 image plane,位于相機(jī)前方 10 厘米處。
現(xiàn)在來試試看會是什么樣子。
攝像頭場景運行后,可以看到依然有一艘飛船浮在空中。可以試著在任意地方點擊屏幕,可以看到快照圖片就浮在了空間里你點擊的位置。


這只是 ARKit 的萬千可能性之一,但的確是非??犰诺捏w驗。以上就是 ARKit 的使用。
追蹤質(zhì)量
剛剛的 demo 使用了 ARKit 的追蹤功能,現(xiàn)在來討論如何獲得最佳質(zhì)量的追蹤結(jié)果。

- 追蹤依賴于源源不斷的傳感器數(shù)據(jù)。這表示如果不再提供相機(jī)畫面,追蹤就會停止。
- 追蹤在良好紋理的環(huán)境中會獲得最佳工作狀態(tài)。這表示場景從視覺上來說需要足夠復(fù)雜,以便從相機(jī)畫面中找到特征點。所以如果你對著一張白墻,或房間里光線不足,可能就無法找到特征點了,追蹤功能就會受限。
- 追蹤在靜止場景中會獲得最佳工作狀態(tài)。所以如果相機(jī)里的大部分東西都在移動,視覺數(shù)據(jù)無法對應(yīng)運動數(shù)據(jù),就會導(dǎo)致漂移,這同樣也會限制追蹤狀態(tài)。
為了應(yīng)對這些情況,ARCamera 提供了 tracking state 屬性。

tracking state 有三個可能值:Not Avaiable 不可用,Normal 正常,以及 Limited 受限。新的 session 會從 Not Avaiable 開始,表示攝像頭的 transform 為空,即身份矩陣(identity matrix)。一般很快就會找到第一個追蹤姿態(tài)(tracking pose),狀態(tài)會從 Not Avaiable 變?yōu)?Normal,表示現(xiàn)在可以用攝像頭的 transform 了。如果后面追蹤受限,追蹤狀態(tài)會從 Normal 變?yōu)?Limited,而且會告訴你原因。例如用戶面對一面白墻,或沒有足夠的光線,也就是特征不足。這時應(yīng)該告知用戶。所以,Apple 提供了一個 session delegate 方法供我們實現(xiàn):
func session(_ session: ARSession, cameraDidChangeTrackingState camera: ARCamera) {
if case .limited(let reason) = camera.trackingState {
// 告知用戶追蹤狀態(tài)受限
...
}
}
此時可以獲得追蹤狀態(tài),如果受限的話還會告訴你原因。應(yīng)該把原因告知用戶。因為只有他們才能真正修復(fù)追蹤狀態(tài),要么開燈,要么別面對白墻。還有一種可能是傳感器數(shù)據(jù)不可用。對于這種情況,應(yīng)通過 session interruptions 來處理。
Session Interruptions
如果攝像頭輸入不可用,主要原因是 app 進(jìn)入后臺或在 iPad 上做多任務(wù),session 也就無法獲得相機(jī)畫面。在這種情況下,追蹤會不可用或停止,session 也會被終止。為了應(yīng)對這種情況,Apple 為我們提供了方便的 delegate 方法:
func sessionWasInterrupted(_ session: ARSession) {
showOverlay()
}
func sessionInterruptionEnded(_ session: ARSession) {
hideOverlay()
// 選擇性重新開始整個體驗
...
}
此時最好能將屏幕覆蓋或模糊,以便告知用戶當(dāng)前體驗已被暫停,也沒有進(jìn)行追蹤。中斷時一定要重點注意,由于沒有進(jìn)行追蹤,設(shè)備的相對位置也就無法使用。如果用戶移動了,當(dāng)前的 anchor 或場景中的物理位置可能就無法像原來一樣排布。對于這種情況,可能需要選擇性重新開始整個體驗。
以上就是追蹤功能。下面討論一下場景理解。
場景理解
場景理解的目標(biāo)是找出環(huán)境中更多有關(guān)信息,以便在此環(huán)境中放置視覺對象,包括環(huán)境的 3D 拓?fù)湟约肮庹涨闆r等信息。

來看個例子,這是一張桌子。如果想在這張桌上放一個虛擬對象,首先要知道那兒有可以放東西的表面。

- 這一步可以通過平面檢測實現(xiàn)。
- 下一步要找出放置虛擬對象的3D坐標(biāo)系,這一步可以通過命中測試實現(xiàn)。也就是從設(shè)備發(fā)送光線,使之與現(xiàn)實世界相交,以便找出此坐標(biāo)系。
- 為了以更真實的方式放置物體,需要估算光線以匹配環(huán)境中的光線。
下面依次講解上面的三個步驟。
平面檢測

- 平面檢測可以提供相對于重力的水平面。包括地面以及類似桌子等平行平面。
- ARKit 會在后臺聚合多個幀的信息,所以當(dāng)用戶繞著場景移動設(shè)備時,它會掌握更多有關(guān)平面的信息。
- 平面檢測還能校準(zhǔn)平面的邊緣,即在平面所有檢測到的部分四周套上一個矩形,并將其與主要區(qū)域?qū)R。所以從中也能得知物理平面的主要角度。
- 如果同一個物理平面檢測到了多個虛擬平面,ARKit 會負(fù)責(zé)合并它們。組合后的平面會擴(kuò)大至二者范圍,因此后檢測的那些平面就會從 session 中移除。
代碼實現(xiàn)
// 啟用 session 的平面檢測
// 創(chuàng)建新的 world tracking configuration
let configuration = ARWorldTrackingSessionConfiguration()
// 啟用平面檢測
configuration.planeDetection = .horizontal
// 改變運行中 session 的 configuration
mySession.run(configuration)
首先創(chuàng)建一個 ARWorldTrackingSessionConfiguration。平面檢測 planeDetection 是 ARWorldTrackingSessionConfiguration 的一個屬性。要啟用平面檢測,只要設(shè)置 planeDetection 屬性為 horizontal 即可。然后調(diào)用 ARSession 的 run 方法,用 configuration 作為參數(shù),就會開始檢測環(huán)境中的平面。如果想關(guān)掉平面檢測,只要設(shè)置 plane detection 屬性為 None,然后再次對 ARSession 調(diào)用 run 方法即可。session 中之前檢測到的平面都會保留下來,也就是還存在于 ARFrames anchors 中。每當(dāng)新的平面被檢測到時,它們會以 ARPlaneAnchor 形式表示。
ARPlaneAnchor
ARAnchor 用于表示真實世界中的位置和角度,而 ARPlaneAnchor 是它的子類。

檢測到新的 anchor 時,會調(diào)用 delegate 方法:
// 檢測到新平面時調(diào)用
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
addPlaneGeometry(forAnchors: anchors)
}
此方法可以用于如視覺化平面。Extent 就是此平面的范圍,以及一個相對的 center 屬性。

如果用戶繞著場景移動設(shè)備,對平面的了解會增多,所以范圍 extent 可能也會相應(yīng)改變。

此時會調(diào)用 delegate 方法:
// plane 的 transform 或 extent 變化時調(diào)用
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
updatePlaneGeometry(forAnchors: anchors)
}
可以使用此方法更新視覺效果。注意 center 也會相應(yīng)發(fā)生改變,因為平面會向某個方向擴(kuò)張。從 session 中移除 anchor 時,會調(diào)用 delegate 方法:
// 合并時移除 plane 會調(diào)用
func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
removePlaneGeometry(forAnchors: anchors)
}
如果 ARKit 合并了平面并移除了之前的小平面時會調(diào)用此方法,可以相應(yīng)更新視覺效果。
現(xiàn)在我們知道了環(huán)境中都有哪些平面,下面看看如何實際地放點東西進(jìn)去。這一步要使用命中測試。
命中測試

- 命中測試就是從設(shè)備發(fā)送一條光線,并與真實世界相交并找到相交點。
- ARKit 會使用所有能用的場景信息,包括所有檢測到的平面以及3D特征點,這些信息都被 ARWorldTracking 用于確定位置。
- ARKit 然后發(fā)射光與所有能用的場景信息相交,然后用數(shù)組返回所有相交點,以距離升序排序。所以該數(shù)組的第一個元素就是離攝像頭最近的相交點。
- 有不同的相交方式??梢酝ㄟ^ hit-test type 來定義。一共有四種方式,來具體看一看。
命中測試類型
-
Existing plane using extent: 如果在運行平面檢測,并且 ARKit 已在環(huán)境中檢測到了某個平面,就可以利用此平面。但你可以選擇使用平面的范圍或忽視它的范圍。也就是說,例如你想讓用戶在某個平面上移動對象,就應(yīng)該考慮范圍,所以若光在范圍內(nèi)相交,就會產(chǎn)生一個相交點。如果這束光打到了范圍的外面,就不會產(chǎn)生相交點。
image Existing plane: 但如果你只檢測到了地面的一小部分,但希望來回移動家具,就可以選擇忽略范圍,把當(dāng)前平面當(dāng)做無限平面。在這種情況下,你總是會獲得相交點。

-
Estimated plane: 如果沒有在運行平面檢測或者還沒有檢測到某個平面,也可以根據(jù)目前的 3D 特征點來估算平面。在這種情況下,ARKit 會尋找環(huán)境中的處于共同平面的點并為它們安裝一個平面。隨后也返回與此平面的相交點。
imageimage -
Feature point: 如果你想在某個很小的表面上放東西,但此表面無法生成平面,或者是某個非常不規(guī)則的環(huán)境,也可以選擇直接和特征點相交。也就是說光線會與特征點產(chǎn)生相交點,并將距離最近的特征點其作為結(jié)果返回。
image
代碼實現(xiàn)
// 根據(jù)命中測試添加 ARAnchor
let point = CGPoint(x: 0.5, y: 0.5) // 畫面中心
// 對幀執(zhí)行命中測試
let results = frame.hitTest(point, types: [.existingPlane, .estimatedHorizontalPlane])
// 使用第一個結(jié)果
if let closestResult = results.first {
// 用它創(chuàng)建 ARAnchor
let anchor = ARAnchor(transform: closestResult.worldTransform)
// 添加到 session
session.add(anchor: anchor)
}
首先用 CGPoint 來定義從設(shè)備發(fā)射的光線,以標(biāo)準(zhǔn)畫面空間坐標(biāo)系表示。也就是說畫面左上角是 (0,0),右下角是 (1,1)。所以如果我們想讓光從屏幕中心發(fā)射,就用 (0.5,0.5) 來定義 CGPoints。對于 SceneKit 或 SpriteKit,Apple 提供了自定義 overlay,只要發(fā)送這些坐標(biāo)系中的 CGPoint 即可。所以可以通過 touch gesture 使用 UI tap 的結(jié)果作為輸入來定義此光線。將此點用作 histTest 方法的參數(shù),同時還有一個參數(shù)是命中測試類型。本例里用的是 existingPlane,表示會與ARKit 目前檢測到的所有平面相交,同時還有 estimatedHorizontalPlane 可用作沒有檢測到平面時的備選方案。然后 ARKit 會返回結(jié)果數(shù)組。訪問第一個結(jié)果,也就是離相機(jī)最近的相交點。用命中測試結(jié)果 的 worldTransform 屬性來創(chuàng)建一個新的 ARAnchor,并將其添加到 session 以便持續(xù)追蹤。
如果將以上代碼應(yīng)用到下面的場景中,然后把手機(jī)對準(zhǔn)桌子,就會返回屏幕中間與桌子的相交點。

然后在此位置放一個虛擬茶杯。

渲染引擎會默認(rèn)背景畫面有完美的光照條件。所以這增強(qiáng)現(xiàn)實看起來就像真的一樣。但是如果你更暗的環(huán)境中,相機(jī)畫面就會變暗,這是增強(qiáng)現(xiàn)實看上去就有一點出戲,好像在發(fā)光似的。

此時就需要調(diào)整虛擬對象的相對亮度。

因此就需要光線估算。
光線估算
- 光線估算需要使用攝像頭畫面,借助曝光信息來決定相對亮度。
- 對于光照良好的畫面,默認(rèn)為 1000 流明。對于更亮的環(huán)境,會得到更高的值。對于更暗的環(huán)境,會得到更低的值。如果是物理光源,也可以直接把這個值分配給 ambientIntensity 屬性。
- 光線估算是默認(rèn)啟用的??梢酝ㄟ^ ARSessionConfiguration 的 isLightEstimationEnabled 屬性進(jìn)行配置:
configuration.isLightEstimationEnabled = true
光線估算的結(jié)果以 ARFrame 的 lightEstimate 屬性的 ambientIntensity 值表示:
// 獲取 ambient intensity 值
let intensity = frame.lightEstimate?.ambientIntensity
demo
下面來實際看一個 demo,了解如何使用 ARKit 中的場景理解。這個 demo 是 ARKit 示例應(yīng)用,可以從 Apple 的開發(fā)者網(wǎng)站下載。它利用場景理解功能,以便在環(huán)境中放置對象。打開之后,在桌子上來回移動,就可以看到一個對焦矩形。

這一步就是在屏幕中心的位置做命中測試,以便找到相交點來放置物體。所以如果我對著桌子移動,這個矩形也會一起在桌子上滑動。

這一步還使用了平面檢測,我們可以讓平面檢測的過程可視化,以便直觀了解正在發(fā)生的事情。打開這里的 Debug 菜單并激活第二個選項,即 Debug Visualizations。

然后關(guān)閉這個菜單。就可以看到被檢測到的平面了。

為了更好地理解新平面的檢測過程,我們來重新檢測一個平面。如果我指向一個新的平面,然后迅速指向這個平面的另一個位置,就會有兩個平面被檢測到:

但如果我順著這個平面移動,ARKit 會發(fā)現(xiàn)其實這里只有一個平面,所以這兩個平面最終會合并為一個平面。


下面,我們來實際放置一些物體。女朋友讓我在辦公桌上放一些鮮花,我不想讓她失望,所以給這里來一個浪漫的花瓶。

點擊對焦矩形,并選擇添加“Vase”。

這里就是就是用屏幕中心進(jìn)行命中測試并找到相交點,然后放置物體。重點要注意,這個花瓶是以真實世界的比例出現(xiàn)的,這是因為以下兩點:1、世界追蹤為我們提供了縮放比例;2、3D 模型是在真實世界坐標(biāo)系中構(gòu)建的。所以要為增強(qiáng)現(xiàn)實創(chuàng)建內(nèi)容的話,一定要考慮第2點,花瓶不應(yīng)該和一棟建筑物一樣高,也不應(yīng)該太小。
以上就是示例程序??梢詮?Apple 的網(wǎng)站上下載并試著放入自己的內(nèi)容。
渲染
下面我們來看看如何用 ARKit 進(jìn)行渲染。渲染需要追蹤、場景理解以及你的內(nèi)容。要用 ARKit 渲染,需要處理 ARFrame 中提供的所有信息。

對于 SceneKit 和 SpriteKit,Apple 提供了可定制化的視圖來為你處理 ARFrame 并進(jìn)行渲染。而對于 Metal,可以創(chuàng)建自己的渲染引擎或?qū)?ARKit 整合進(jìn)目前的渲染引擎,Apple 提供了一張模板與它的使用介紹,使用此模板是一個很好的出發(fā)點。下面挨個講解它們。
SceneKit

對于 SceneKit,Apple 提供了 ARSCNView,它是 SCNView 的子類,包含一個 ARSession,用于更新渲染。
ARSCNView
- ARSCNView 會繪制攝像頭背景畫面。
- ARSCNView 會根據(jù) ARCamera 中的追蹤 transform 更新 SCNCamera。場景保持不變,ARKit 只是控制 SCNCamera 在場景中移動,模擬用戶在現(xiàn)實世界中來回移動設(shè)備。
- 使用光線估算時,ARSCNView 會自動在場景中放置一個 SCNLight 來更新光照。
- ARSCNView 會將 SCNNodees 映射到 ARAnchors,所以實際上不需要直接操作 ARAnchors,使用 SCNodees 即可。當(dāng)一個新的 ARAnchor 被添加到 session 時,ARSCNView 也會創(chuàng)建一個 node。每次更新 ARAnchor 時,例如改變 transform,也會自動更新 nodes 的 transform。這一步是通過 ARSCNView delegate 來實現(xiàn)的。
ARSCNViewDelegate
session 每次添加新的 anchor 時,ARSCNView 都會創(chuàng)建一個新的 SCNNode。如果想用自定義 node,可以實現(xiàn) renderer nodeFor anchor 方法并返回自定義 node。

然后 SCNNode 會被添加到場景中,此時會接收另一個 delegate 方法:

node 被更新時同樣也會接收 delegate 方法,例如 ARAnchor 的 transform 改變時,DSCNNode 的 transform 也會自動改變,此時會收到兩個回調(diào)。transform 更新前一個,更新后一個。

從 session 中移除 ARAnchor 時,也會自動從場景中移除對應(yīng)的 SCNNode,并調(diào)用 renderer didRemove node for anchor:

以上就是 ARKit 中的 SceneKit。下面來看 SpriteKit。
SpriteKit

對于 SpriteKit,Apple 提供了 ARSKView,它是 SKView 的子類。
ARSKView
- ARSKView 包含 ARSession,用于更新渲染。
- ARSKView 會繪制攝像頭背景畫面。
- ARSKView 會將 SKNodes 映射到 ARAnchors。
ARSKView 提供的 delegate 方法與 SceneKit 很相似。主要的區(qū)別是 SpriteKit 是 2D 渲染引擎,這表示不能簡單地移動攝像頭,其實這里 ARKit 是將 ARAnchor 的位置投影到 SpriteKit 視圖上,然后把 Sprites 渲染為投影位置上的廣告牌(billboard),也就是說 Sprites 總是會面對攝像頭。
如果想更多了解相關(guān)內(nèi)容,可以去看來自 SpriteKit 團(tuán)隊的 session,“Going beyond 2-D in SpriteKit”,會講如何整合 ARKit 和 SpriteKit。
下面來看看如何借助 Metal 在 ARKit 中實現(xiàn)自定義渲染。
自定義渲染

處理流程
ARKit 的渲染主要要做四件事。
- 繪制攝像頭背景畫面。通常需要創(chuàng)建紋理并繪制在背景。
- 根據(jù) ARCamera 更新虛擬攝像頭。需要設(shè)置視圖矩陣和投影矩陣。
- 根據(jù)光線估算更新場景中的光線。
- 如果基于場景理解放置了幾何體,使用 ARAnchor 來正確設(shè)置 transform。
這些所需的信息都被包含在 ARFrame 中。有兩種方式獲取 ARFrame。
訪問 ARFrame
第一種方式是通過 ARSession 上的 currentFrame 屬性。
if let frame = mySession.currentFrame {
if( frame.timestamp > _lastTimestamp ) {
updateRenderer(frame) // 用此幀更新渲染程序
_lastTimestamp = frame.timestamp
}
}
如果有自己的渲染循環(huán),你可以使用此方法來訪問 currentFrame。同時還要利用 ARFrame 的 timestamp 屬性以避免多次渲染同一幀。
另一種方式是使用 Session Delegate,每次計算新的一幀時,都會調(diào)用 session didUpdate frame 方法:
func session(_ session: ARSession, didUpdate frame: ARFrame) {
// 用此幀更新渲染程序
updateRenderer(frame)
}
此時可以用它來更新渲染。此方法默認(rèn)在主線程被調(diào)用,但你也可以提供自己的 dispatch queue 來調(diào)用它。下面來看看具體如何更新渲染。
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)
// 根據(jù) anchors 更新幾何體
drawGeometry(forAnchors: frame.anchors)
}
首先要繪制攝像頭背景畫面。可以訪問 AFFrame 上的 capturedImage 屬性,這是 CVPixel Buffer。然后可以基于此 Pixel Buffer 生成 Metal texture,并在背景四邊形中進(jìn)行繪制。要注意由于這是通過 AV Foundation 暴露給我們的 Pixel Buffer,所以不應(yīng)持有這些幀太多或太久,否則就會停止接收更新。下一步是根據(jù) ARCamera 更新虛擬攝像頭,因此需要確定視圖矩陣以及投影矩陣。視圖矩陣即 camera transform 的逆矩陣。Apple 在 ARCamera 上提供了一個 convenience 方法,以幫助我們生成投影矩陣。第三步是更新光線,訪問 lightEstimate 屬性并使用它的 ambientIntensity 值來更新光照模型。最后一步是遍歷 anchors 和 anchors 的3D位置,并更新幾何體的 transform,包括手動添加到 session 的 anchor 以及平面檢測自動添加的 anchor。
繪制到 viewport
繪制攝像頭畫面時還有兩點需要注意,一是 ARFrame 中的 captured iamge 總是以相同的角度提供。所以如果用戶旋轉(zhuǎn)了物理設(shè)備,畫面可能對不上用戶界面,此時需要應(yīng)用 transform 來正確渲染。

二是攝像頭畫面的寬高比可能與設(shè)備不同。所以需要考慮這一點以便正確渲染屏幕中的的攝像頭畫面。

幫助方法
對于以上兩點,Apple 提供了方便的幫助方法。ARFrame 有一個 displayTransform 方法:
// 給定 viewport 尺寸和角度并獲得此幀的 display transform
let transform = frame.displayTransform(withViewportSize: viewportSize, orientation: .portrait)
此方法可以獲得從幀空間到視圖空間的 transform。只要提供 view port 的尺寸以及界面角度,就會得到對應(yīng)的 transform。在 Metal 那個例子里,使用此 transform 的逆矩陣來調(diào)整相機(jī)畫面的紋理坐標(biāo)。
// 給定 viewport 尺寸和角度并獲得攝像頭的投影矩陣
let projectionMatrix = camera.projectionMatrix(withViewportSize: viewportSize, orientation: .portrait,
zNear: 0.001, zFar: 1000)
同時還有投影矩陣方差,給它用戶界面角度以及 view port 尺寸,并告知平面裁剪(clipping planes)限制,然后就可以用得到的投影矩陣在相機(jī)畫面上正確繪制虛擬內(nèi)容。以上就是關(guān)于 ARKit 的介紹。
總結(jié)
ARKit 是 high-level API,為在 iOS 上創(chuàng)建增強(qiáng)現(xiàn)實應(yīng)用而設(shè)計。ARKit 提供 WorldTracking 功能,能夠得到設(shè)備相對于起始位置的相對位置。為了在現(xiàn)實世界中放置物體,ARKit 還提供了場景理解功能。場景理解可以檢測平面,也能夠?qū)φ鎸嵤澜邕M(jìn)行命中測試來找到3D坐標(biāo)系并在那里放置物體。同時為了提升增強(qiáng)內(nèi)容的真實性,ARKit 提供了基于攝像頭畫面的光線估算。ARKit 還提供了與 SceneKit 和 SpriteKit 的定制化整合,如果想自己開發(fā)渲染引擎的話,同時還有一張 Metal 模板供你采用。






