本篇寫一個 AR demo,demo包含三個部分的內(nèi)容:
- 基于后置攝像頭的平面檢測
- 基于前置攝像頭的人臉追蹤
- 基于后置攝像頭的圖像識別
寫在前面
本文在一個項(xiàng)目中實(shí)現(xiàn)引言部分提到的三個AR功能,由于是一個AR 的 Hello World 項(xiàng)目,本文只編寫實(shí)現(xiàn) AR 功能的核心代碼,和 3D 渲染相關(guān)的內(nèi)容本文只展示代碼,不會詳細(xì)講解,后面的文章會做系統(tǒng)的講解。
本文內(nèi)容結(jié)構(gòu)如下:
1. 搭建第一個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 文件,是一個3D的飛船模型
- 打開 Main.storyboard ,默認(rèn)給啟動頁的 ViewController對象 添加了一個ARSCNView實(shí)例
- ViewController.swift 文件中添加了一些添加3D飛船模型的代碼
現(xiàn)在直接在真機(jī)上 run 這個項(xiàng)目,效果如下:
Amazing,飛船渲染在我們的真實(shí)世界中了,看看是怎么通過代碼加載進(jìn)來的。
1. 查看 main.storyboard,發(fā)現(xiàn)系統(tǒng)為我們的 ViewController 實(shí)例的 view 添加了一個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 的一個性能統(tǒng)計(jì)屬性,設(shè)置為 true 之后,sceneView 底部就會顯示一個sceneView 的性能統(tǒng)計(jì)狀態(tài)欄,點(diǎn)擊上面的加號之后,這個狀態(tài)欄會展開,上面的 gif 有展示這個過程。
// 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 ,把這個文件轉(zhuǎn)換為一個名為 scene 的 SCNScene 實(shí)例,然后將這個場景設(shè)置為 sceneView 的 scene 屬性。
這樣我們加載這個包含飛船的場景到真實(shí)世界中。
3. 查看 ViewController.swift 的 viewWillAppear: 方法,在視圖即將出現(xiàn)的時候,初始化一個 ARWorldTrackingConfiguration 實(shí)例 configuration ,然后用這個configuration 運(yùn)行 ARSession對象。
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// Run the view's session
sceneView.session.run(configuration)
4. 查看 ViewController.swift 的 viewWillDisappear:方法,在視圖消失的時候,停止這個session,和 session.run
成對出現(xiàn)。
// Pause the view's session
sceneView.session.pause()
但是!我們發(fā)現(xiàn)這個飛船是在viewDidLoad的時候加載的,并沒有融入對世界理解,也沒有交互!
看完系統(tǒng)默認(rèn)添加的代碼之后,在編寫AR 代碼之前,我們先給我們 Demo 搭建一個簡單的視圖框架。
1.3 利用storyboard快速構(gòu)建Demo視圖層級
新建三個ViewController,繼承自UIViewController,每一個控制器代表一個功能:
- TKWorldTrackingViewController 負(fù)責(zé)實(shí)現(xiàn)平面檢測功能
- TKFaceTrackingViewController 負(fù)責(zé)實(shí)現(xiàn)人臉檢測相關(guān)功能
- TKImageRecognizeViewController 負(fù)責(zé)實(shí)現(xiàn)物體識別相關(guān)功能
并利用storyboard快速構(gòu)建整個如圖層級,關(guān)于storyboard的使用,本教程不做闡述,完成之后如下如所示:
此時,準(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會話,我們希望在 當(dāng)前 view 出現(xiàn)的時候運(yùn)行會話。在 viewWillAppear:
中創(chuàng)建一個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 消失的時候,在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)行了,接下來要做的就是,在檢測到平面之后,接收并處理ARKit發(fā)出來的通知。
實(shí)現(xiàn)思路如下:
- 上一遍文章關(guān)于世界追蹤的描述中有提到過,ARKit 檢測到平面后,會在場景中添加錨點(diǎn)
- 遵守 sceneView 的 ARSCNViewDelegate ,實(shí)現(xiàn)
renderer(: didAdd: for:)
方法,當(dāng)場景中添加錨點(diǎn)的時候,viewcontroller 就可以收到通知 - 判斷新添加錨點(diǎn)的類型,如果是 ARPlaneAnchor 類型,就認(rèn)為檢測到平面了
讓 TKWorldTrackingViewController
遵守 ARSCNViewDelegate
。
class TKWorldTrackingViewController: UIViewController,ARSCNViewDelegate
然后在viewDidLoad:
中添加如下代碼:
sceneView.delegate = self
并實(shí)現(xiàn)代理方法,判斷當(dāng)前新增的錨點(diǎn)類型,如果是 ARPlaneAnchor,就在當(dāng)前錨點(diǎn)出添加一個 box。
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// 1. 判斷當(dāng)前新增的錨點(diǎn)類型
guard anchor is ARPlaneAnchor else { return }
// 2. 在檢測到的平面處添加 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)擊 “Demo1:平面檢測” 按鈕,效果如下:
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. 在檢測到的人臉處添加 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:人臉檢測” 按鈕,效果如下:
4. 開發(fā)基于AR的圖像識別功能
4.1 將需要識別的2D圖片導(dǎo)入到項(xiàng)目中
在 Assets.xcassets 文件目錄下新建一個 AR Resource Group 類型的目錄。
然后將要識別的對象,對應(yīng)的2D圖片拖拽到如下圖片中的紅框位置。
接下來的步驟很重要,查看圖片的 Show the Attributes inspector,給圖片設(shè)置大小,這個值是我們需要識別的物體在真實(shí)世界中的大小!!!
這個值的精度直接決定了識別效果。
經(jīng)過測量,我需要識別的企鵝,高度約為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)目中沒有這個文件,會拋出異常。在viewWillApper:
中添加如下代碼。
guard let referenceImages = ARReferenceImage.referenceImages(inGroupNamed: "AR Resources", bundle: nil) else {
fatalError("AR Resources 資源文件不存在 。")
}
接著,新建一個ARWorldTrackingConfiguration實(shí)例,將 referenceImages 賦給 detectionImages
屬性。
let configuration = ARWorldTrackingConfiguration()
configuration.detectionImages = referenceImages
用上面的 configuration 運(yùn)行AR 會話。
sceneView.session.run(configuration)
4.3 添加圖像識別代碼
在renderer(: didAdd: for:)
方法中處理圖像識別結(jié)果的回調(diào),在識別到圖像的位置添加一個平面,做識別結(jié)果可視化標(biāo)識。
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
// 1. 判斷anchor是否為ARImageAnchor
guard let imageAnchor = anchor as? ARImageAnchor else { return }
// 2. 在檢測到的物體圖像處添加 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添加到檢測到的圖像錨點(diǎn)處
node.addChildNode(planeNode)
}
在真機(jī)上 run 項(xiàng)目,點(diǎn)擊 “Demo3:物體識別” 按鈕,效果如下:
至此,我們完成了關(guān)于我們第一個AR項(xiàng)目,接下來會圍繞SceneKit,系統(tǒng)的介紹和 3D 內(nèi)容渲染相關(guān)的內(nèi)容。