search
LoginSignup
1

More than 3 years have passed since last update.

posted at

updated at

Organization

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

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

はじめに

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

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

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

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

Image from iOS (3).png
Image from iOS (4).png

最小実装

新規プロジェクト作成

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

スクリーンショット 2019-12-17 2.23.45.png
【図: Augmented Reality Appを新規作成】

FocusSquareクラス追加

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

スクリーンショット 2019-12-17 2.24.55.png
【図: Placing Objects and Handling 3D Interaction プロジェクト内の Focus Square】

スクリーンショット 2019-12-17 2.28.32.png
【図: 追加時オプションは Create groups】

スクリーンショット 2019-12-17 2.25.42.png
【図: 追加後の状態】

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

Image from iOS (5).png
【図: 対象外領域の場合には枠表示が変化】

さいごに

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

Image from iOS (8).png
【図: ARアイコンがあるWebサイト】

Image from iOS (6).png
【図: オブジェクト読み込み完了後、半透明表示】

Image from iOS (7).png
【図: オブジェクト読み込み完了後、半透明表示で数秒後】

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

Image from iOS (1).png
【図: SwiftShot(枠線色を橙に変更)】

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1