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 を選択する。

<img width="726" alt="arkit-1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/bd7d3e5d-1505-3a75-3354-69b416db7a3b.png">

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

<img width="727" alt="arkit-2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/d0afd529-7c63-c5d8-dadf-e5e868953143.png">

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

<img width="956" alt="arkit-3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/93592449-4f27-1942-8cb2-0cd29f3761fd.png">

## ソースコード

ViewController.swift を修正する。

viewDidLoad メソッドを以下のように書き換える。
SCNScene を自前で生成する。
検出した3D空間の特徴点を表示するように設定する。

```swift
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 で垂直面の検出が可能になる。

```swift
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 テキストを生成している。

```swift
// 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 テキストの状態を更新している。

```swift
// 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 テキストを削除している。

```swift
// 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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/67934fc4-d35f-86f8-af6c-9cf47d769b87.jpeg)

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

![arkit-5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/66c69514-d93a-974e-1dad-381d7e567dfe.jpeg)

![arkit-6.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/00725443-386c-e155-efdb-754413c90d3c.jpeg)

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

![arkit-7.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/f175215c-e21c-6c1a-2482-6c542460b682.jpeg)

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

![arkit-8.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/f287fa3a-c2cc-2d04-f571-b770bdbc1ffd.jpeg)
![arkit-9.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/5e494c52-b36d-5c7b-27c6-2ceb154f4b47.jpeg)

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

![arkit-a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/139365/0c97cc35-68df-37a5-fa32-3a7dbc602dba.jpeg)

## 参考資料
- [ARKitのワールドトラッキングについて \- 日本語ドキュメント \- Apple Developer](https://developer.apple.com/jp/documentation/arkit/understanding_world_tracking_in_arkit/)
- [Tracking and Visualizing Planes \| Apple Developer Documentation](https://developer.apple.com/documentation/arkit/tracking_and_visualizing_planes)
- [ARSCNViewDelegate \- ARKit \| Apple Developer Documentation](https://developer.apple.com/documentation/arkit/arscnviewdelegate)
- [iOS 11 Programming 第2章 ARKit](https://peaks.cc/books/iOS11)
- [iOS 12 Programming 第4章 ARKit](https://peaks.cc/iOS12)
- [iOS12samplecode/04/02\_PlaneGeometry at master · peaks\-cc/iOS12samplecode](https://github.com/peaks-cc/iOS12samplecode/tree/master/04/02_PlaneGeometry)