サンプルコード

再構築されたシーンの視覚化と操作

ポリゴンメッシュを使って周囲の物理的形状を測定します。

ダウンロード(英語)

概要

iPad OS 13.4以降を搭載する第4世代のiPad Proで、ARKitは、LiDARスキャナを使って周囲の物理的形状のポリゴンモデルを作成します。LiDARスキャナは、ユーザーの前にある広い範囲の深度情報を瞬時に取得します。そのため、ARKitは、ユーザーが動かなくてもリアルワールドの形状を測定できます。ARKitは深度情報を連続する頂点(vertex)に変換し、これらの頂点が相互に接続されてメッシュを形成します。情報を区分するために、ARKitは複数のアンカーを作成し、個々のアンカーにメッシュの一意の部分が割り当てられます。これらのメッシュアンカーの集合体が、ユーザーを取り巻くリアルワールドのシーンを表します。

メッシュを利用すると、次のことが可能になります。

  • 現実世界の面上の位置をより正確に特定する。

  • ARKitが認識できる現実世界のオブジェクトを分類(classify)する。

  • アプリの仮想コンテンツを、その前にある現実世界のオブジェクトで遮蔽する。

  • 仮想コンテンツと実環境の間のリアルな相互作用を実現する(たとえば、仮想のボールを現実世界の壁にバウンドさせた後、ボールが物理法則に従うようにする)

ここで紹介するサンプルアプリではRealityKitを使ってAR体験を提供します。下の図は、ユーザーがこのアプリを実行し、デバイスを現実世界の椅子に向けたときにRealityKitがARKitからのリアルワールド情報を利用して、デバッグ表示を行う方法を示しています。

カメラフィードに表示される椅子とRealityKitによって視覚化されたメッシュオーバーレイのスクリーンショット

周囲の物理的形状の視覚化

シーンメッシュを有効にするために、サンプルではworld-configurationのsceneReconstruction(英語)プロパティをメッシュオプションの1つに設定します。

arView.automaticallyConfigureSession = false
let configuration = ARWorldTrackingConfiguration()
configuration.sceneReconstruction = .meshWithClassification

サンプルでは、RealityKitのARView(英語)を使ってグラフィックスをレンダリングします。実行時にメッシュを視覚化するために、ARView(英語)にはsceneUnderstanding(英語)デバッグオプションがあります。

arView.debugOptions.insert(.showSceneUnderstanding)

AR体験を開始するにあたって、サンプルではメインビューコントローラのviewDidLoadコールバックを使ってアプリの初回起動時のセッションを設定して実行します。

arView.session.run(configuration)

平面検出機能の追加

アプリのシーン再構築で平面検出機能が有効化されると、ARKitはメッシュの作成時にその情報を考慮します。LiDARスキャナが現実世界の面上で生成したメッシュがやや歪んでいる場合も、ARKitはその面上で平面と判断した部分は、そのメッシュを平坦化します。

平面検出がメッシュにもたらす変化を確認できるように、このアプリには切り替えボタンが表示されます。サンプルでは、ボタンハンドラで平面検出設定を調整し、変更を反映させるためセッションを再起動します。

@IBAction func togglePlaneDetectionButtonPressed(_ button: UIButton) {
    guard let configuration = arView.session.configuration as? ARWorldTrackingConfiguration else {
        return
    }
    if configuration.planeDetection == [] {
        configuration.planeDetection = [.horizontal, .vertical]
        button.setTitle("Stop Plane Detection", for: [])
    } else {
        configuration.planeDetection = []
        button.setTitle("Start Plane Detection", for: [])
    }
    arView.session.run(configuration)
}

オブジェクトの面上の位置の特定

メッシュを使用して面上の位置を取得すると、かつてないほど優れた正確性を実現できます。レイキャストは、メッシュを考慮に入れることで、平坦でない面や特徴がほとんどない面(白い壁など)との交差する位置を得られます。

正確なレイキャストの結果を確認できるように、このアプリは、ユーザーが画面をタップしたときに光線(レイ)をキャストします。サンプルでは、対象とするターゲットとしてARRaycastQuery.Target.estimatedPlane(英語)を、向きのオプションとしてARRaycastQuery.TargetAlignment.any(英語)を指定します。これらはメッシュ化された実世界のオブジェクト上の位置を取得するために必要です。

let tapLocation = sender.location(in: arView)
if let result = arView.raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .any).first {
    // ...

メッシュ化された現実世界の花びんとその表面を交差する光線の図

ユーザーのレイキャストの結果が返されると、このアプリは交点に小さな球面を配置して視覚的フィードバックを提供します。

let resultAnchor = AnchorEntity(world: result.worldTransform)
resultAnchor.addChild(sphere(radius: 0.01, color: .lightGray))
arView.scene.addAnchor(resultAnchor, removeAfter: 3)

ユーザーのレイキャストと交差しているメッシュの面上に配置された仮想の球面のスクリーンショット

実世界のオブジェクトの分類

ARKitが備える分類機能は、リアルワールドのメッシュモデルを分析して、特定の実世界のオブジェクトを識別します。ARKitはメッシュ内部にある床面、テーブル、椅子、窓、天井を識別できます。完全なリストについては、ARMeshClassification(英語)を参照してください。

ユーザーが画面をタップし、レイキャストがメッシュ化された実世界のオブジェクトと交差すると、メッシュの分類結果を示すテキストが表示されます。

メッシュ化された現実世界の花びんとその分類を表示するラベルの図

ARView(英語)automaticallyConfigureSession(英語)プロパティがtrueに設定されている場合、RealityKitはデフォルトで分類を無効化します。これは、分類がオクルージョンや物理演算で不要なためです。メッシュ分類を有効にするために、サンプルではsceneReconstruction(英語)プロパティをmeshWithClassification(英語)に設定してデフォルト値をオーバーライドします。

arView.automaticallyConfigureSession = false
let configuration = ARWorldTrackingConfiguration()
configuration.sceneReconstruction = .meshWithClassification

このアプリはメッシュとの交点部分の分類結果を取得しようとします。

nearbyFaceWithClassification(to: result.worldTransform.position) { (centerOfFace, classification) in
    // ...

メッシュの3つの頂点ごとに、フェイスと呼ばれる三角形が形成されます。ARKitはフェイスごとに分類結果を割り当てるため、サンプルではメッシュ内を検索して交点近くのフェイスを見つけます。そのフェイスに分類結果が存在する場合、このアプリは画面上にその分類結果を表示します。このルーチンでは広範囲にわたる処理が必要になるため、サンプルの処理は、レンダラが停止状態にならないように非同期で実行されます。

DispatchQueue.global().async {
    for anchor in meshAnchors {
        for index in 0..<anchor.geometry.faces.count {
            // Get the center of the face so that we can compare it to the given location.
            let geometricCenterOfFace = anchor.geometry.centerOf(faceWithIndex: index)
            
            // Convert the face's center to world coordinates.
            var centerLocalTransform = matrix_identity_float4x4
            centerLocalTransform.columns.3 = SIMD4<Float>(geometricCenterOfFace.0, geometricCenterOfFace.1, geometricCenterOfFace.2, 1)
            let centerWorldPosition = (anchor.transform * centerLocalTransform).position
             
            // We're interested in a classification that is sufficiently close to the given location––within 5 cm.
            let distanceToFace = distance(centerWorldPosition, location)
            if distanceToFace <= 0.05 {
                // Get the semantic classification of the face and finish the search.
                let classification: ARMeshClassification = anchor.geometry.classificationOf(faceWithIndex: index)
                completionBlock(centerWorldPosition, classification)
                return
            }
        }
    }

分類結果を入手したら、サンプルではその分類結果を表示する3Dテキストを作成します。

let textEntity = self.model(for: classification)

メッシュによってテキストが部分的に隠れないように、サンプルではテキストの位置をわずかにオフセットして、読みやすさを確保しています。光線の負方向にオフセットを計算することで、テキストをカメラ方向(面から遠ざかる方向)に少しだけ効果的に移動させます。

let rayDirection = normalize(result.worldTransform.position - self.arView.cameraTransform.translation)
let textPositionInWorldCoordinates = result.worldTransform.position - (rayDirection * 0.1)

テキストが画面上に常に同じサイズで表示されるように、カメラからのテキストの距離に基づいて縮尺を適用します。

let raycastDistance = distance(result.worldTransform.position, self.arView.cameraTransform.translation)
textEntity.scale = .one * raycastDistance

テキストを表示するために、調整済みの交点にあるアンカー付きのエンティティ内にテキストを配置します(向きはカメラ方向)。

var resultWithCameraOrientation = self.arView.cameraTransform
resultWithCameraOrientation.translation = textPositionInWorldCoordinates
let textAnchor = AnchorEntity(world: resultWithCameraOrientation.matrix)
textAnchor.addChild(textEntity)
self.arView.scene.addAnchor(textAnchor, removeAfter: 3)

分類結果の取得元であるフェイスの頂点の位置を視覚化するために、サンプルでは現実世界の頂点の位置に小さな球面を作成します。

if let centerOfFace = centerOfFace {
    let faceAnchor = AnchorEntity(world: centerOfFace)
    faceAnchor.addChild(self.sphere(radius: 0.01, color: classification.color))
    self.arView.scene.addAnchor(faceAnchor, removeAfter: 3)
}

分類済みのメッシュの原点を識別する仮想の球面のスクリーンショット

メッシュによる仮想コンテンツのオクルージョン

オクルージョンは、リアルワールドに存在する物体が、カメラの視点からアプリの仮想コンテンツの一部分を覆い隠す機能です。この錯覚を実現するために、RealityKitは、ユーザーに表示される仮想コンテンツの手前にあるすべてのメッシュをチェックし、それらのメッシュによって見えなくなっている仮想コンテンツの部分の描画を省略します。サンプルでは、オクルージョンを有効にするためにocclusion(英語)オプションを環境のsceneUnderstanding(英語)プロパティに追加します。

arView.environment.sceneUnderstanding.options.insert(.occlusion)

実行時に、このアプリはメッシュ化されたリアルワールドの物体によって隠された仮想テキストの部分の描画を省略します。

アプリによって配置されたバーチャルオブジェクトを遮蔽する現実世界のテーブルのスクリーンショット

物理演算を利用した実世界のオブジェクトとの相互作用

シーンメッシュでは、仮想コンテンツと実環境の間のリアルな相互作用を実現できます。これは、RealityKitの物理演算エンジンがメッシュを利用してリアルワールドの正確なモデルを作成できるためです。サンプルでは、物理演算を有効にするために、physics(英語)オプションを環境のsceneUnderstanding(英語)プロパティに追加します。

arView.environment.sceneUnderstanding.options.insert(.physics)

仮想コンテンツがメッシュ化された実世界のオブジェクトと接触するのを検出するために、addAnchor(_:,removeAfter:) Scene(英語)のextensionで、Collision Shapesを使ってテキストの形状を定義します。

if model.collision == nil {
    model.generateCollisionShapes(recursive: true)
    model.physicsBody = .init()
}

このアプリは、オブジェクトを分類し、テキストを表示すると、3秒待ってから仮想テキストを落下させます。サンプルでテキストのphysicsBody(英語)mode(英語)PhysicsBodyMode.dynamic(英語)に設定すると、テキストは重力の作用で落下します。

Timer.scheduledTimer(withTimeInterval: seconds, repeats: false) { (timer) in
    model.physicsBody?.mode = .dynamic
}

テキストが落下して、メッシュ化された実世界のオブジェクトと衝突する(床面に着地するなど)ときに相互作用が働きます。

リアルワールドのバスケットボールのゴールから落下している仮想のボールのスクリーンショット

関連項目

面のトラッキングと操作

平面のトラッキングと視覚化(英語)

物理的環境で面を検出し、3D空間でその形と位置を視覚化します。

class ARPlaneAnchor(英語)

物理的環境でARKitが検出する平面です。

class ARMeshAnchor(英語)

再構築されたシーンメッシュの区分。

レイキャストとヒットテスト(英語)

スクリーン上の位置に対応する、現実世界の面上の位置を検索します。