ARKit, SceneKit個人的まとめ - Vol.1

仕事でARKitを触ることになったのでまとめ
**・[基礎編はこちら](https://qiita.com/jbnkk40/items/f83f632e73a48f0908de)**

# やったこと
カメラの映像に
・テキストを作成・配置する
・テクスチャを作成・配置する
・タップで動かす
・ドラッグで動かす
・平面を検出する

#### 使ったもの
・ARKit
・SceneKit

# コード
#### テキストを配置する
・backgroundColor出ても最初の一瞬だけ
・SCNViewとARSCNViewは違うらしい->**あとで追記**
・willAppearでconfig設定してrun & willDisAppearでpauseの流れが鉄板らしい
->[平面検出](https://qiita.com/jbnkk40/items/fc7a6cbac66fd02df6e7#%E3%81%9D%E3%82%8C%E3%82%89%E3%82%92%E3%83%89%E3%83%A9%E3%83%83%E3%82%B0%E3%81%A7%E5%8B%95%E3%81%8B%E3%81%99%E5%B9%B3%E9%9D%A2%E3%82%92%E6%A4%9C%E5%87%BA%E3%81%99%E3%82%8B)のときは`viewDidLoad`ではなく`render(_ didAdd)`に書く
->**リアルタイムでテクスチャの座標を更新したい**場合とか

```ARText.swift
import UIKit
import SceneKit
import ARKit
 
class ViewController: UIViewController, ARSCNViewDelegate {
 
    @IBOutlet weak var scnView: ARSCNView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
 
        let scene = SCNScene()
        scnView.backgroundColor = UIColor.red
        scnView.allowsCameraControl = true //カメラ位置をタップでコントロール可能にする
        scnView.scene = scene

        let str = "本日は晴天なり"
        let depth: CGFloat = 0.5
        let text = SCNText(string: str, extrusionDepth: depth)
        text.font = UIFont(name: "HiraKakuProN-W6", size: 0.5);
        let textNode = SCNNode(geometry: text)
        
        let (min, max) = (textNode.boundingBox)
        let x = CGFloat(max.x - min.x)
        let y = CGFloat(max.y - min.y)
        textNode.position = SCNVector3(-(x/2), -1, -2)
        
        print("\(str) width=\(x)m height=\(y)m depth=\(depth)m")

        scnView.scene.rootNode.addChildNode(textNode)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        let configuration = ARWorldTrackingConfiguration()
        scnView.session.run(configuration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        scnView.session.pause()
    }
}
```
#### テクスチャを作成・配置する
SCNFloorは無限平面
SCNPlaneは有限平面？

#### ドラッグ・タップで動かす・平面を検出する
・secondSceneViewという名前に特に意味はないです
・namedにはファイル名、inDirectoryにはパスを書きましょう(当たり前のはずなのにここ逆にしてて**時間ムッチャ無駄にしたのでメモ**)
->ここのsceneはこの情報のSCNSceneが存在するかどうかnilチェックしてるだけ
->viewDidLoadのsceneSecondとは別物

```swift
guard let scene = SCNScene(named: "FinalBaseMesh.scn", inDirectory: "art.scnassets") else {fatalError()}
```
・withNameはidentityのNameにしよう
->**Nameはファイル名(.scn)と一緒でないとここでクラッシュする**
->GeometoryのNameもこれと同じにすると平面検出ごとにテクスチャが新規追加される
->テクスチャは出るけどなんか一色の塊みたいな感じになってて変(Sampleにある`ship.scn`はlightの設定加えたらでた)

```swift
secondSceneView.autoenablesDefaultLighting = true
```
->**あとで追記**

```swift
guard let bearNode = scene.rootNode.childNode(withName: "FinalBaseMesh", recursively: true) else {fatalError()}
```

```ARDrag+DetectSurface.swift
import UIKit
import SceneKit
import ARKit

class SecondViewController: UIViewController, ARSCNViewDelegate {

    @IBOutlet var secondSceneView: ARSCNView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let sceneSecond = SCNScene()
        secondSceneView.scene = sceneSecond
        secondSceneView.delegate = self
        secondSceneView.showsStatistics = true
        secondSceneView.debugOptions = ARSCNDebugOptions.showFeaturePoints
        //tapGesture
        secondSceneView.addGestureRecognizer(UITapGestureRecognizer(
            target: self, action: #selector(self.tapView(sender:))))
        secondSceneView.addGestureRecognizer(UIPanGestureRecognizer(
            target: self, action: #selector(self.dragView(sender:))))
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        let configuration = ARWorldTrackingConfiguration()
        //検知対象を指定する。現在は水平のみ指定可能。
        configuration.planeDetection = .horizontal
        //セッションを稼働
        secondSceneView.session.run(configuration)
    }
    
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        //セッションを停止
        secondSceneView.session.pause()
    }
    

    // MARK: - ARSCNViewDelegate
    
    func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
        print("\(self.classForCoder)/" + #function)
        guard let planeAnchor = anchor as? ARPlaneAnchor else {fatalError()}
        //sceneとnodeを読み込み
        guard let scene = SCNScene(named: "FinalBaseMesh.scn", inDirectory: "art.scnassets") else {fatalError()}
        //identityちゃんと設定
        guard let bearNode = scene.rootNode.childNode(withName: "FinalBaseMesh", recursively: true) else {fatalError()}
        // nodeのスケールを調整する
        let (min, max) = bearNode.boundingBox
        let w = CGFloat(max.x - min.x)
        // 1mを基準にした縮尺を計算
        let magnification = 1.0 / w
        bearNode.scale = SCNVector3(magnification, magnification, magnification)
        // nodeのポジションを設定
        bearNode.position = SCNVector3(planeAnchor.center.x, 0, planeAnchor.center.z)

        //作成したノードを追加
        DispatchQueue.main.async(execute: {
            node.addChildNode(bearNode)
        })

    }
    
    @objc func tapView(sender: UIGestureRecognizer) {
        let tapPoint = sender.location(in: secondSceneView)
        let results = secondSceneView.hitTest(tapPoint, types: .existingPlaneUsingExtent)
        if !results.isEmpty {
            if let result = results.first ,
                let anchor = result.anchor ,
                let node = secondSceneView.node(for: anchor) {
                
                let action1 = SCNAction.rotateBy(x: CGFloat(-90 * (Float.pi / 180)), y: 0, z: 0, duration: 0.5)
                let action2 = SCNAction.wait(duration: 1)

                DispatchQueue.main.async(execute: {
                    node.runAction(
                        SCNAction.sequence([
                        action1,
                        action2,
                        action1.reversed()
                        ])
                    )
                })
                
            }
        }
    }
    
    @objc func dragView(sender: UIGestureRecognizer) {
        let tapPoint = sender.location(in: secondSceneView)
        
        let results = secondSceneView.hitTest(tapPoint, types: .existingPlane)
        if !results.isEmpty {
            if let result = results.first ,
                let anchor = result.anchor ,
                let node = secondSceneView.node(for: anchor) {
                
                DispatchQueue.main.async(execute: {
                    // 実世界の座標をSCNVector3で返したものを反映
                    node.position = SCNVector3(result.worldTransform.columns.3.x, result.worldTransform.columns.3.y, result.worldTransform.columns.3.z)
                })
            }
        }
    }
    
}
```

#### 分からないこと
・ライフサイクルとrenderの違いはなに
->backgroundcolor一瞬しか反映しない

SCNViewだと怒られないことがARSCNViewだと怒られた
->インスタンス書いてなかったから？

```
Terminating app due to uncaught exception 'NSInvalidUnarchiveOperationException', reason: 'Could not instantiate class named ARSCNView because no class named ARSCNView was found; the class needs to be defined in source code or linked in from a library (ensure the class is part of the correct target)'
```

```swift
@IBOutlet var sceneView: ARSCNView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        //let scnView = self.view as! SCNView
        sceneView.backgroundColor = UIColor.black
    }
```

# 参考
**・[ARKitで簡単ARやってみた - Qiita](https://qiita.com/pontake-dev/items/daf4a27cccc222f9ae9d)**
**・[ARKitのはじめかた その1「5分で出来るARアプリ」 - Qiita](https://qiita.com/1901drama/items/c0eb01b16370c48f84cd)**
**・[SceneKitのシーンやノードを理解【3次元世界への入り口】 – techpartner](https://techpartner.jp/blog/basic-sceankit)**