ARKit介紹
AR 全稱 Augmented Reality(增強現實)是一種在視覺上呈現虛擬物體與現實場景結合的技術。Apple 公司在 2017 年 6 月正式推出了 ARKit,iOS 開發者可以在這個平臺上使用簡單便捷的 API 來開發 AR 應用程序。為了獲得 ARKit 的完整功能,需要 A9 及以上芯片。其實也就是大部分運行 iOS 11 的設備,包括 iPhone 6S。
研究過程中,做了一個卷尺的Demo,現在介紹下項目中用到的技術點。
項目實踐
iOS 平臺的 AR 應用通常由 ARKit 和渲染引擎兩部分構成:
ARKit
ARKit 的 ARSession 負責管理每一幀的信息。ARSession 做了兩件事:拍攝圖像并獲取傳感器數據;對數據進行分析處理后逐幀輸出。如下圖:
設備追蹤
設備追蹤確保了虛擬物體的位置不受設備移動的影響。在啟動 ARSession 時需要傳入一個 ARSessionConfiguration 的子類對象,以區別三種追蹤模式:
- ARFaceTrackingConfiguration
- ARWorldTrackingConfiguration
- AROrientationTrackingConfiguration
其中 ARFaceTrackingConfiguration 可以識別人臉的位置、方向以及獲取拓撲結構。此外,還可以探測到預設的 52 種豐富的面部動作,如眨眼、微笑、皺眉等等。ARFaceTrackingConfiguration 需要調用支持 TrueDepth 的前置攝像頭進行追蹤。
本項目主要是使用ARWorldTrackingConfiguration進行追蹤,獲取特征點。
追蹤步驟
// 創建一個 ARSessionConfiguration.
// 暫時無需在意 ARWorldTrackingSessionConfiguration.
let configuration = ARWorldTrackingSessionConfiguration()
// Create a session.
let session = ARSession()
// Run.
session.run(configuration)
從上面的代碼看,運行一個 ARSession 的過程是很簡單的,那么 ARSession 的底層如何進行世界追蹤的呢?
- 首先,ARSession 底層使用了 AVCaputreSession 來獲取攝像機拍攝的視頻(一幀一幀的圖像序列)。
- 其次,ARSession 底層使用了 CMMotionManager 來獲取設備的運動信息(比如旋轉角度、移動距離等)
- 最后,ARSession 根據獲取的圖像序列以及設備的運動信息進行分析,最后輸出 ARFrame,ARFrame 中就包含有渲染虛擬世界所需的所有信息。
追蹤信息點
AR-World 的坐標系如下,當我們運行 ARSession 時設備所在的位置就是 AR-World 的坐標系原點。
在這個 AR-World 坐標系中,ARKit 會追蹤以下幾個信息:
- 追蹤設備的位置以及旋轉,這里的兩個信息均是相對于設備起始時的信息。
- 追蹤物理距離(以“米”為單位),例如 ARKit 檢測到一個平面,我們希望知道這個平面有多大。
- 追蹤我們手動添加的希望追蹤的點,例如我們手動添加的一個虛擬物體。
追蹤如何工作
蘋果文檔中對世界追蹤過程是這么解釋的:ARKit使用視覺慣性測距技術,對攝像頭采集到的圖像序列進行計算機視覺分析,并且與設備的運動傳感器信息相結合。ARKit 會識別出每一幀圖像中的特征點,并且根據特征點在連續的圖像幀之間的位置變化,然后與運動傳感器提供的信息進行比較,最終得到高精度的設備位置和偏轉信息。
- 上圖中劃出曲線的運動的點代表設備,可以看到以設備為中心有一個坐標系也在移動和旋轉,這代表著設備在不斷的移動和旋轉。這個信息是通過設備的運動傳感器獲取的。
- 動圖中右側的黃色點是 3D 特征點。3D特征點就是處理捕捉到的圖像得到的,能代表物體特征的點。例如地板的紋理、物體的邊邊角角都可以成為特征點。上圖中我們看到當設備移動時,ARKit 在不斷的追蹤捕捉到的畫面中的特征點。
- ARKit 將上面兩個信息進行結合,最終得到了高精度的設備位置和偏轉信息。
ARWorldTrackingConfiguration
ARWorldTrackingConfiguration 提供 6DoF(Six Degree of Freedom)的設備追蹤。包括三個姿態角 Yaw(偏航角)、Pitch(俯仰角)和 Roll(翻滾角),以及沿笛卡爾坐標系中 X、Y 和 Z 三軸的偏移量:
不僅如此,ARKit 還使用了 VIO(Visual-Inertial Odometry)來提高設備運動追蹤的精度。在使用慣性測量單元(IMU)檢測運動軌跡的同時,對運動過程中攝像頭拍攝到的圖片進行圖像處理。將圖像中的一些特征點的變化軌跡與傳感器的結果進行比對后,輸出最終的高精度結果。
從追蹤的維度和準確度來看,ARWorldTrackingConfiguration 非常強悍。但如官方文檔所言,它也有兩個致命的缺點:
- 受環境光線質量影響
- 受劇烈運動影響
由于在追蹤過程中要通過采集圖像來提取特征點,所以圖像的質量會影響追蹤的結果。在光線較差的環境下(比如夜晚或者強光),拍攝的圖像無法提供正確的參考,追蹤的質量也會隨之下降。
追蹤過程中會逐幀比對圖像與傳感器結果,如果設備在短時間內劇烈的移動,會很大程度上干擾追蹤結果。
追蹤狀態
世界追蹤有三種狀態,我們可以通過 camera.trackingState 獲取當前的追蹤狀態。
從上圖我們看到有三種追蹤狀態:
- Not Available:世界追蹤正在初始化,還未開始工作。
- Normal: 正常工作狀態。
- Limited:限制狀態,當追蹤質量受到影響時,追蹤狀態可能會變為 Limited 狀態。
與 TrackingState 關聯的一個信息是 ARCamera.TrackingState.Reason,這是一個枚舉類型:
- case excessiveMotion:設備移動過快,無法正常追蹤。
- case initializing:正在初始化。
- case insufficientFeatures:特征過少,無法正常追蹤。
- case none:正常工作。
我們可以通過 ARSessionObserver 協議去獲取追蹤狀態的變化,比較簡單,可以直接查看接口文檔。
ARFrame
ARFrame 中包含有世界追蹤過程獲取的所有信息,ARFrame 中與世界追蹤有關的信息主要是:anchors 和 camera:
- camera: 含有攝像機的位置、旋轉以及拍照參數等信息。
var camera: [ARCamera]
- ahchors: 代表了追蹤的點或面。
var anchors: [ARAnchor]
ARAnchor
- ARAnchor 是空間中相對真實世界的位置和角度。
- ARAnchor 可以添加到場景中,或是從場景中移除?;旧蟻碚f,它們用于表示虛擬內容在物理環境中的錨定。所以如果要添加自定義 anchor,添加到 session 里就可以了。它會在 session 生命周期中一直存在。但如果你在運行諸如平面檢測功能,ARAnchor 則會被自動添加到 session 中。
- 要響應被添加的 anchor,可以從 current ARFrame 中獲得完整列表,此列表包含 session 正在追蹤的所有 anchor。
- 或者也可以響應 delegate 方法,例如 add、update 以及 remove,session 中的 anchor 被添加、更新或移除時會通知。
ARCamera
每個 ARFrame 都會包含一個 ARCamera。ARCamera 對象表示虛擬攝像頭。虛擬攝像頭就代表了設備的角度和位置。
- ARCamera 提供了一個 transform。transform 是一個 4x4 矩陣。提供了物理設備相對于初始位置的變換。
- ARCamera 提供了追蹤狀態(tracking state),通知你如何使用 transform,這個在后面會講。
- ARCamera 提供了相機內部功能(camera intrinsics)。包括焦距和主焦點,用于尋找投影矩陣。投影矩陣是 ARCamera 上的一個 convenience 方法,可用于渲染虛擬你的幾何體。
場景解析
場景解析主要功能是對現實世界的場景進行分析,解析出比如現實世界的平面等信息,可以讓我們把一些虛擬物體放在某些實物處。ARKit 提供的場景解析主要有平面檢測、場景交互以及光照估計三種,下面逐個分析。
平面檢測(Plane detection)
- ARKit 的平面檢測用于檢測出現實世界的水平面。
上圖中可以看出,ARkit 檢測出了兩個平面,圖中的兩個三維坐標系是檢測出的平面的本地坐標系,此外,檢測出的平面是有一個大小范圍的。
- 平面檢測是一個動態的過程,當攝像機不斷移動時,檢測到的平面也會不斷的變化。下圖中可以看到當移動攝像機時,已經檢測到的平面的坐標原點以及平面范圍都在不斷的變化。
- 此外,隨著平面的動態檢測,不同平面也可能會合并為一個新的平面。下圖中可以看到已經檢測到的平面隨著攝像機移動合并為了一個平面。
- 開啟平面檢測
開啟平面檢測很簡單,只需要在 run ARSession 之前,將 ARSessionConfiguration 的 planeDetection 屬性設為 true 即可。
// Create a world tracking session configuration.
let configuration = ARWorldTrackingSessionConfiguration()
configuration.planeDetection = .horizontal
// Create a session.
let session = ARSession()
// Run.
session.run(configuration)
- 平面的表示方式
當 ARKit 檢測到一個平面時,ARKit 會為該平面自動添加一個 ARPlaneAnchor,這個 ARPlaneAnchor 就表示了一個平面。
- 當 ARKit 系統檢測到新平面時,ARKit 會自動添加一個 ARPlaneAnchor 到 ARSession 中。我們可以通過 ARSessionDelegate 獲取當前 ARSession 的 ARAnchor 改變的通知,主要有以下三種情況:
新加入了 ARAnchor
func session(_ session: ARSession, didAdd anchors: [ARAnchor])
對于平面檢測來說,當新檢測到某平面時,我們會收到該通知,通知中的 ARAnchor 數組會包含新添加的平面,其類型是 ARPlaneAnchor,我們可以像下面這樣使用:
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
for anchor in anchors {
if let anchor = anchor as? ARPlaneAnchor {
print(anchor.center)
print(anchor.extent)
}
}
}
ARAnchor 更新
func session(_ session: ARSession, didUpdate anchors: [ARAnchor])
從上面我們知道當設備移動時,檢測到的平面是不斷更新的,當平面更新時,會回調這個接口。
刪除 ARAnchor
func session(_ session: ARSession, didRemove anchors: [ARAnchor])
當手動刪除某個 Anchor 時,會回調此方法。此外,對于檢測到的平面來說,如果兩個平面進行了合并,則會刪除其中一個,此時也會回調此方法。
場景交互(Hit-testing)
Hit-testing 是為了獲取當前捕捉到的圖像中某點擊位置有關的信息(包括平面、特征點、ARAnchor 等)。
原理圖如下
當點擊屏幕時,ARKit 會發射一個射線,假設屏幕平面是三維坐標系中的 xy 平面,那么該射線會沿著 z 軸方向射向屏幕里面,這就是一次 Hit-testing 過程。此次過程會將射線遇到的所有有用信息返回,返回結果以離屏幕距離進行排序,離屏幕最近的排在最前面。
ARFrame 提供了 Hit-testing 的接口:
func hitTest(_ point: CGPoint, types: ARHitTestResult.ResultType) -> [ARHitTestResult]
上述接口中有一個 types 參數,該參數表示此次 Hit-testing 過程需要獲取的信息類型。ResultType 有以下四種:
- featurePoint
表示此次 Hit-testing 過程希望返回當前圖像中 Hit-testing 射線經過的 3D 特征點。如下圖:
- estimatedHorizontalPlane
表示此次 Hit-testing 過程希望返回當前圖像中 Hit-testing 射線經過的預估平面。預估平面表示 ARKit 當前檢測到一個可能是平面的信息,但當前尚未確定是平面,所以 ARKit 還沒有為此預估平面添加 ARPlaneAnchor。如下圖:
- existingPlaneUsingExtent
表示此次 Hit-testing 過程希望返回當前圖像中 Hit-testing 射線經過的有大小范圍的平面。
上圖中,如果 Hit-testing 射線經過了有大小范圍的綠色平面,則會返回此平面,如果射線落在了綠色平面的外面,則不會返回此平面。
- existingPlane
表示此次 Hit-testing 過程希望返回當前圖像中 Hit-testing 射線經過的無限大小的平面。
上圖中,平面大小是綠色平面所展示的大小,但 exsitingPlane 選項表示即使 Hit-testing 射線落在了綠色平面外面,也會將此平面返回。換句話說,將所有平面無限延展,只要 Hit-testing 射線經過了無限延展后的平面,就會返回該平面。
示例代碼如下
// Adding an ARAnchor based on hit-test
let point = CGPoint(x: 0.5, y: 0.5) // Image center
// Perform hit-test on frame.
let results = frame. hitTest(point, types: [.featurePoint, .estimatedHorizontalPlane])
// Use the first result.
if let closestResult = results.first {
// Create an anchor for it.
anchor = ARAnchor(transform: closestResult.worldTransform)
// Add it to the session.
session.add(anchor: anchor)
}
上面代碼中,Hit-testing 的 point(0.5, 0.5)代表屏幕的中心,屏幕左上角為(0, 0),右下角為(1, 1)。 對于 featurePoint 和 estimatedHorizontalPlane 的結果,ARKit 沒有為其添加 ARAnchor,我們可以使用 Hit-testing 獲取信息后自己為 ARSession 添加 ARAnchor,上面代碼就顯示了此過程。
光照估計(Light estimation)
上圖中,一個虛擬物體茶杯被放在了現實世界的桌子上。
當周圍環境光線較好時,攝像機捕捉到的圖像光照強度也較好,此時,我們放在桌子上的茶杯看起來就比較貼近于現實效果,如上圖最左邊的圖。但是當周圍光線較暗時,攝像機捕捉到的圖像也較暗,如上圖中間的圖,此時茶杯的亮度就顯得跟現實世界格格不入。
針對這種情況,ARKit 提供了光照估計,開啟光照估計后,我們可以拿到當前圖像的光照強度,從而能夠以更自然的光照強度去渲染虛擬物體,如上圖最右邊的圖。
光照估計基于當前捕捉到的圖像的曝光等信息,給出一個估計的光照強度值(單位為 lumen,光強單位)。默認的光照強度為 1000lumen,當現實世界較亮時,我們可以拿到一個高于 1000lumen 的值,相反,當現實世界光照較暗時,我們會拿到一個低于 1000lumen 的值。
ARKit 的光照估計默認是開啟的,當然也可以通過下述方式手動配置:
configuration.isLightEstimationEnabled = true
獲取光照估計的光照強度也很簡單,只需要拿到當前的 ARFrame,通過以下代碼即可獲取估計的光照強度:
let intensity = frame.lightEstimate?.ambientIntensity
SceneKit
渲染是呈現 AR world 的最后一個過程。此過程將創建的虛擬世界、捕捉的真實世界、ARKit 追蹤的信息以及 ARKit 場景解析的的信息結合在一起,渲染出一個 AR world。渲染過程需要實現以下幾點才能渲染出正確的 AR world:
- 將攝像機捕捉到的真實世界的視頻作為背景。
- 將世界追蹤到的相機狀態信息實時更新到 AR world 中的相機。
- 處理光照估計的光照強度。
- 實時渲染虛擬世界物體在屏幕中的位置。
如果我們自己處理這個過程,可以看到還是比較復雜的,ARKit 為簡化開發者的渲染過程,為開發者提供了簡單易用的使用 SceneKit(3D 引擎)以及 SpriteKit(2D 引擎)渲染的視圖ARSCNView以及ARSKView。當然開發者也可以使用其他引擎進行渲染,只需要將以上幾個信息進行處理融合即可。
SceneKit 的坐標系
我們知道 UIKit 使用一個包含有 x 和 y 信息的 CGPoint 來表示一個點的位置,但是在 3D 系統中,需要一個 z 參數來描述物體在空間中的深度,SceneKit 的坐標系可以參考下圖:
這個三維坐標系中,表示一個點的位置需要使用(x,y,z)坐標表示。紅色方塊位于 x 軸,綠色方塊位于 y 軸,藍色方塊位于 z 軸,灰色方塊位于原點。在 SceneKit 中我們可以這樣創建一個三維坐標:
let position = SCNVector3(x: 0, y: 5, z: 10)
SceneKit 中的場景和節點
我們可以將 SceneKit 中的場景(SCNScene)想象為一個虛擬的 3D 空間,然后可以將一個個的節點(SCNNode)添加到場景中。SCNScene 中有唯一一個根節點(坐標是(x:0, y:0, z:0)),除了根節點外,所有添加到 SCNScene 中的節點都需要一個父節點。
下圖中位于坐標系中心的就是根節點,此外還有添加的兩個節點 NodeA 和 NodeB,其中 NodeA 的父節點是根節點,NodeB 的父節點是 NodeA:
SCNScene 中的節點加入時可以指定一個三維坐標(默認為(x:0, y:0, z:0)),這個坐標是相對于其父節點的位置。這里說明兩個概念:
- 本地坐標系:以場景中的某節點(非根節點)為原點建立的三維坐標系
- 世界坐標系:以根節點為原點創建的三維坐標系稱為世界坐標系。
上圖中我們可以看到 NodeA 的坐標是相對于世界坐標系(由于 NodeA 的父節點是根節點)的位置,而 NodeB 的坐標代表了 NodeB 在 NodeA 的本地坐標系位置(NodeB 的父節點是 NodeA)。
SceneKit 中的攝像機
有了 SCNScene 和 SCNNode 后,我們還需要一個攝像機(SCNCamera)來決定我們可以看到場景中的哪一塊區域(就好比現實世界中有了各種物體,但還需要人的眼睛才能看到物體)。攝像機在 SCNScene 的工作模式如下圖:
上圖中包含以下幾點信息:
- SceneKit 中 SCNCamera 拍攝的方向始終為 z 軸負方向。
- 視野(Field of View)是攝像機的可視區域的極限角度。角度越小,視野越窄,反之,角度越大,視野越寬。
- 視錐體(Viewing Frustum)決定著攝像頭可視區域的深度(z 軸表示深度)。任何不在這個區域內的物體將被剪裁掉(離攝像頭太近或者太遠),不會顯示在最終的畫面中。
在 SceneKit 中我們可以使用如下方式創建一個攝像機:
let scene = SCNScene()
let cameraNode = SCNNode()
let camera = SCNCamera()
cameraNode.camera = camera
cameraNode.position = SCNVector3(x: 0, y: 0, z: 0)
scene.rootNode.addChildNode(cameraNode)
SCNView
最后,我們需要一個 View 來將 SCNScene 中的內容渲染到顯示屏幕上,這個工作由 SCNView 完成。這一步其實很簡單,只需要創建一個 SCNView 實例,然后將 SCNView 的 scene 屬性設置為剛剛創建的 SCNScene,然后將 SCNView 添加到 UIKit 的 view 或 window 上即可。示例代碼如下:
let scnView = SCNView()
scnView.scene = scene
vc.view.addSubview(scnView)
scnView.frame = vc.view.bounds