ARKit介紹
AR 全稱 Augmented Reality(擴增實境)是一種在視覺上呈現虛擬物體與現實場景結合的技術。Apple 公司在 2017 年 6 月正式推出了 ARKit,iOS 開發者可以在這個平臺上使用簡單便捷的 API 來開發 AR 應用程式。為了獲得 ARKit 的完整功能,需要 A9 及以上晶片。其實也就是大部分執行 iOS 11 的裝置,包括 iPhone 6S。
研究過程中,做了一個捲尺的Demo,現在介紹下專案中用到的技術點。
專案實踐
iOS 平臺的 AR 應用通常由 ARKit 和渲染引擎兩部分構成:
下面主要說說ARKit的功能點和渲染部分SceneKit兩個方面。
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 可以新增到場景中,或是從場景中移除。基本上來說,它們用於表示虛擬內容在物理環境中的錨定。所以如果要新增自定義 anchor,新增到 session 裡就可以了。它會在 session 生命週期中一直存在。但如果你在執行諸如平面檢測功能,ARAnchor 則會被自動新增到 session 中。
- 要響應被新增的 anchor,可以從 current ARFrame 中獲得完整列表,此列表包含 session 正在追蹤的所有 anchor。
- 或者也可以響應 delegate 方法,例如 add、update 以及 remove,session 中的 anchor 被新增、更新或移除時會通知。
ARCamera
每個 ARFrame 都會包含一個 ARCamera。ARCamera 物件表示虛擬攝像頭。虛擬攝像頭就代表了裝置的角度和位置。
- ARCamera 提供了一個 transform。transform 是一個 4×4 矩陣。提供了物理裝置相對於初始位置的變換。
- 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
複製程式碼
關注我
歡迎關注公眾號:jackyshan,技術乾貨首發微信,第一時間推送。