AR 全稱 Augmented Reality(加強現實)是一種在視覺上呈現虛擬物體與現實場景結合的技術。Apple 公司在 2017 年 6 月正式推出了 ARKit,iOS 開發者能夠在這個平臺上使用簡單便捷的 API 來開發 AR 應用程序。爲了得到 ARKit 的完整功能,須要 A9 及以上芯片。其實也就是大部分運行 iOS 11 的設備,包括 iPhone 6S。數組
研究過程當中,作了一個捲尺的Demo,如今介紹下項目中用到的技術點。bash
iOS 平臺的 AR 應用一般由 ARKit 和渲染引擎兩部分構成:微信
下面主要說說ARKit的功能點和渲染部分SceneKit兩個方面。session
ARKit 的 ARSession 負責管理每一幀的信息。ARSession 作了兩件事:拍攝圖像並獲取傳感器數據;對數據進行分析處理後逐幀輸出。以下圖:dom
設備追蹤確保了虛擬物體的位置不受設備移動的影響。在啓動 ARSession 時須要傳入一個 ARSessionConfiguration 的子類對象,以區別三種追蹤模式:ui
其中 ARFaceTrackingConfiguration 能夠識別人臉的位置、方向以及獲取拓撲結構。此外,還能夠探測到預設的 52 種豐富的面部動做,如眨眼、微笑、皺眉等等。ARFaceTrackingConfiguration 須要調用支持 TrueDepth 的前置攝像頭進行追蹤。 本項目主要是使用ARWorldTrackingConfiguration進行追蹤,獲取特徵點。spa
// 建立一個 ARSessionConfiguration.
// 暫時無需在乎 ARWorldTrackingSessionConfiguration.
let configuration = ARWorldTrackingSessionConfiguration()
// Create a session.
let session = ARSession()
// Run.
session.run(configuration)
複製代碼
從上面的代碼看,運行一個 ARSession 的過程是很簡單的,那麼 ARSession 的底層如何進行世界追蹤的呢?3d
AR-World 的座標系以下,當咱們運行 ARSession 時設備所在的位置就是 AR-World 的座標系原點。code
在這個 AR-World 座標系中,ARKit 會追蹤如下幾個信息:orm
蘋果文檔中對世界追蹤過程是這麼解釋的:ARKit使用視覺慣性測距技術,對攝像頭採集到的圖像序列進行計算機視覺分析,而且與設備的運動傳感器信息相結合。ARKit 會識別出每一幀圖像中的特徵點,而且根據特徵點在連續的圖像幀之間的位置變化,而後與運動傳感器提供的信息進行比較,最終獲得高精度的設備位置和偏轉信息。
ARWorldTrackingConfiguration 提供 6DoF(Six Degree of Freedom)的設備追蹤。包括三個姿態角 Yaw(偏航角)、Pitch(俯仰角)和 Roll(翻滾角),以及沿笛卡爾座標系中 X、Y 和 Z 三軸的偏移量:
不只如此,ARKit 還使用了 VIO(Visual-Inertial Odometry)來提升設備運動追蹤的精度。在使用慣性測量單元(IMU)檢測運動軌跡的同時,對運動過程當中攝像頭拍攝到的圖片進行圖像處理。將圖像中的一些特徵點的變化軌跡與傳感器的結果進行比對後,輸出最終的高精度結果。 從追蹤的維度和準確度來看,ARWorldTrackingConfiguration 很是強悍。但如官方文檔所言,它也有兩個致命的缺點:
因爲在追蹤過程當中要經過採集圖像來提取特徵點,因此圖像的質量會影響追蹤的結果。在光線較差的環境下(好比夜晚或者強光),拍攝的圖像沒法提供正確的參考,追蹤的質量也會隨之降低。
追蹤過程當中會逐幀比對圖像與傳感器結果,若是設備在短期內劇烈的移動,會很大程度上干擾追蹤結果。
世界追蹤有三種狀態,咱們能夠經過 camera.trackingState 獲取當前的追蹤狀態。
從上圖咱們看到有三種追蹤狀態:
與 TrackingState 關聯的一個信息是 ARCamera.TrackingState.Reason,這是一個枚舉類型:
咱們能夠經過 ARSessionObserver 協議去獲取追蹤狀態的變化,比較簡單,能夠直接查看接口文檔。
ARFrame 中包含有世界追蹤過程獲取的全部信息,ARFrame 中與世界追蹤有關的信息主要是:anchors 和 camera:
var camera: [ARCamera]
複製代碼
var anchors: [ARAnchor]
複製代碼
每一個 ARFrame 都會包含一個 ARCamera。ARCamera 對象表示虛擬攝像頭。虛擬攝像頭就表明了設備的角度和位置。
場景解析主要功能是對現實世界的場景進行分析,解析出好比現實世界的平面等信息,可讓咱們把一些虛擬物體放在某些實物處。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 就表示了一個平面。
新加入了 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 是爲了獲取當前捕捉到的圖像中某點擊位置有關的信息(包括平面、特徵點、ARAnchor 等)。
原理圖以下
當點擊屏幕時,ARKit 會發射一個射線,假設屏幕平面是三維座標系中的 xy 平面,那麼該射線會沿着 z 軸方向射向屏幕裏面,這就是一次 Hit-testing 過程。這次過程會將射線遇到的全部有用信息返回,返回結果以離屏幕距離進行排序,離屏幕最近的排在最前面。
ARFrame 提供了 Hit-testing 的接口:
func hitTest(_ point: CGPoint, types: ARHitTestResult.ResultType) -> [ARHitTestResult]
複製代碼
上述接口中有一個 types 參數,該參數表示這次 Hit-testing 過程須要獲取的信息類型。ResultType 有如下四種:
表示這次 Hit-testing 過程但願返回當前圖像中 Hit-testing 射線通過的 3D 特徵點。以下圖:
表示這次 Hit-testing 過程但願返回當前圖像中 Hit-testing 射線通過的預估平面。預估平面表示 ARKit 當前檢測到一個多是平面的信息,但當前還沒有肯定是平面,因此 ARKit 尚未爲此預估平面添加 ARPlaneAnchor。以下圖:
表示這次 Hit-testing 過程但願返回當前圖像中 Hit-testing 射線通過的有大小範圍的平面。
上圖中,若是 Hit-testing 射線通過了有大小範圍的綠色平面,則會返回此平面,若是射線落在了綠色平面的外面,則不會返回此平面。
表示這次 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,上面代碼就顯示了此過程。
上圖中,一個虛擬物體茶杯被放在了現實世界的桌子上。
當週圍環境光線較好時,攝像機捕捉到的圖像光照強度也較好,此時,咱們放在桌子上的茶杯看起來就比較貼近於現實效果,如上圖最左邊的圖。可是當週圍光線較暗時,攝像機捕捉到的圖像也較暗,如上圖中間的圖,此時茶杯的亮度就顯得跟現實世界格格不入。
針對這種狀況,ARKit 提供了光照估計,開啓光照估計後,咱們能夠拿到當前圖像的光照強度,從而可以以更天然的光照強度去渲染虛擬物體,如上圖最右邊的圖。
光照估計基於當前捕捉到的圖像的曝光等信息,給出一個估計的光照強度值(單位爲 lumen,光強單位)。默認的光照強度爲 1000lumen,當現實世界較亮時,咱們能夠拿到一個高於 1000lumen 的值,相反,當現實世界光照較暗時,咱們會拿到一個低於 1000lumen 的值。
ARKit 的光照估計默認是開啓的,固然也能夠經過下述方式手動配置:
configuration.isLightEstimationEnabled = true
複製代碼
獲取光照估計的光照強度也很簡單,只須要拿到當前的 ARFrame,經過如下代碼便可獲取估計的光照強度:
let intensity = frame.lightEstimate?.ambientIntensity
複製代碼
渲染是呈現 AR world 的最後一個過程。此過程將建立的虛擬世界、捕捉的真實世界、ARKit 追蹤的信息以及 ARKit 場景解析的的信息結合在一塊兒,渲染出一個 AR world。渲染過程須要實現如下幾點才能渲染出正確的 AR world:
若是咱們本身處理這個過程,能夠看到仍是比較複雜的,ARKit 爲簡化開發者的渲染過程,爲開發者提供了簡單易用的使用 SceneKit(3D 引擎)以及 SpriteKit(2D 引擎)渲染的視圖ARSCNView以及ARSKView。固然開發者也可使用其餘引擎進行渲染,只須要將以上幾個信息進行處理融合便可。
咱們知道 UIKit 使用一個包含有 x 和 y 信息的 CGPoint 來表示一個點的位置,可是在 3D 系統中,須要一個 z 參數來描述物體在空間中的深度,SceneKit 的座標系能夠參考下圖:
這個三維座標系中,表示一個點的位置須要使用(x,y,z)座標表示。紅色方塊位於 x 軸,綠色方塊位於 y 軸,藍色方塊位於 z 軸,灰色方塊位於原點。在 SceneKit 中咱們能夠這樣建立一個三維座標:
let position = SCNVector3(x: 0, y: 5, z: 10)
複製代碼
咱們能夠將 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)。
有了 SCNScene 和 SCNNode 後,咱們還須要一個攝像機(SCNCamera)來決定咱們能夠看到場景中的哪一塊區域(就比如現實世界中有了各類物體,但還須要人的眼睛才能看到物體)。攝像機在 SCNScene 的工做模式以下圖:
上圖中包含如下幾點信息:
在 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)
複製代碼
最後,咱們須要一個 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,技術乾貨首發微信,第一時間推送。