この投稿は何?
iOS アプリケーションのための拡張現実フレームワーク ARKit を使って、以下の機能を実装します。
- 現実空間にある水平で平らな面(サーフェス)を検出する
- 検出した平面に仮想ノード(四角いプレーン)を置く
ARKit のキホン
ARKit は、iPhone が認識した現実空間に、仮想的なオブジェクトを投影します。
なお、以前に投稿した記事「【ARKit】拡張現実テンプレートを再現する」で、ARKitを使用するまでの準備を解説しています。
この投稿では、前述した2つの機能にフォーカスします。
開発環境
この投稿の内容は、以下の環境で開発しました。
また、ARアプリケーションの動作を確認するには、iPhoneSE 以降の実機が必要です。
予め、Xcode と iPhone が正しく接続できることを確認してください。
- macOS10.14.2 がインストールされた Macbook Pro
- Xcode10.1
- iOS12.1 がインストールされた iPhoneX
手順
プロジェクトを作成する
- Create a new Xcode project を選択します。
- Product Name は、「Find Flat Surface」としておきます。
- Augmented Reality App Template を選択します。
現実空間を追跡する
ARKit は、iPhone に搭載されたカメラとモーションセンサーを使って、現実空間をトラッキングします。
現実空間のトラッキングは、セッションとして実行されます。
アプリが起動されて画面に表示されたタイミングでセッションを実行し、アプリがバックグラウンドになり画面から非表示になったらセッションを停止します。
また、ARKit で発生したイベント(「平面を検出」したり「検出した情報を更新」したとき)の処理を ViewController クラス側で扱えるように、デリゲートしています。
[平面を検出!!]--------------------->[プレーンを置く]
ARKit ---------[デリゲート]-------> ViewController
class ViewController: UIViewController, ARSCNViewDelegate {
@IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
}
平面を検出する
コンフィギュレーションを設定する
ARセッションで、どんな機能を有効にするかを指定する ARWorldTrackingConfiguration オブジェクトのインスタンス configuration を生成しています。
このインスタンスの planeDetection プロパティで、「水平な平面」を検出することを指定します。
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal]
sceneView.session.run(configuration)
}
プレーンを配置する
アンカーとは
ARKit は、カメラ画像内で平面を検出すると、ARPlaneAnchor オブジェクトというアンカーを生成します。
アンカーは、その平面の寸法や位置の情報を持っています。
プレーンノードを作る
シーン(3Dの仮想空間)にあるすべての物体は Node として管理されます。
シーンは、SceneGraph と呼ばれるツリー構造になっており、すべてのノードは1つの親ノード(ルート)の子ノードとして追加されます。
なお、ノードは主に以下の情報があります。
- ジオメトリ(形状)
- ポジション(座標)
今回は、SCNPlane 型オブジェクトを使って、板状のジオメトリを持つノードを生成します。
この板状ノードのサイズは ARKit が検出したアンカー情報に基づいています。
また、ポシジョンは3軸(x, y, z)の3D座標系で指定されます。
アンカー情報の y 軸は高さ方向の数値なので、水平な板状ノードの寸法は x, z軸の数値に基づきます。
なお、生成した板状ノードは、デフォルト状態で垂直に立っている状態です。
x軸方向に -90 度回転することで、検出した平面に置いたようにします。
func createFloor(from anchor: ARPlaneAnchor) -> SCNNode{
let anchorWidth = CGFloat(anchor.extent.x)
let anchorHeight = CGFloat(anchor.extent.z)
let planeGeometry = SCNPlane(width: anchorWidth, height: anchorHeight)
planeGeometry.firstMaterial?.diffuse.contents = UIColor.green
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.eulerAngles.x = -Float.pi/2
planeNode.opacity = 0.25
return planeNode
}
平面を検出したときのデリゲート処理
検出されたアンカー情報から、板状ノードを生成して、ルートノードに追加します。
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
let floor = createFloor(from: planeAnchor)
node.addChildNode(floor)
}
平面情報が更新されたときのデリゲート処理
ARKit はセッション実行中に絶え間なく現実空間を追跡し、その情報を更新し続けることで、高品質な体験をユーザに提供します。
すでに検出されている平面のサイズが変化するなどアンカー情報が更新されたとき、板状ノードのポジション・ジオメトリも変更します。
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor,
let planeNode = node.childNodes.first,
let planeNodeGeometry = planeNode.geometry as? SCNPlane
else { return }
let updatedPosition = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)
planeNode.position = updatedPosition
planeNodeGeometry.width = CGFloat(planeAnchor.extent.x)
planeNodeGeometry.height = CGFloat(planeAnchor.extent.z)
}
完成したコード
完成したコードは以下のようになります。
import UIKit
import SceneKit
import ARKit
class ViewController: UIViewController, ARSCNViewDelegate {
@IBOutlet var sceneView: ARSCNView!
override func viewDidLoad() {
super.viewDidLoad()
sceneView.delegate = self
sceneView.debugOptions = [.showWorldOrigin, .showFeaturePoints]
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let configuration = ARWorldTrackingConfiguration()
configuration.planeDetection = [.horizontal]
sceneView.session.run(configuration)
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
sceneView.session.pause()
}
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor else { return }
let floor = createFloor(from: planeAnchor)
node.addChildNode(floor)
}
func renderer(_ renderer: SCNSceneRenderer, didUpdate node: SCNNode, for anchor: ARAnchor) {
guard let planeAnchor = anchor as? ARPlaneAnchor,
let planeNode = node.childNodes.first,
let planeNodeGeometry = planeNode.geometry as? SCNPlane
else { return }
let updatedPosition = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)
planeNode.position = updatedPosition
planeNodeGeometry.width = CGFloat(planeAnchor.extent.x)
planeNodeGeometry.height = CGFloat(planeAnchor.extent.z)
}
func createFloor(from anchor: ARPlaneAnchor) -> SCNNode{
let anchorWidth = CGFloat(anchor.extent.x)
let anchorHeight = CGFloat(anchor.extent.z)
let planeGeometry = SCNPlane(width: anchorWidth, height: anchorHeight)
planeGeometry.firstMaterial?.diffuse.contents = UIColor.green
let planeNode = SCNNode(geometry: planeGeometry)
planeNode.eulerAngles.x = -Float.pi/2
planeNode.opacity = 0.25
return planeNode
}
}
ビルド
実機の iPhone を接続して実行します。
アプリが起動すると、ARKit が水平な面を検出して、その位置に半透明なグリーンのノードを表示します。
また、カメラを動かしながら移動すると、トラッキング情報が更新されて、ノードの位置と形状が変化します。
まとめ
「ARKit が現実空間をトラッキング → 平面を検出 → その情報から仮想オブジェクトを配置」する仕組みが理解できました。
随所に SceneKit の概念が見られますが、ARプログラミングには欠かせないようです。
合わせて、学習していく必要性を感じました。