Edited at

ARKit Hello World (平面上に描く Hello World テキスト in 拡張現実)


概要


  • iOS の ARKit の平面検出を使って Hello World を平面上 (水平・垂直) に表示するアプリを作る

  • Storyboard の状態を修正せず、ソースコードだけで実現する


今回の環境


  • Xcode 10.3

  • ARKit 2

  • iPhone X + iOS 12.4.1


プロジェクトの作成

Xcode にてテンプレートから iOS + Argumented Reality App を選択する。

arkit-1.png

Language に Swift を、Content Technology に SceneKit を指定する。

arkit-2.png

AppDelegate.swift と ViewController.swift という Swift のソースコードが用意される。

arkit-3.png


ソースコード

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 テキストが地面に表示されている。

arkit-4.jpg

検出された平面の大きさに合わせて Hello World テキストが大きくなる。

arkit-5.jpg

arkit-6.jpg

壁の垂直面を検出して Hello World テキストを表示。

arkit-7.jpg

看板などの垂直の平面に Hello World テキストを表示。

arkit-8.jpg

arkit-9.jpg

階段の水平・垂直の平面に Hello World テキストを表示。

arkit-a.jpg


参考資料