直擊蘋果 ARKit 技術

battle_field發表於2019-04-09

蘋果在 WWDC2017 中推出了 ARKit,通過這個新框架可以看出蘋果未來會在 AR 方向不斷髮展,本著學習興趣,對此項新技術進行了學習,並在團隊進行了一次分享,利用業餘時間把幾周前分享的內容整理成文件供大家交流學習。

本文並不是簡單的介紹 ARKit 中的 API 如何使用,而是在介紹 ARKit API 的同時附上了一些理論知識,所以有些內容可能會不太容易理解。筆者能力有限,文章若有誤請指出。團隊分享(本次分享為網易杭研院前端技術部的內部分享)時進行了一次直播,若文中有不理解的內容,歡迎觀看下錄播:ARKit 分享直播 。本文也附上分享的 PPT 地址:ARKit-keynote

如需轉載請註明作者和原文地址。

一、什麼是 AR?

AR 全稱 Augmented Reality(擴增實境),是一種在攝像機捕捉到的真實世界中加入計算機程式創造的虛擬世界的技術。下圖是一個簡單的 AR 的 Demo:

ARChair.gif

在上圖中,椅子是計算機程式建立的虛擬世界的物體,而背景則是攝像機捕捉到的真實世界,AR 系統將兩者結合在一起。我們從上圖可以窺探出 AR 系統由以下幾個基礎部分組成:

  1. 捕捉真實世界:上圖中的背景就是真實世界,一般由攝像機完成。
  2. 虛擬世界:例如上圖中的椅子就是虛擬世界中的一個物體模型。當然,可以有很多物體模型,從而組成一個複雜的虛擬世界。
  3. 虛擬世界與現實世界相結合:將虛擬世界渲染到捕捉到的真實世界中。
  4. 世界追蹤:當真實世界變化時(如上圖中移動攝像機),要能追蹤到當前攝像機相對於初始時的位置、角度變化資訊,以便實時渲染出虛擬世界相對於現實世界的位置和角度。
  5. 場景解析:例如上圖中可以看出椅子是放在地面上的,這個地面其實是 AR 系統檢測出來的。
  6. 與虛擬世界互動:例如上圖中縮放、拖動椅子。(其實也屬於場景解析的範疇)

根據上面的描述,我們可以得出 AR 系統的大致結構圖:

ARKitSystem.png

Note:這裡只介紹基於計算機顯示器的 AR 系統實現方案,此外還有光學透視式和視訊透視式方案。可參考擴增實境-組成形式

二、ARKit 簡介

ARKitLogo.png

ARKit 是蘋果 WWDC2017 中釋出的用於開發iOS平臺 AR 功能的框架。ARKit 為上一節中提到的 AR 系統架構中各個部分都提供了實現方案,並且為開發者提供了簡單便捷的 API,使得開發者更加快捷的開發 AR 功能。

ARKit 的使用需要一定的軟硬體設施:

  • 軟體:
    • 開發工具:Xcode9
    • iOS11
    • MacOS 10.12.4 及以上版本(為了支援 Xcode9)
  • 硬體:
    • 處理器為 A9 及以上的 iPhone 或 iPad 裝置(iPhone 6s 為 A9 處理器)

下面幾節中,我們將逐步介紹 ARKit 中 AR 系統的各個組成部分。

三、ARKit 架構

ARKit 定義了一套簡單易用的 API,API 中引入了多個類,為了更加清晰的理解 ARKit,我們從 AR 系統組成的角度對 ARKit API 進行了分類,如下圖:

ARKitArchitecture

上圖列出了 ARKit API 中的幾個主要的類,如 ARSession、ARSessionConfiguration、ARFrame、ARCamera 等。並依據各個類的功能進行了模組劃分:紅色(World Tracking)、藍色(Virtual World)、土色(Capture Real Wrold)、紫色(Scene Understanding)、綠色(Rendering)。

對於上圖,ARSession 是核心整個ARKit系統的核心,ARSession 實現了世界追蹤、場景解析等重要功能。而 ARFrame 中包含有 ARSession 輸出的所有資訊,是渲染的關鍵資料來源。雖然 ARKit 提供的 API 較為簡單,但看到上面整個框架後,對於初識整個體系的開發者來說,還是會覺著有些龐大。沒關係,後面幾節會對每個模組進行單獨的介紹,當讀完最後時,再回頭來看這個架構圖,或許會更加明瞭一些。

四、構建虛擬世界

我們將 Virtual World 從 ARKit 架構圖中抽出:

VirtualWorld.png

ARKit 本身並不提供建立虛擬世界的引擎,而是使用其他 3D/2D 引擎進行建立虛擬世界。iOS 系統上可使用的引擎主要有:

  • Apple 3D Framework - SceneKit.
  • Apple 2D Framework - SpriteKit.
  • Apple GPU-accelerated 3D graphics Engine - Metal.
  • OpenGl
  • Unity3D
  • Unreal Engine

ARKit 並沒有明確要求開發者使用哪種方式構建虛擬世界,開發者可以利用 ARKit 輸出的真實世界、世界追蹤以及場景解析的資訊(存在於 ARFrame 中),自己將通過圖形引擎建立的虛擬世界渲染到真實世界中。值得一提的是,ARKit 提供了 ARSCNView 類,該類基於 SceneKit 為 3D 虛擬世界渲染到真實世界提供了非常簡單的 API,關於 ARSCNView 會在最後的渲染部分進行介紹,所以下面我們來介紹下 SceneKit。

SceneKit 簡介

這裡不對 SceneKit 進行深入探討,只簡單介紹下基礎概念。讀者只需要理解 SceneKit 裡虛擬世界的構成就可以了。

SceneKit 的座標系

我們知道 UIKit 使用一個包含有 x 和 y 資訊的 CGPoint 來表示一個點的位置,但是在 3D 系統中,需要一個 z 引數來描述物體在空間中的深度,SceneKit 的座標系可以參考下圖:

SceneKitCoordinate.png

這個三維座標系中,表示一個點的位置需要使用(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:

SceneKitNode.png

SCNScene 中的節點加入時可以指定一個三維座標(預設為(x:0, y:0, z:0)),這個座標是相對於其父節點的位置。這裡說明兩個概念:

  • 本地座標系:以場景中的某節點(非根節點)為原點建立的三維座標系
  • 世界座標系:以根節點為原點建立的三維座標系稱為世界座標系。

上圖中我們可以看到 NodeA 的座標是相對於世界座標系(由於 NodeA 的父節點是根節點)的位置,而 NodeB 的座標代表了 NodeB 在 NodeA 的本地座標系位置(NodeB 的父節點是 NodeA)。

SceneKit 中的攝像機

有了 SCNScene 和 SCNNode 後,我們還需要一個攝像機(SCNCamera)來決定我們可以看到場景中的哪一塊區域(就好比現實世界中有了各種物體,但還需要人的眼睛才能看到物體)。攝像機在 SCNScene 的工作模式如下圖:

SceneKitCamera.png

上圖中包含以下幾點資訊:

  • 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
複製程式碼

五、捕捉真實世界(Capture Real World)

捕捉真實世界就是為了將我們現實世界的場景作為 ARKit 顯示場景的背景。為了方便閱讀,我們首先將 Capture Real World 從 ARKit 架構圖中抽取出:

CaptureRealWorld.png

ARSession

如果我們想要使用 ARKit,我們必須要建立一個 ARSession 物件並執行 ARSession。基本步驟如下:

// 建立一個 ARSessionConfiguration.
// 暫時無需在意 ARWorldTrackingSessionConfiguration.
let configuration = ARWorldTrackingSessionConfiguration()
// Create a session.
let session = ARSession()
// Run.
session.run(configuration)
複製程式碼

從上面的程式碼看,執行一個 ARSession 的過程是很簡單的,那麼 ARSession 的底層如何捕捉現實世界場景的呢?

  • 首先,ARSession 底層使用了 AVCaputreSession 來獲取攝像機拍攝的視訊(一幀一幀的影象序列)。
  • 然後,ARSession 將獲取的影象序列進行處理,最後輸出 ARFrame,ARFrame 中就包含有現實世界場景的所有資訊。

ARFrame

從上一步驟得知 ARFrame 中包含了現實世界場景的所有資訊,那麼 ARFrame 中與現實世界場景有關的資訊有哪些?

  • var capturedImage: CVPixelBuffer 該屬性是攝像機捕捉到的影象資訊,就是構成我們現實世界場景中的一幀影象。順便說明下,對於攝像機捕捉的每一幀影象,都會生成一個 ARFrame。

  • var timestamp: TimeInterval 該屬性是攝像機捕捉到的對應於 capturedImage 的一幀影象的時間。

  • var camera: ARCamera 獲取現實世界的相機資訊。詳細介紹見下。

ARCamera

ARCamera 是 ARFrame 中的一個屬性,之因為單獨拿出來說,是因為這裡有必要介紹下相機的一些特性,ARCamera 中與現實世界場景有關的資訊有兩個:

  • var imageResolution: CGSize 該屬性表示了相機捕捉到的影象的長度和寬度(以畫素為單位),可以理解成捕捉到的影象的解析度。

  • var intrinsics: matrix_float3x3 intrinsics 是一個 3x3 矩陣,這個矩陣將我們現實世界中三維座標系的點對映到相機捕捉的影象中。有興趣可看下面的詳述。

Intrinsic Matrix

Intrinsic Matrix 是相機的一個固有屬性,也就是說每個相機都會有 Intrinsic Matrix,因為所有的相機都需要將現實世界中三維空間的點對映到捕捉的影象中二維空間的點。

那麼這個矩陣是如何工作的呢?我們先來看一個圖片:

IntrinsicMatrix.png

上圖包含如下基本資訊:

  • 一個三維座標系(紅色 x 軸,綠色 y 軸,藍色 z 軸)。
  • 空間中的一個點(藍色的點 N,座標為(x', y', z'))。
  • 相機的成像平面(紫色的平行四邊形)
  • 成像平面與 z 軸的交點(點 M)
  • 成像平面的原點(黃色的點 O),也就是捕捉的二維影象的二維座標系的原點。

現在我們需要將三維空間的點(x', y', z')對映到成像平面中的一個點(N')。下面我們來看下對映過程。

Intrinsic Matrix 一般是下圖所示的樣子:

IntrinsicMatrixValue.png

上圖中,fx 和 fy 是攝像機鏡頭的焦距,這裡不做深究,ox 和 oy 則是點 M(成像平面與 z 軸交點)相對於點 O(成像平面二維座標系原點)的 x 與 y 方向的偏移。

下圖展示了利用 Intrinsic Matrix 將 N 對映 N' 的過程:

IntrinsicMatrixMap.png

上圖中,Intrinsic Matrix 與表示點 N 的向量相乘後,再除以 z',就得到了一個 z 座標為 1 的三維向量, 我們丟棄掉 z 座標資訊就得到了 N' 的座標:((x' * fx)/z' + ox, (y' * fy)/z' + oy)。

這就是 Intrinsic Matrix 的作用過程,至於為何這麼對映,則是相機原理的內容了,由於水平有限,就不做介紹了。如果不太好理解,我們這樣簡單理解為相機使用這個矩陣就可以將空間中的某個點對映到二維成像平面的一個點。

六、世界追蹤(World Tracking)

在第一部分 AR 系統介紹時,我們看到虛擬椅子是放在地面上的,當我們移動時可以看到不同角度,我們也可以移動椅子,這些功能的實現都離不開世界追蹤。總結來說,世界追蹤用來為真實世界與虛擬世界結合提供有效資訊,以便我們能在真實世界中看到一個更加真實的虛擬世界。

為了方便閱讀,我們首先將 World Tracking 從 ARKit 架構圖中抽取出:

WorldTracking.png

下面我們分析一下 ARKit 中與世界追蹤相關的技術以及類。

ARSession

如果我們想要使用 ARKit,我們必須要建立一個 ARSession 物件並執行 ARSession。基本步驟如下:

// 建立一個 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 的座標系原點。

WorldTrackingCoordinate.png

在這個 AR-World 座標系中,ARKit 會追蹤以下幾個資訊:

  • 追蹤裝置的位置以及旋轉,這裡的兩個資訊均是相對於裝置起始時的資訊。
  • 追蹤物理距離(以“米”為單位),例如 ARKit 檢測到一個平面,我們希望知道這個平面有多大。
  • 追蹤我們手動新增的希望追蹤的點,例如我們手動新增的一個虛擬物體。

世界追蹤如何工作?

蘋果文件中對世界追蹤過程是這麼解釋的:ARKit 使用視覺慣性測距技術,對攝像頭採集到的影象序列進行計算機視覺分析,並且與裝置的運動感測器資訊相結合。ARKit 會識別出每一幀影象中的特徵點,並且根據特徵點在連續的影象幀之間的位置變化,然後與運動感測器提供的資訊進行比較,最終得到高精度的裝置位置和偏轉資訊。

我們通過一個 gif 圖來理解上面這段話:

WorldTrackingProcess.gif

  • 上圖中劃出曲線的運動的點代表裝置,可以看到以裝置為中心有一個座標系也在移動和旋轉,這代表著裝置在不斷的移動和旋轉。這個資訊是通過裝置的運動感測器獲取的。
  • 動圖中右側的黃色點是 3D 特徵點。3D 特徵點就是處理捕捉到的影象得到的,能代表物體特徵的點。例如地板的紋理、物體的邊邊角角都可以成為特徵點。上圖中我們看到當裝置移動時,ARKit 在不斷的追蹤捕捉到的畫面中的特徵點。
  • ARKit 將上面兩個資訊進行結合,最終得到了高精度的裝置位置和偏轉資訊。

Configuration

在執行 ARSession 時,我們必須要有一個 Configuration。Configuration 告訴 ARKit 應該如何追蹤裝置的運動。ARKit 為我們提供了兩種型別的 Configuration:

  • ARSessionConfiguration:提供 3DOF 追蹤。
  • ARWorldTrackingSessionConfiguration:提供 6DOF 追蹤。

上面兩個 Configuration 的差別就是 DOF 不一樣,那麼什麼是 DOF?

  1. DOF

    自由度(DOF,Degree Of Freedom)表示描述系統狀態的獨立引數的個數。6DOF 主要包括如下 6 個引數:

    DOF.png

    • 平移:

      1. 上下移動
      2. 左右移動
      3. 前後移動
    • 旋轉: 4. Yawing 2. Pitching 3. Rolling

    其中 3DOF 中只包含旋轉中的三個引數,平移的幾個引數其實很好理解,為了更形象的理解旋轉引數,我們看下面幾個動圖:

    • Yawing 效果:

      yaw.gif

    • Pitching 效果:

      pitch.gif

    • Rolling 效果:

      roll.gif

  2. 3DOF 與 6DOF 追蹤的效果差異

    ARWorldTrackingSessionConfiguration 使用 3 個旋轉引數和 3 個平移引數來追蹤物理系統的狀態, ARSessionConfiguration 使用 3 個旋轉引數來追蹤物理系統的狀態。那麼兩者有什麼效果差別?

    我們下面看兩個圖,下圖一使用 3DOF 的 ARSessionConfiguration 進行世界追蹤,下圖二使用 6DOF 的 ARWorldTrackingSessionConfiguration 進行世界追蹤:

    3DOF.gif

    6DOF.gif

    上面兩個圖,都是先對裝置進行旋轉,再對裝置進行平移。

    那麼,對於 3DOF 追蹤,我們旋轉裝置時可以看到虛擬的飛機視角有所變化;但當平移時,我們可以看到飛機是隨著裝置進行移動的。

    對於 6DOF 追蹤,我們旋轉裝置時可以看到虛擬的飛機視角有所變化(這點與 3DOF 追蹤沒有區別);平移時,我們可以看到飛機的不同位置,例如向上平移看到了飛機的上表面,圍著飛機平移可以看到飛機的四周,而 3DOF 沒有提供這種平移的追蹤。如果還是不理解兩者區別,可以看動圖的後半段,效果差異其實是非常明顯的。

  3. 判斷當前裝置是否支援某類 SessionConfiguration

    class var isSupported: Bool
    複製程式碼

    示例程式碼如下:

    if ARWorldTrackingSessionConfiguration.isSupported {
        configuration = ARWorldTrackingSessionConfiguration()
    } else {
        configuration = ARSessionConfiguration()
    }
    複製程式碼

    關於 ARSessionConfiguration 我們就介紹到這裡,下面我們看一下 ARFrame。

ARFrame

ARFrame 中包含有世界追蹤過程獲取的所有資訊,ARFrame 中與世界追蹤有關的資訊主要是:anchors 和 camera:

  • camera: 含有攝像機的位置、旋轉以及拍照引數等資訊。

    var camera: [ARCamera]
    複製程式碼
  • ahchors: 代表了追蹤的點或面。

    var anchors: [ARAnchor]
    複製程式碼

至於 ARCamera 和 ARAnchor 是什麼?下面分別進行介紹。

ARAnchor

ARAnchor.png

我們可以把 ARAnchor(AR 錨點) 理解為真實世界中的某個點或平面,anchor 中包含位置資訊和旋轉資訊。拿到 anchor 後,可以在該 anchor 處放置一些虛擬物體。對於 ARAnchor 有如下幾種操作:

  • 我們可以使用 ARSession 的 add/remove 方法進行手動新增或刪除 Anchor。例如,我們新增了一個虛擬物體到 ARKit 中,在之後的某個時候我們想要在剛才的虛擬物體上面再放置一個東西,那麼我們可以為這個虛擬物體新增一個 anchor 到 ARSession 中,這樣在後面可以通過 ARSession 獲取到這個虛擬物體的錨點資訊。

  • 通過 ARSession 獲取當前幀的所有 anchors:

    let anchors = session.currentFrame.anchors
    複製程式碼
  • ARKit 自動新增 anchors。例如,ARKit 檢測到了一個平面,ARKit 會為該平面建立一個 ARPlaneAnchor 並新增到 ARSession 中。

  • ARSessionDelegate 可以監聽到新增、刪除和更新 ARAnchor 的通知。

ARAnchor 中的主要引數是 transform,這個引數是一個 4x4 的矩陣,矩陣中包含了 anchor 偏移、旋轉和縮放資訊。

var transform: matrix_float4x4
複製程式碼

這裡可能存在的一個疑問就是,為何是一個 4x4 的矩陣,三維座標系表示一個點不是用三個座標就可以了嗎?

4x4 矩陣?

物體在三維空間中的運動通常分類兩類:平移和旋轉,那麼表達一個物體的變化就應該能夠包含兩類運動變化。

  • 平移

4x4Translation.png

首先看上圖,假設有一個長方體(黃色虛線)沿 x 軸平移Δx、沿 y 軸平移Δy、沿 z 軸平移Δz 到了另一個位置(紫色虛線)。長方體的頂點 P(x1, y1, z1)則平移到了 P'(x2, y2, z2),使用公式表示如下:

4x4TranslationExpression.png

  • 旋轉

4x4Rotation.png

在旋轉之前,上圖中包含以下資訊:

  • 黃色虛線的長方體
  • P(x1, y1, z1)是長方體的一個頂點
  • P 點在 xy 平面的投影點 Q(x1, y1, 0)
  • Q 與座標原點的距離為 L
  • Q 與座標原點連線與 y 軸的夾角是α

那麼在旋轉之前,P 點座標可以表示為:

    x1 = L * sinα
    y1 = L * cosα
    z1 = z1
複製程式碼

下面我們讓長方體繞著 z 軸逆時針旋轉β角度,那麼看圖可以得到以下資訊:

  • P 點會繞著 z 軸逆時針旋轉β角度到達 P'(x2, y2, z2)
  • P' 在 xy 平面投影點 Q'(x2, y2, 0)
  • Q' 與 Q 在以 xy 平面原點為圓心,半徑為 L 的圓上
  • Q' 與原點連線與 Q 與原點連線之間的夾角為 β
  • Q'與原點連線與 y 軸的角度是 α-β。

那麼在旋轉之後,P' 點的座標可以表示為:

4x4RotationExpression.png

使用矩陣來表示:

4x4RotationExpression2.png

從上面的分析可以看出,為了表達旋轉資訊,我們需要一個 3x3 的矩陣,在表達了旋轉資訊的 3x3 矩陣中,我們無法表達平移資訊,為了同時表達平移和旋轉資訊,在 3D 計算機圖形學中引入了齊次座標系,在齊次座標系中,使用四維矩陣表示一個點或向量:

HomogeneousPoint.png

加入一個變化是先繞著 z 軸旋轉 β 角度,再沿 x 軸平移Δx、沿 y 軸平移Δy、沿 z 軸平移Δz,我們可以用以下矩陣變化表示:

Homogeneous.png

最後,還有一種變化是縮放,在齊次座標系中只需要在前三列矩陣中某個位置新增一個係數即可,比較簡單,這裡不在展示矩陣變換。從上面可以看出,為了完整的表達一個物體在 3D 空間的變化,需要一個 4x4 矩陣。

ARCamera

ARCamera 中的主要引數是:

  • transform: 表示攝像頭相對於起始時的位置和旋轉資訊。至於為何是 4x4 矩陣,上面已經解釋過了。

    var transform: matrix_float4x4
    複製程式碼
  • eulerAngles: 尤拉角,另一種表示攝像頭的偏轉角度資訊的方式,與我們之前介紹的 3DOF 有關, 尤拉證明了一個物體的任何旋轉都可以分解為 yaw、pitch、roll 三個方向的旋轉。

    var eulerAngles: vector_float3
    複製程式碼
  • projectionMatrix: 投影矩陣,其實這個很類似於上面介紹的 Intrinsic Matrix,但不同點是,投影矩陣是將 AR-world 中的物體投影到螢幕上,由於 AR-world 中採用的是齊次座標,所以這裡是一個 4x4 矩陣,投影矩陣除了決定 AR-world 中點應該對映到螢幕哪個點之外,還決定了哪些範圍的點是不需要的,我們看下圖:

ProjectionMatrix.png

上圖中 Field-of-View 和 View-Frustum 影響了投影矩陣的部分引數,對於超過 Field-of-View 或者超出 View-Frustum 範圍的點,ARKit 不會對其進行投影對映到螢幕。

此外,ARKit 還提供了一個介面讓我們自定義 Field-of-View 和 View-Frustum:

func projectionMatrix(withViewportSize: CGSize,
                           orientation: UIInterfaceOrientation,
                                 zNear: CGFloat,
                                  zFar: CGFloat)
複製程式碼

追蹤質量:

世界追蹤需要一定的條件才能達到較好的效果,如果達不到所需的條件要求,那麼世界追蹤的質量會降低,甚至會無法追蹤。較好的世界追蹤質量主要有以下三個依賴條件:

  • 運動感測器不能停止工作。如果運動感測器停止了工作,那麼就無法拿到裝置的運動資訊。根據我們之前提到的世界追蹤的工作原理,毫無疑問,追蹤質量會下降甚至無法工作。

  • 真實世界的場景需要有一定特徵點可追蹤。世界追蹤需要不斷分析和追蹤捕捉到的影象序列中特徵點,如果影象是一面白牆,那麼特徵點非常少,那麼追蹤質量就會下降。

  • 裝置移動速度不能過快。如果裝置移動太快,那麼 ARKit 無法分析出不同影象幀之中的特徵點的對應關係,也會導致追蹤質量下降。

追蹤狀態

世界追蹤有三種狀態,我們可以通過 camera.trackingState 獲取當前的追蹤狀態。

TrackingState.png

從上圖我們看到有三種追蹤狀態:

  • Not Available:世界追蹤正在初始化,還未開始工作。
  • Normal: 正常工作狀態。
  • Limited:限制狀態,當追蹤質量受到影響時,追蹤狀態可能會變為 Limited 狀態。

與 TrackingState 關聯的一個資訊是 ARCamera.TrackingState.Reason,這是一個列舉型別:

  • case excessiveMotion:裝置移動過快,無法正常追蹤。
  • case initializing:正在初始化。
  • case insufficientFeatures:特徵過少,無法正常追蹤。
  • case none:正常工作。

我們可以通過 ARSessionObserver 協議去獲取追蹤狀態的變化,比較簡單,可以直接檢視介面文件,這裡不做深入介紹。

到這裡,ARKit 中有關於世界追蹤的知識基本介紹完了,世界追蹤算是 ARKit 中核心功能了,如果理解了本部分內容,相信去看蘋果的介面文件也會覺著非常容易理解。如果沒有看懂,可以去看一下分享的錄播(本文開頭有連結)。

七、場景解析(Scene Understanding)

為了方便閱讀,我們首先將 Scene Understanding 從 ARKit 架構圖中抽取出:

SceneUnderstanding.png

場景解析主要功能是對現實世界的場景進行分析,解析出比如現實世界的平面等資訊,可以讓我們把一些虛擬物體放在某些實物處。ARKit 提供的場景解析主要有平面檢測場景互動以及光照估計三種,下面逐個分析。

平面檢測(Plane detection)

主要功能:

  • ARKit 的平面檢測用於檢測出現實世界的水平面。

    PlaneDetection.png

    上圖中可以看出,ARkit 檢測出了兩個平面,圖中的兩個三維座標系是檢測出的平面的本地座標系,此外,檢測出的平面是有一個大小範圍的。

  • 平面檢測是一個動態的過程,當攝像機不斷移動時,檢測到的平面也會不斷的變化。下圖中可以看到當移動攝像機時,已經檢測到的平面的座標原點以及平面範圍都在不斷的變化。

    MultipleFramePlane.gif

  • 此外,隨著平面的動態檢測,不同平面也可能會合併為一個新的平面。下圖中可以看到已經檢測到的平面隨著攝像機移動合併為了一個平面。

    PlaneMerge.gif

開啟平面檢測

開啟平面檢測很簡單,只需要在 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 就表示了一個平面。

ARPlaneAnchor 主要有以下屬性:

  • alignment: 表示該平面的方向,目前只有 horizontal 一個可能值,表示這個平面是水平面。ARKit 目前無法檢測出垂直平面。

    var alignment: ARPlaneAnchor.Alignment
    複製程式碼
  • center: 表示該平面的本地座標系的中心點。如下圖中檢測到的平面都有一個三維座標系,center 所代表的就是座標系的原點:var center: vector_float3

    PlaneDetection.png

  • extent: 表示該平面的大小範圍。如上圖中檢測到的螢幕都有一個範圍大小。

    var extent: vector_float3
    複製程式碼

ARSessionDelegate

當 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 等)。

工作原理

先看一下原理圖:

Hit-testing.png

當點選螢幕時,ARKit 會發射一個射線,假設螢幕平面是三維座標系中的 xy 平面,那麼該射線會沿著 z 軸方向射向螢幕裡面,這就是一次 Hit-testing 過程。此次過程會將射線遇到的所有有用資訊返回,返回結果以離螢幕距離進行排序,離螢幕最近的排在最前面。

ResultType

ARFrame 提供了 Hit-testing 的介面:

func hitTest(_ point: CGPoint, types: ARHitTestResult.ResultType) -> [ARHitTestResult]
複製程式碼

上述介面中有一個 types 引數,該參數列示此次 Hit-testing 過程需要獲取的資訊型別。ResultType 有以下四種:

  • featurePoint 表示此次 Hit-testing 過程希望返回當前影象中 Hit-testing 射線經過的 3D 特徵點。如下圖:

    HitFeaturePoint.gif

  • estimatedHorizontalPlane 表示此次 Hit-testing 過程希望返回當前影象中 Hit-testing 射線經過的預估平面。預估平面表示 ARKit 當前檢測到一個可能是平面的資訊,但當前尚未確定是平面,所以 ARKit 還沒有為此預估平面新增 ARPlaneAnchor。如下圖:

    HitEstimatedPlane.gif

  • existingPlaneUsingExtent 表示此次 Hit-testing 過程希望返回當前影象中 Hit-testing 射線經過的有大小範圍的平面。

    HitExistingPlaneUseExtent.gif

    上圖中,如果 Hit-testing 射線經過了有大小範圍的綠色平面,則會返回此平面,如果射線落在了綠色平面的外面,則不會返回此平面。

  • existingPlane 表示此次 Hit-testing 過程希望返回當前影象中 Hit-testing 射線經過的無限大小的平面。

    HitExistingPlane.gif

    上圖中,平面大小是綠色平面所展示的大小,但 exsitingPlane 選項表示即使 Hit-testing 射線落在了綠色平面外面,也會將此平面返回。換句話說,將所有平面無限延展,只要 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)

LightEstimation.png

上圖中,一個虛擬物體茶杯被放在了現實世界的桌子上。

當週圍環境光線較好時,攝像機捕捉到的影象光照強度也較好,此時,我們放在桌子上的茶杯看起來就比較貼近於現實效果,如上圖最左邊的圖。但是當週圍光線較暗時,攝像機捕捉到的影象也較暗,如上圖中間的圖,此時茶杯的亮度就顯得跟現實世界格格不入。

針對這種情況,ARKit 提供了光照估計,開啟光照估計後,我們可以拿到當前影象的光照強度,從而能夠以更自然的光照強度去渲染虛擬物體,如上圖最右邊的圖。

光照估計基於當前捕捉到的影象的曝光等資訊,給出一個估計的光照強度值(單位為 lumen,光強單位)。預設的光照強度為 1000lumen,當現實世界較亮時,我們可以拿到一個高於 1000lumen 的值,相反,當現實世界光照較暗時,我們會拿到一個低於 1000lumen 的值。

ARKit 的光照估計預設是開啟的,當然也可以通過下述方式手動配置:

configuration.isLightEstimationEnabled = true
複製程式碼

獲取光照估計的光照強度也很簡單,只需要拿到當前的 ARFrame,通過以下程式碼即可獲取估計的光照強度:

let intensity = frame.lightEstimate?.ambientIntensity
複製程式碼

八、渲染(Rendering)

Rendering.png

渲染是呈現 AR world 的最後一個過程。此過程將建立的虛擬世界、捕捉的真實世界、ARKit 追蹤的資訊以及 ARKit 場景解析的的資訊結合在一起,渲染出一個 AR world。渲染過程需要實現以下幾點才能渲染出正確的 AR world:

  • 將攝像機捕捉到的真實世界的視訊作為背景。
  • 將世界追蹤到的相機狀態資訊實時更新到 AR world 中的相機。
  • 處理光照估計的光照強度。
  • 實時渲染虛擬世界物體在螢幕中的位置。

如果我們自己處理這個過程,可以看到還是比較複雜的,ARKit 為簡化開發者的渲染過程,為開發者提供了簡單易用的使用 SceneKit(3D 引擎)以及 SpriteKit(2D 引擎)渲染的檢視ARSCNView以及ARSKView。當然開發者也可以使用其他引擎進行渲染,只需要將以上幾個資訊進行處理融合即可。這裡只介紹下ARSCNView,對於使用其他引擎渲染,可參考 WWDC2017-ARKit 最後的 Metal 渲染示例。

ARSCNView 的功能

ARSCNView 幫我們做了如下幾件事情:

  • 將攝像機捕捉到的真實世界的視訊作為背景。
  • 處理光照估計資訊,不斷更新畫面的光照強度。
  • 將 SCNNode 與 ARAnchor 繫結,也就是說當新增一個 SCNNode 時,ARSCNView 會同時新增一個 ARAnchor 到 ARKit 中。
  • 不斷更新 SceneKit 中的相機位置和角度。
  • 將 SceneKit 中的座標系結合到 AR world 的座標系中,不斷渲染 SceneKit 場景到真實世界的畫面中。

關於 ARSCNView 的各個屬性這裡不再進行一一介紹了,如果已經掌握了之前章節的內容,相信直接看 ARSCNView 的介面文件不會有什麼問題。下面對 ARSCNViewDelegate 做一下簡單介紹。

ARSCNViewDelegate

我們在介紹場景解析時,已經介紹過了 ARSessionDelegate,而 ARSCNViewDelegate 其實與 ARSessionDelegate 是有關係的,下面我們再來看下 ARSessionDelegate 的三個回撥:

  • 新加入了 ARAnchor

     func session(_ session: ARSession, didAdd anchors: [ARAnchor])
    複製程式碼

    當 ARKit 新新增一個 ARAnchor 時,ARSessionDelegate 會收到上述回撥。此時,ARKit 會回撥 ARSCNViewDelegate 的下面一個方法詢問需要為此新加的 ARAnchor 新增 SCNNode。

    func renderer(_ renderer: SCNSceneRenderer, nodeFor: ARAnchor) -> SCNNode?
    複製程式碼

    當呼叫完上個方法之後,ARKit 會回撥 ARSCNViewDelegate 的下面一個方法告知 delegate 已為新新增的 ARAnchor 新增了一個 SCNNode。

    func renderer(_ renderer: SCNSceneRenderer, didAdd: SCNNode, for: ARAnchor)
    複製程式碼
  • ARAnchor 更新

    func session(_ session: ARSession, didUpdate anchors: [ARAnchor])
    複製程式碼

    當某個 ARAnchor 更新時,ARSessionDelegate 會收到上述回撥,此時,ARSCNViewDelegate 會收到以下回撥:

    func renderer(_ renderer: SCNSceneRenderer, willUpdate: SCNNode, for: ARAnchor)
    func renderer(_ renderer: SCNSceneRenderer, didUpdate: SCNNode, for: ARAnchor)
    複製程式碼
  • 刪除 ARAnchor

    func session(_ session: ARSession, didRemove anchors: [ARAnchor])
    複製程式碼

    當某個 ARAnchor 刪除時,ARSessionDelegate 會收到上述回撥,此時,ARSCNViewDelegate 會收到以下回撥:

    func renderer(_ renderer: SCNSceneRenderer, didRemove: SCNNode, for: ARAnchor)
    複製程式碼

本章節沒有對渲染進行太過深入的介紹,主要考慮到渲染過程中用到的知識點,在之前大部分已經介紹過了,剩下的只是一些介面的使用方法,相信使用過蘋果各種 Kit 的開發者在掌握以上章節內容後,直接檢視開發者文件,並參考蘋果的官方 demo,使用起來應該不會遇到太多困難。

介紹完渲染之後,本文也就算是結束了。文中的內容涉及到了多個圖形學的有關知識,有些不太好理解的歡迎交流,或者檢視分享的錄播視訊:ARKit 分享直播

歡迎轉載本文,請宣告原文地址及作者,多謝。

九、參考文件

相關文章