概要
- iOS の ARKit の平面検出を使って Hello World を平面上 (水平・垂直) に表示するアプリを作る
- Storyboard の状態を修正せず、ソースコードだけで実現する
今回の環境
- Xcode 10.3
- ARKit 2
- iPhone X + iOS 12.4.1
プロジェクトの作成
Xcode にてテンプレートから iOS + Argumented Reality App を選択する。
Language に Swift を、Content Technology に SceneKit を指定する。
AppDelegate.swift と ViewController.swift という Swift のソースコードが用意される。
ソースコード
ViewController.swift を修正する。
viewDidLoad メソッドを以下のように書き換える。
SCNScene を自前で生成する。
検出した3D空間の特徴点を表示するように設定する。
override func viewDidLoad() {
super.viewDidLoad()
// Set the view's delegate
sceneView.delegate = self
// Show statistics such as fps and timing information
sceneView.showsStatistics = true
// シーンを生成
let scene = SCNScene()
// Set the scene to the view
sceneView.scene = scene
// デバッグ用設定
// 検出した3D空間の特徴点を表示する
sceneView.debugOptions = [.showFeaturePoints]
}
viewWillAppear メソッドを以下のように書き換える。
平面検出機能を有効にする。horizontal で水平面の検出、vertical で垂直面の検出が可能になる。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// Create a session configuration
let configuration = ARWorldTrackingConfiguration()
// 平面検出 (水平・垂直) が有効になるように設定
configuration.planeDetection = [.horizontal, .vertical]
// Run the view's session
sceneView.session.run(configuration)
}
以下の renderer(SCNSceneRenderer, didAdd: SCNNode, for: ARAnchor) メソッドを追加する。
平面を新しく検出した際に、平面可視化用の平面ジオメトリと、Hello World テキストを生成している。
// ARSCNViewDelegate#renderer
// Tells the delegate that a SceneKit node corresponding to a new AR anchor has been added to the scene.
// 新しいアンカーに対応するノードがシーンに追加されたときに呼び出される
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if let planeAnchor = anchor as? ARPlaneAnchor {
// 平面ジオメトリを作成
let planeGeometry = ARSCNPlaneGeometry(device: sceneView.device!)!
// 検出した平面の形状 (アンカーのジオメトリ) に平面ジオメトリを合わせる
planeGeometry.update(from: planeAnchor.geometry)
// 平面ジオメトリにランダムな色を設定
let planeColor = UIColor(
red: CGFloat.random(in: 0.0 ... 1.0),
green: CGFloat.random(in: 0.0 ... 1.0),
blue: CGFloat.random(in: 0.0 ... 1.0),
alpha: 0.2) // 目立たせたくないので半透明にする
planeGeometry.materials.first?.diffuse.contents = planeColor
// 平面ジオメトリ用ノードを作成
let planeNode = SCNNode(geometry: planeGeometry)
// 表示するテキストを用意
let text = SCNText(string: "Hello, world!", extrusionDepth: 0.0)
text.font = UIFont.boldSystemFont(ofSize: CGFloat(planeAnchor.extent.x / 10.0))
// テキストに色を設定: 水平面は白色、垂直面は黒色
let textColor = planeAnchor.alignment == .horizontal ? UIColor.white : UIColor.black
text.materials.first?.diffuse.contents = textColor
// テキスト用ノードを作成
let textNode = SCNNode(geometry: text)
// テキストノードの中心を座標の基準にする
let (min, max) = (textNode.boundingBox)
let textBoundsWidth = (max.x - min.x)
let textBoundsheight = (max.y - min.y)
textNode.pivot = SCNMatrix4MakeTranslation(textBoundsWidth/2 + min.x, textBoundsheight/2 + min.y, 0)
// テキストノードをx軸周りに90度回転して、検出した平面に沿わせる
textNode.transform = SCNMatrix4MakeRotation(-Float.pi / 2.0, 1, 0, 0)
// 検出した平面の中心にテキストノードを置く
textNode.position = SCNVector3(planeAnchor.center)
// 平面ジオメトリ用ノードにテキスト用ノードを追加
planeNode.addChildNode(textNode)
DispatchQueue.main.async(execute: {
// アンカーに対応するノードに平面ジオメトリ用ノードを追加
node.addChildNode(planeNode)
})
}
}
以下の renderer(SCNSceneRenderer, didUpdate: SCNNode, for: ARAnchor) メソッドを追加する。
検出した平面の形状を更新した際に、平面可視化用の平面ジオメトリと、Hello World テキストの状態を更新している。
// ARSCNViewDelegate#renderer
// Tells the delegate that a SceneKit node's properties have been updated to match the current state of its corresponding anchor.
// ノードのプロパティに現在のアンカーの状態が反映されたときに呼び出される
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
if let planeAnchor = anchor as? ARPlaneAnchor {
DispatchQueue.main.async(execute: {
for childNode in node.childNodes {
// 追加したノードの平面ジオメトリを取得
if let plainGeometry = childNode.geometry as? ARSCNPlaneGeometry {
// 検出した平面の形状 (アンカーのジオメトリ) に平面ジオメトリを合わせる
plainGeometry.update(from: planeAnchor.geometry)
// テキスト用ノードを更新
if let textNode = childNode.childNodes.first {
if let text = textNode.geometry as? SCNText {
// 検出した平面のサイズからフォントのサイズをざっくり決める
let size = CGFloat(min(planeAnchor.extent.x, planeAnchor.extent.z) / 10.0)
text.font = UIFont.boldSystemFont(ofSize: size)
// テキストノードの中心を座標の基準にする
let (min, max) = (textNode.boundingBox)
let textBoundsWidth = (max.x - min.x)
let textBoundsheight = (max.y - min.y)
textNode.pivot = SCNMatrix4MakeTranslation(textBoundsWidth/2 + min.x, textBoundsheight/2 + min.y, 0)
// 検出した平面の中心にテキストノードを置く
textNode.position = SCNVector3(planeAnchor.center)
}
}
break
}
}
})
}
}
以下の renderer(SCNSceneRenderer, didRemove: SCNNode, for: ARAnchor) メソッドを追加する。
検出した平面の情報が削除された際に、平面可視化用の平面ジオメトリと、Hello World テキストを削除している。
// ARSCNViewDelegate#renderer
// Tells the delegate that the SceneKit node corresponding to a removed AR anchor has been removed from the scene.
// 削除されたアンカーに対応するノードが削除されたときに呼び出される
func renderer(_ renderer: SCNSceneRenderer, didRemove node: SCNNode, for anchor: ARAnchor) {
for childNode in node.childNodes {
if childNode.geometry as? ARSCNPlaneGeometry != nil {
// 追加した平面ジオメトリ用ノードを削除
childNode.removeFromParentNode()
break
}
}
}
実機での実行結果
実機 (iPhone X + iOS 12.4.1) にインストールして実行してみる。
Hello World テキストが地面に表示されている。
検出された平面の大きさに合わせて Hello World テキストが大きくなる。
壁の垂直面を検出して Hello World テキストを表示。
看板などの垂直の平面に Hello World テキストを表示。
階段の水平・垂直の平面に Hello World テキストを表示。
参考資料
- ARKitのワールドトラッキングについて - 日本語ドキュメント - Apple Developer
- Tracking and Visualizing Planes | Apple Developer Documentation
- ARSCNViewDelegate - ARKit | Apple Developer Documentation
- iOS 11 Programming 第2章 ARKit
- iOS 12 Programming 第4章 ARKit
- iOS12samplecode/04/02_PlaneGeometry at master · peaks-cc/iOS12samplecode