ARアプリ導入部インタラクションの最小実装

[AR Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ar) 16日目の記事です。

# はじめに

ARアプリにおいて、ARコンテンツの表示位置設定は毎回課題となる処理です。

特定のマーカーにカメラを向けるとコンテンツが表示される場合は分かりやすいですが、例えばテーブルや床から平面を検知させオブジェクトを表示したい場合は、ユーザが迷わないような導入部を作る必要があります。

Apple提供サンプルコード [Placing Objects and Handling 3D Interaction](https://developer.apple.com/documentation/arkit/placing_objects_and_handling_3d_interaction) ではiOS13から追加された [ARCoachingOverlayView](https://developer.apple.com/documentation/arkit/arcoachingoverlayview) と独自の FocusSquare クラスを使用し自然な導入インタラクションを実現しています。

自作ARアプリでも利用できるよう、今回はその2つを連動させた最小実装を書いてみます。

<img width="375" alt="Image from iOS (3).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/ddf49b9f-2bb3-5b6c-9cdd-f36e2c94d4c5.png">
<img width="375" alt="Image from iOS (4).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/ef1ae785-d015-5f6f-2fcf-0fd3e837f3d1.png">


# 最小実装

## 新規プロジェクト作成

今回はSwift、SceneKitを使用し新規プロジェクトを作成します。

<img width="735" alt="スクリーンショット 2019-12-17 2.23.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/2bfb4751-15ff-ea82-ba7d-c282cd51ee03.png">
【図: Augmented Reality Appを新規作成】

## FocusSquareクラス追加

[Placing Objects and Handling 3D Interaction](https://developer.apple.com/documentation/arkit/placing_objects_and_handling_3d_interaction) から `Focus Square` フォルダをコピーしプロジェクトに追加。

<img width="829" alt="スクリーンショット 2019-12-17 2.24.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/7200da51-12cc-c7d7-6512-d429e98a1dc8.png">
【図: Placing Objects and Handling 3D Interaction プロジェクト内の Focus Square】

<img width="295" alt="スクリーンショット 2019-12-17 2.28.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/3418855f-8724-df14-17da-c3105a1a67f6.png">
【図: 追加時オプションは Create groups】

<img width="272" alt="スクリーンショット 2019-12-17 2.25.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/87b1a50e-0263-b3d4-ec0d-080b8ac4b0a1.png">
【図: 追加後の状態】

## ViewController実装

以下をViewControllerに実装。

```swift
import UIKit
import SceneKit
import ARKit

class ViewController: UIViewController {

    @IBOutlet private var sceneView: ARSCNView!

    private let coachingOverlay = ARCoachingOverlayView()
    private let focusSquare = FocusSquare()
    private let updateQueue = DispatchQueue(label: "tokyo.shmdevelopment.serialSceneKitQueue")
    
    // MARK: - View Controller Life Cycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        sceneView.delegate = self
        sceneView.showsStatistics = true
        sceneView.debugOptions = .showFeaturePoints

        setupCoachingOverlay()
        sceneView.scene.rootNode.addChildNode(focusSquare)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let configuration = ARWorldTrackingConfiguration()
        configuration.planeDetection = [.horizontal]
        sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    }
        
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        
        sceneView.session.pause()
    }

    // MARK: - Focus Square

    private func updateFocusSquare(isObjectVisible: Bool) {
        if isObjectVisible || coachingOverlay.isActive {
            focusSquare.hide()
        } else {
            focusSquare.unhide()
        }
        
        if let camera = sceneView.session.currentFrame?.camera,
            case .normal = camera.trackingState,
            let query = sceneView.getRaycastQuery(),
            let result = sceneView.castRay(for: query).first {
            
            updateQueue.async {
                self.sceneView.scene.rootNode.addChildNode(self.focusSquare)
                self.focusSquare.state = .detecting(raycastResult: result, camera: camera)
            }
        } else {
            updateQueue.async {
                self.focusSquare.state = .initializing
                self.sceneView.pointOfView?.addChildNode(self.focusSquare)
            }
        }
    }
}

extension ViewController: ARSCNViewDelegate {

    func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
        DispatchQueue.main.async {
            self.updateFocusSquare(isObjectVisible: false)
        }
    }
}

extension ViewController: ARCoachingOverlayViewDelegate {
    
    private func setupCoachingOverlay() {
        coachingOverlay.session = sceneView.session
        coachingOverlay.delegate = self
        coachingOverlay.translatesAutoresizingMaskIntoConstraints = false
        sceneView.addSubview(coachingOverlay)
        
        NSLayoutConstraint.activate([
            coachingOverlay.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            coachingOverlay.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            coachingOverlay.widthAnchor.constraint(equalTo: view.widthAnchor),
            coachingOverlay.heightAnchor.constraint(equalTo: view.heightAnchor)
            ])
        
        coachingOverlay.goal = .horizontalPlane
    }
}

extension ARSCNView {

    fileprivate func castRay(for query: ARRaycastQuery) -> [ARRaycastResult] {
        return session.raycast(query)
    }

    fileprivate func getRaycastQuery(for alignment: ARRaycastQuery.TargetAlignment = .any) -> ARRaycastQuery? {
        return raycastQuery(from: screenCenter, allowing: .estimatedPlane, alignment: alignment)
    }

    fileprivate var screenCenter: CGPoint {
        return CGPoint(x: bounds.midX, y: bounds.midY)
    }
}
```

<img width="375" alt="Image from iOS (5).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/72bce077-b6e4-9129-a994-3117a3240cd7.png">
【図: 対象外領域の場合には枠表示が変化】


# さいごに

Safariから3DオブジェクトをAR表示できるようになりました。
ARアイコンをタップするだけでカメラが起動し自然なインタラクションでオブジェクトの表示や操作ができます。

<img width="375" alt="Image from iOS (8).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/30111f14-57e0-cafd-e23f-5f37785e58ba.png">
【図: ARアイコンがあるWebサイト】

<img width="375" alt="Image from iOS (6).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/85a7a0ee-3bd4-a4f4-1833-fd4857dfffd7.png">
【図: オブジェクト読み込み完了後、半透明表示】

<img width="375" alt="Image from iOS (7).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/6db4aa60-3e9e-dbd9-677d-a9fe631ade27.png">
【図: オブジェクト読み込み完了後、半透明表示で数秒後】


またApple提供サンプルコード [SwiftShot](https://developer.apple.com/documentation/arkit/swiftshot_creating_a_game_for_augmented_reality) では、FocusSquare表示が多機能になっていて、ゲーム盤面のサイズ指定、位置指定、向き調整が可能になっています。

<img width="375" alt="Image from iOS (1).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/63205/69f3f521-eedc-401d-6701-cdcbc607ba2b.png">
【図: SwiftShot(枠線色を橙に変更)】

導入部実装は一度作れば使い回せるのでこれを機に自分用の導入インタラクションを作れると良いですね！