【iOS】AR体験をよりリアルにするために3Dモデルにオクルージョンを適用する方法

2024.03.19

はじめに

これまでiPhoneだけを用いて、オンデバイスで3Dモデルの作成と自分の目の前に作成した3Dモデルの配置をチャレンジしてきました。

詳細が気になる方はこれまでの記事を見ていただければと思います。

しかし、実際に3Dモデルを配置してみると、現実世界にあるオブジェクトの後ろに配置したはずなのに、その物体より前に表示されているように見えたり、手を差し出すと手より手前にあるように表示されていました

今回はAR体験が現実世界とより一体感を持ち、よりリアルな印象を与えるため、オクルージョンの適用方法について調査しました。

環境

  • Xcode 15.2
  • iOS 17.3.1
  • iPhone 15 Pro

オクルージョンとは

オクルージョンとは、塞ぐを意味する英単語「occlude」で、IT業界では手前にある物体が後ろにある物体を隠す状態のことをいいます。

ARコンテンツにおいてオクルージョン技術が効いていることで、空間認識によって現実世界の物体の前後関係を把握し、物体の場所によってコンテンツの一部を隠したり、表示させたりすることができます。そのためARコンテンツの前を人が通り過ぎるということが可能になり、ARで表現されたものをより本物のように感じさせることができます。

引用: AR用語集 - オクルージョン

現状の体験

画面タップで3Dモデルを配置するARView

protocol ARPlacementViewDelegate: AnyObject {
    func arPlacementView(_ arPlacementView: ARPlacementView, didTapAt position: SIMD3<Float>)
}

class ARPlacementView: ARView {

    required init(frame: CGRect) {
        super.init(frame: frame)
        addTapGesture()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    weak var delegate: ARPlacementViewDelegate?

    func addTapGesture() {
        let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        self.addGestureRecognizer(tapGesture)
    }

    @objc func handleTap(_ recognizer: UITapGestureRecognizer) {
        // タップしたロケーションを取得
        let tapLocation = recognizer.location(in: self)

        // タップした位置に対応する3D空間上の平面とのレイキャスト結果を取得
        let raycastResults = raycast(from: tapLocation, allowing: .estimatedPlane, alignment: .horizontal)

        guard let firstResult = raycastResults.first else { return }
        // タップした位置をワールド座標系に変換
        let position = simd_make_float3(firstResult.worldTransform.columns.3)
        delegate?.arPlacementView(self, didTapAt: position)
    }
}

ARViewContainer

SwiftUI上でARViewを表示する為のラッパーです。

import SwiftUI
import ARKit
import RealityKit

struct ARViewContainer: UIViewRepresentable {

    @Environment(AppDataModel.self) var model

    func makeUIView(context: Context) -> ARView {
        let arView = ARPlacementView(frame: .zero)
        arView.delegate = context.coordinator

        let configuration = ARWorldTrackingConfiguration()
        configuration.isLightEstimationEnabled = true

        arView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
        return arView
    }

    func updateUIView(_ uiView: ARView, context: Context) {
    }

    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }

    class Coordinator: NSObject, ARPlacementViewDelegate {

        let parent: ARViewContainer

        init(_ parent: ARViewContainer) {
            self.parent = parent
        }

        // ARViewをタップ時に現在選択中の3DモデルをAR空間上に配置
        @MainActor
        func arPlacementView(_ arPlacementView: ARPlacementView, didTapAt position: SIMD3<Float>) {

            guard let path = parent.model.selectingModelPathToPlace,
                  let model = try? ModelEntity.loadModel(contentsOf: path,
                                                         withName: String(describing: Date.now))
            else { return }

            let anchor = AnchorEntity(world: position)
            anchor.children.append(model)
            arPlacementView.scene.anchors.append(anchor)
        }
    }
}

現状の実装だと配置した3Dモデルが人の手や手前に配置しているオブジェクトとの前後関係が無視された状態になっています。

3Dモデルに人物との前後関係を考慮させる

3Dモデルに人物との前後関係を考慮させる為には、People Occlusion機能を使用します。

実装はとても簡単でARConfigurationframeSemanticsプロパティに.personSegmentationWithDepthを指定するだけです。

configuration.frameSemantics = [.personSegmentationWithDepth]

personSegmentationでもPeople Occlusionは実現できますが、personSegmentationでは深度を考慮せずに描画されるため、今回はよりリアルな印象を与えるのが目的なので.personSegmentationWithDepthを指定しています。

func makeUIView(context: Context) -> ARView {
    let arView = ARPlacementView(frame: .zero)
    arView.delegate = context.coordinator

    let configuration = ARWorldTrackingConfiguration()
    configuration.isLightEstimationEnabled = true
    // People Occlusionを適用
    configuration.frameSemantics = [.personSegmentationWithDepth]

    arView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    return arView
}

これで人の手をかざすと3Dモデルが見えなくなり、手を退けると3Dモデルが表示されるようになりました。

3Dモデルに現実世界のオブジェクトとの前後関係を考慮させる

現実世界のオブジェクトをAR空間上で認識できるようにメッシュ化を行います。ARConfigurationsceneReconstructionプロパティに.meshWithClassificationもしくは.meshを指定することでメッシュ化により物理環境のおおよその形状を検出できるようになります。

.meshでは、メッシュ化だけですが、.meshWithClassificationを指定すると、その現実世界のオブジェクトが何に該当するのか分類してくれます。

configuration.sceneReconstruction = .meshWithClassification

次にARView.Environment.SceneUnderstanding.OptionsにARシーンで利用する設定を加えることができます。そこで.occlusionを加えると現実世界のオブジェクトとのオクルージョンを適用できるようになります。

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

下記のように実装することで現実世界のオブジェクトとの前後関係も考慮できるようになりました。

func makeUIView(context: Context) -> ARView {
    let arView = ARPlacementView(frame: .zero)
    arView.delegate = context.coordinator

    let configuration = ARWorldTrackingConfiguration()
    configuration.isLightEstimationEnabled = true
    // People Occlusionを適用
    configuration.frameSemantics = [.personSegmentationWithDepth]
    // メッシュ化とオブジェクトの分類
    configuration.sceneReconstruction = .meshWithClassification
    // シーンの設定にオクルージョンを適用
    arView.environment.sceneUnderstanding.options.insert(.occlusion)

    arView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors])
    return arView
}

おわりに

オクルージョンを適用して、3Dモデルと現実世界のオブジェクトや人物との前後関係を考慮することでより良いAR体験になったように感じました。

iPhoneを活用したより良いAR体験をできるようになるため、引き続き調査をしていきたいと思います。

参考