本篇寫一個(gè) AR demo,demo包含三個(gè)部分的內(nèi)容:
- 基于后置攝像頭的平面檢測(cè)
- 基于前置攝像頭的人臉追蹤
- 基于后置攝像頭的圖像識(shí)別
寫在前面
本文在一個(gè)項(xiàng)目中實(shí)現(xiàn)引言部分提到的三個(gè)AR功能,由于是一個(gè)AR 的 Hello World 項(xiàng)目,本文只編寫實(shí)現(xiàn) AR 功能的核心代碼,和 3D 渲染相關(guān)的內(nèi)容本文只展示代碼,不會(huì)詳細(xì)講解,后面的文章會(huì)做系統(tǒng)的講解。
本文內(nèi)容結(jié)構(gòu)如下:

1. 搭建第一個(gè)AR項(xiàng)目
1.1 搭建過程
本文的開發(fā)環(huán)境:Xcode9.3 + iPhone X真機(jī) iOS 11.3。
打開Xcode > Create a new Xcode project > Augmented Reality App > next,如下圖:

應(yīng)用名稱命名為 HelloWorld,開發(fā)語言選swift,Content Technology 選擇 SceneKit,然后點(diǎn)擊下一步:

1.2 項(xiàng)目結(jié)構(gòu)和默認(rèn)添加的源碼解讀
打開剛剛新建的工程,和Single View App模板相比,使用 Augmented Reality App 模板新建的工程,初始化內(nèi)容有如下差異:
- 添加了 art.scnassets 資源文件夾,里面放著資源文件,打開 ship.scn 文件,是一個(gè)3D的飛船模型
- 打開 Main.storyboard ,默認(rèn)給啟動(dòng)頁的 ViewController對(duì)象 添加了一個(gè)ARSCNView實(shí)例
- ViewController.swift 文件中添加了一些添加3D飛船模型的代碼
現(xiàn)在直接在真機(jī)上 run 這個(gè)項(xiàng)目,效果如下:

Amazing,飛船渲染在我們的真實(shí)世界中了,看看是怎么通過代碼加載進(jìn)來的。
1. 查看 main.storyboard,發(fā)現(xiàn)系統(tǒng)為我們的 ViewController 實(shí)例的 view 添加了一個(gè)ARSCNView類型的subview,并將其設(shè)置為ViewController的屬性。
@IBOutlet var sceneView: ARSCNView!
2. 查看 ViewController.swift 的 viewDidLoad: 方法:
sceneView.delegate = self
這行代碼給 sceneView 設(shè)置 ARSCNViewDelegate 代理,在ViewController 中就可以獲取 sceneView 的渲染狀態(tài)回調(diào)。
sceneView.showsStatistics = true
showsStatistics 是 SCNView 的一個(gè)性能統(tǒng)計(jì)屬性,設(shè)置為 true 之后,sceneView 底部就會(huì)顯示一個(gè)sceneView 的性能統(tǒng)計(jì)狀態(tài)欄,點(diǎn)擊上面的加號(hào)之后,這個(gè)狀態(tài)欄會(huì)展開,上面的 gif 有展示這個(gè)過程。
// Create a new scene
let scene = SCNScene(named: "art.scnassets/ship.scn")!
// Set the scene to the view
sceneView.scene = scene
這兩行代碼從我們資源文件夾 art.scnassets 中讀取資源文件 ship.scn ,把這個(gè)文件轉(zhuǎn)換為一個(gè)名為 scene 的 SCNScene 實(shí)例,然后將這個(gè)場(chǎng)景設(shè)置為 sceneView 的 scene 屬性。
這樣我們加載這個(gè)包含飛船的場(chǎng)景到真實(shí)世界中。
3. 查看 ViewController.swift 的 viewWillAppear: 方法,在視圖即將出現(xiàn)的時(shí)候,初始化一個(gè) ARWorldTrackingConfiguration 實(shí)例 configuration ,然后用這個(gè)configuration 運(yùn)行 ARSession對(duì)象。
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// Run the view's session
sceneView.session.run(configuration)
4. 查看 ViewController.swift 的 viewWillDisappear:方法,在視圖消失的時(shí)候,停止這個(gè)session,和 session.run 成對(duì)出現(xiàn)。
// Pause the view's session
sceneView.session.pause()
但是!我們發(fā)現(xiàn)這個(gè)飛船是在viewDidLoad的時(shí)候加載的,并沒有融入對(duì)世界理解,也沒有交互!
看完系統(tǒng)默認(rèn)添加的代碼之后,在編寫AR 代碼之前,我們先給我們 Demo 搭建一個(gè)簡(jiǎn)單的視圖框架。
1.3 利用storyboard快速構(gòu)建Demo視圖層級(jí)
新建三個(gè)ViewController,繼承自UIViewController,每一個(gè)控制器代表一個(gè)功能:
- TKWorldTrackingViewController 負(fù)責(zé)實(shí)現(xiàn)平面檢測(cè)功能
- TKFaceTrackingViewController 負(fù)責(zé)實(shí)現(xiàn)人臉檢測(cè)相關(guān)功能
- TKImageRecognizeViewController 負(fù)責(zé)實(shí)現(xiàn)物體識(shí)別相關(guān)功能
并利用storyboard快速構(gòu)建整個(gè)如圖層級(jí),關(guān)于storyboard的使用,本教程不做闡述,完成之后如下如所示:

此時(shí),準(zhǔn)備完畢,下面正式編寫AR代碼!??!
2. 開發(fā) World Tracking 功能
首先在 TKWorldTrackingViewController 中引入 ARKit。
import ARKit
添加sceneView屬性,用來展示AR視圖。
var sceneView : SCNView!
在 viewDidLoad: 中初始化 sceneView,并作為 subview 添加到view上。
override func viewDidLoad() {
super.viewDidLoad()
sceneView = SCNView(frame: view.bounds)
view.addSubview(sceneView)
}
sceneView 已經(jīng)初始化完成,現(xiàn)在需要運(yùn)行 sceneView 的 AR會(huì)話,我們希望在 當(dāng)前 view 出現(xiàn)的時(shí)候運(yùn)行會(huì)話。在 viewWillAppear:中創(chuàng)建一個(gè)ARWorldTrackingConfiguration 實(shí)例 configuration ,然后用 configuration 運(yùn)行 AR session。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = .horizontal
sceneView.session.run(configuration)
}
在當(dāng)前 view 消失的時(shí)候,在viewWillDisappear:中停止AR session。
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
// Pause the view's session
sceneView.session.pause()
}
到目前為止,AR session 已經(jīng)成功運(yùn)行了,接下來要做的就是,在檢測(cè)到平面之后,接收并處理ARKit發(fā)出來的通知。
實(shí)現(xiàn)思路如下:
- 上一遍文章關(guān)于世界追蹤的描述中有提到過,ARKit 檢測(cè)到平面后,會(huì)在場(chǎng)景中添加錨點(diǎn)
- 遵守 sceneView 的 ARSCNViewDelegate ,實(shí)現(xiàn)
renderer(: didAdd: for:)方法,當(dāng)場(chǎng)景中添加錨點(diǎn)的時(shí)候,viewcontroller 就可以收到通知 - 判斷新添加錨點(diǎn)的類型,如果是 ARPlaneAnchor 類型,就認(rèn)為檢測(cè)到平面了
讓 TKWorldTrackingViewController 遵守 ARSCNViewDelegate。
class TKWorldTrackingViewController: UIViewController,ARSCNViewDelegate
然后在viewDidLoad:中添加如下代碼:
sceneView.delegate = self
并實(shí)現(xiàn)代理方法,判斷當(dāng)前新增的錨點(diǎn)類型,如果是 ARPlaneAnchor,就在當(dāng)前錨點(diǎn)出添加一個(gè) box。
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// 1. 判斷當(dāng)前新增的錨點(diǎn)類型
guard anchor is ARPlaneAnchor else { return }
// 2. 在檢測(cè)到的平面處添加 box
let box = SCNBox(width: 0.08, height: 0.08, length: 0.08, chamferRadius: 0)
let boxNode = SCNNode(geometry: box)
node.addChildNode(boxNode)
}
此時(shí),代碼寫完了,在真機(jī)上 run 項(xiàng)目,點(diǎn)擊 “Demo1:平面檢測(cè)” 按鈕,效果如下:

3. 開發(fā) Face Tracking 功能
TKFaceTrackingViewController 中引入 ARKit、添加 sceneView 屬性、在 viewDidLoad: 中初始化 sceneView 的代碼,和上一節(jié)“開發(fā) World Tracking 功能”中一樣。
運(yùn)行 session 代碼如下:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARFaceTrackingConfiguration()
sceneView.session.run(configuration)
}
獲取人臉和添加box方法和上一節(jié)講的類似,代碼如下:
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// 1. 判斷anchor是否為ARFaceAnchor
guard anchor is ARFaceAnchor else { return }
// 2. 在檢測(cè)到的人臉處添加 box
let box = SCNBox(width: 0.08, height: 0.08, length: 0.08, chamferRadius: 0)
let boxNode = SCNNode(geometry: box)
node.addChildNode(boxNode)
}
在真機(jī)上 run 項(xiàng)目,點(diǎn)擊 “Demo2:人臉檢測(cè)” 按鈕,效果如下:

4. 開發(fā)基于AR的圖像識(shí)別功能
4.1 將需要識(shí)別的2D圖片導(dǎo)入到項(xiàng)目中
在 Assets.xcassets 文件目錄下新建一個(gè) AR Resource Group 類型的目錄。

然后將要識(shí)別的對(duì)象,對(duì)應(yīng)的2D圖片拖拽到如下圖片中的紅框位置。

接下來的步驟很重要,查看圖片的 Show the Attributes inspector,給圖片設(shè)置大小,這個(gè)值是我們需要識(shí)別的物體在真實(shí)世界中的大?。。?!
這個(gè)值的精度直接決定了識(shí)別效果。
經(jīng)過測(cè)量,我需要識(shí)別的企鵝,高度約為13cm,這里的單位選 Meters,設(shè)置如下。

4.2 加載上面導(dǎo)入的圖片
TKImageRecognizeViewController 中引入 ARKit、添加 sceneView 屬性、在 viewDidLoad: 中初始化 sceneView 的代碼,和前面“開發(fā) World Tracking 功能”中一樣。
運(yùn)行 session 代碼稍有變化。
ARReferenceImage 提供的 referenceImages(:)方法可以導(dǎo)入項(xiàng)目中 AR Resources 文件夾下的所有圖片,如果項(xiàng)目中沒有這個(gè)文件,會(huì)拋出異常。在viewWillApper: 中添加如下代碼。
guard let referenceImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
fatalError("AR Resources 資源文件不存在 。")
}
接著,新建一個(gè)ARWorldTrackingConfiguration實(shí)例,將 referenceImages 賦給 detectionImages屬性。
let configuration = ARWorldTrackingConfiguration()
configuration.detectionImages = referenceImages
用上面的 configuration 運(yùn)行AR 會(huì)話。
sceneView.session.run(configuration)
4.3 添加圖像識(shí)別代碼
在renderer(: didAdd: for:)方法中處理圖像識(shí)別結(jié)果的回調(diào),在識(shí)別到圖像的位置添加一個(gè)平面,做識(shí)別結(jié)果可視化標(biāo)識(shí)。
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// 1. 判斷anchor是否為ARImageAnchor
guard let imageAnchor = anchor as? ARImageAnchor else { return }
// 2. 在檢測(cè)到的物體圖像處添加 plane
let referenceImage = imageAnchor.referenceImage
let plane = SCNPlane(width: referenceImage.physicalSize.width,
height: referenceImage.physicalSize.height)
let planeNode = SCNNode(geometry: plane)
planeNode.eulerAngles.x = -.pi / 2
// 3. 將plane添加到檢測(cè)到的圖像錨點(diǎn)處
node.addChildNode(planeNode)
}
在真機(jī)上 run 項(xiàng)目,點(diǎn)擊 “Demo3:物體識(shí)別” 按鈕,效果如下:

至此,我們完成了關(guān)于我們第一個(gè)AR項(xiàng)目,接下來會(huì)圍繞SceneKit,系統(tǒng)的介紹和 3D 內(nèi)容渲染相關(guān)的內(nèi)容。
