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


AR Advent Calendar 2019 16日目の記事です。

はじめに

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

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

Apple提供サンプルコード Placing Objects and Handling 3D Interaction ではiOS13から追加された ARCoachingOverlayView と独自の FocusSquare クラスを使用し自然な導入インタラクションを実現しています。

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


最小実装

新規プロジェクト作成

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


【図: Augmented Reality Appを新規作成】

FocusSquareクラス追加

Placing Objects and Handling 3D Interaction から Focus Square フォルダをコピーしプロジェクトに追加。


【図: Placing Objects and Handling 3D Interaction プロジェクト内の Focus Square】


【図: 追加時オプションは Create groups】


【図: 追加後の状態】

ViewController実装

以下をViewControllerに実装。

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)
    }
}


【図: 対象外領域の場合には枠表示が変化】

さいごに

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


【図: ARアイコンがあるWebサイト】


【図: オブジェクト読み込み完了後、半透明表示】


【図: オブジェクト読み込み完了後、半透明表示で数秒後】

またApple提供サンプルコード SwiftShot では、FocusSquare表示が多機能になっていて、ゲーム盤面のサイズ指定、位置指定、向き調整が可能になっています。


【図: SwiftShot(枠線色を橙に変更)】

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