[Swift4]ARKitで球体をランダムに描画する

どうも。筋肉エンジニアのtakuyama29です。

最近はマッチングアプリとARKitに心踊らせております。

今日は先日リリースされたARKitについての学習のために簡単なサンプルアプリを作成したので、その実装内容について振り返りながら書いていきます。

# できたもの

作成したサンプルアプリは下記で公開しているので、興味のある方は見てみてください。

Github: [takuyama29/WorldTracking](https://github.com/takuyama29/WorldTracking)

**完成品はこんな感じです**

![Untitled.gif](https://qiita-image-store.s3.amazonaws.com/0/143619/aa53d867-96db-471f-f01a-78001d3343db.gif)

# 環境

このサンプルアプリを使うために必要な環境は以下のとおりです。

- iOS11
- ~~swift3~~
- Swift4
- Xcode9

個人的には、Xcode9からアプリのビルドがワイヤレスになったのがとても嬉しかったです。

特にARアプリのビルドとかをするとスマホを持って歩き回る必要があるので、正直これのためにコードレスにしたんだろうなと思ってます←え


# 実装

今回はARKitの概念を理解するためのサンプルアプリのため、実装はとてもシンプルです。

自分の理解できた範囲でまとめを書いていくので、間違っている点などはコメントで押していただけるとありがたいです。

まずはソース全体を転記します。

```ViewController.swift
import UIKit
import ARKit

class ViewController: UIViewController {
    
    @IBOutlet weak var sceneView: ARSCNView!
    let configration = ARWorldTrackingConfiguration()
        
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
        self.sceneView.session.run(configration)
        self.sceneView.autoenablesDefaultLighting = true
        
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    @IBAction func addButton(_ sender: UIButton) {
        
        let node = SCNNode()

        // Box Node
        node.geometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.05)
        let x = randomNumbers(firstNum: -0.3, secondNum: 0.3)
        let y = randomNumbers(firstNum: -0.3, secondNum: 0.3)
        let z = randomNumbers(firstNum: -0.3, secondNum: 0.3)


        node.geometry?.firstMaterial?.specular.contents = UIColor.black
        node.geometry?.firstMaterial?.diffuse.contents = UIColor.yellow
        node.position = SCNVector3(x,y,z)
        
        self.sceneView.scene.rootNode.addChildNode(node)

    }
    
    @IBAction func resetButton(_ sender: UIButton) {
        resetSession()
    }
    
    func resetSession(){
        self.sceneView.session.pause()
        self.sceneView.scene.rootNode.enumerateChildNodes {(node, _) in
            node.removeFromParentNode()
        }
        
        self.sceneView.session.run(configration, options: [.resetTracking, .removeExistingAnchors])
    }
    
    func randomNumbers(firstNum: CGFloat, secondNum: CGFloat) -> CGFloat {
        return CGFloat(arc4random()) / CGFloat(UINT32_MAX) * abs(firstNum - secondNum) + min(firstNum, secondNum)
    }
}
```

今回のサンプルで編集した内容はこのViewControllerのみです。

### 1. ARの基本的な準備

AR機能を活用する上で必ず必要な実装があります。

**1. ARKitのimport**

ARKitフレームワークAR機能を使う上で必須なので忘れずimportをしてください。


**2. ARSCNViewの配置**

ARSCNViewはUIViewのサブクラスで、端末のカメラで写された景色をレンダリングするためのViewのことです。

<img width="600" alt="Screen_Shot_2017-09-23_at_16_14_50.png" src="https://qiita-image-store.s3.amazonaws.com/0/143619/d2727478-e63e-9784-9314-8a50995990f8.png">

このViewをViewController上にフルサイズで配置して、そのViewの中でSceneを描画することで様々な表現をすることができます。


**3. ARWorldTrackingConfigurationのインスタンス化**

`ARWorldTrackingConfiguration`は、AR機能(session)が開始した際に、端末のモーショントラッキングとカメラの画像処理を行うための設定クラスのようなものです。

```swift
import ARKit

class ViewController: UIViewController {
    
    @IBOutlet weak var sceneView: ARSCNView!
    let configration = ARWorldTrackingConfiguration()

    ...
}
```

### 2. デバック時に活用するオプションを追加

先ほどインスタンス化した`ARSCNView`には、デバックの用途で使うことでできる[ARSCNDebugOptions](https://developer.apple.com/documentation/arkit/arscndebugoptions)というオプションストラクチャがあります。

このオプションには、2つのARデバックオーバーレイが含まれており、下記の様にARSCNViewの`.debugOptions`プロパティにそれぞれ描画したいオーバーレイを指定することでARSCNView上で表現することができます。

```swift
self.sceneView.debugOptions = [ARSCNDebugOptions.showFeaturePoints, ARSCNDebugOptions.showWorldOrigin]
```

1. `.showFeaturePoints`はARKitがデバイス位置を追跡するためにscene解析した中間結果を示す点群をARSCNView上に描画するオプション(sceneView内の黄色い点群)
2. `.showWorldOrigin`はARKitで表現されるARSCNView内の世界の座標軸(X,Y,Z)を視覚的に表現するベクトルを描画するオプション(赤・青・緑の棒)

<img width="375" alt="IMG_3594.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/143619/f7adc38a-eb4c-c1a8-ef87-5f30cfcc558b.png">


また、`.autoenablesDefaultLighting`はsceneView内に表示するNodeに無指向性の光を追加するオプションです。

defaultはfalseなので、ここにtrueを渡してあげると下記の様な感じになります。

**true**

<img width="375" alt="IMG_3595.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/143619/7504f16f-d12f-8f09-d775-4fabd4dcd35f.png">

**false(default)**

<img width="375" alt="IMG_3596.PNG" src="https://qiita-image-store.s3.amazonaws.com/0/143619/5a122513-a7dc-5372-0f02-fef88eb71032.png">

### 3. ARSCNViewのsessionを開始

ここまででsceneViewの設定が完了したのでsessionを開始するために下記を`viewDidLoad()`へ記述します。

```.siwft
self.sceneView.session.run(configration)
```

### 4. カメラを使用する際のパーミッション文言を追加

AR機能を使うためにはカメラを使用する必要があります。
先ほど説明したARSCNViewのsession開始をする`.session.run`を読み込むと、下記の様なアラートダイアログが表示されます。

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/143619/5e998576-416c-ab4d-082b-d297115bf84c.png">

このアラート内に任意の文言を追加したい場合は、Info.plist内に`Privacy - Camera Usage Description`というKeyがあるので、その値に文言を追加します。

<img width="500" alt="Screen_Shot_2017-09-23_at_14_54_32.png" src="https://qiita-image-store.s3.amazonaws.com/0/143619/de58e6fd-047a-8d6e-dc29-464be2e4286c.png">


追加したのはコレ→`You need the camera to display cool ARKit content`

そうすると標準の文字よりも小さいフォントサイズで文言が追加されます。

<img width="300" src="https://qiita-image-store.s3.amazonaws.com/0/143619/c8c4938f-ab07-916a-c9e5-65536e7dcf37.png">

### 5. sceneView内にNodeを追加

ここまででsceneViewの準備が整いました。

ですが、これだけだと見た目はただのカメラと違いがないので、sceneView内にNodeを追加していきます。

**1. SCNNodeをインスタンス化**

今回は[SceneKit](https://developer.apple.com/scenekit/)の[SCNBox](https://developer.apple.com/documentation/scenekit/scnbox)を使用しています。

本来SCNBoxは箱型のオブジェクトなのですが、形の変化させること体感するために球体に変形させています。

```swift
let node = SCNNode()
node.geometry = SCNBox(width: 0.1, height: 0.1, length: 0.1, chamferRadius: 0.05)
```

[chamferRadius](https://developer.apple.com/documentation/scenekit/scnbox/1523302-chamferradius)はNodeオブジェクトの角の丸みを指定するプロパティで、値を0に近づけるほど丸くなっていきます。

また、SCNBoxを生成する際のwidthやheightは、全てメートル単位となっています。


**2. Nodeの配色指定と座標決定**

インスタンス化したNodeの配色は下記の様に設定できます。

```swift
// Node自体の配色
node.geometry?.firstMaterial?.diffuse.contents = UIColor.black
// Nodeの光の配色
node.geometry?.firstMaterial?.specular.contents = UIColor.yellow
```

`.specular`は、先述したNodeの光の配色を指定するプロパティとなります。

次にNodeを描画するsceneView内の座標を指定します。

今回は下記の様に、指定範囲の中で乱数を出力する変数を作成し、NodeのX・Y・Zの座標をランダムに指定します。

```swift
// 任意の範囲で乱数を出力する
func randomNumbers(firstNum: CGFloat, secondNum: CGFloat) -> CGFloat {
    return CGFloat(arc4random()) / CGFloat(UINT32_MAX) * abs(firstNum - secondNum) + min(firstNum, secondNum)
}
```

```swift
// Nodeの座標を決定
let x = randomNumbers(firstNum: -0.3, secondNum: 0.3)
let y = randomNumbers(firstNum: -0.3, secondNum: 0.3)
let z = randomNumbers(firstNum: -0.3, secondNum: 0.3)
node.position = SCNVector3(x,y,z)
```

**3. インスタンス化したNodeをsceneViewに追加**

最後に、sessionが走っているsceneViewの中にNodeを追加します。

```swift
self.sceneView.scene.rootNode.addChildNode(node)
```

これで`Add`ボタンを押した時に新しいNodeがランダムな位置で配置されるようになります。

### 6. sceneViewのsessionをリセット

追加したNodeを全て削除してsceneView内をリセットします。

```swift
func resetSession(){
    // sessionを停止
    self.sceneView.session.pause()
    // 全てのNodeに対して処理を行う
    self.sceneView.scene.rootNode.enumerateChildNodes {(node, _) in
        // Nodeを削除
        node.removeFromParentNode()
    }
    // sessionを再開
    self.sceneView.session.run(configration, options: [.resetTracking, .removeExistingAnchors])
}
```

これで簡単なARアプリの完成です。