ARKitを用いてAR空間共有を試してみる
はじめに
こにちは、次世代システム研究室のBMKです。
今回はARKitを用いてAR空間共有を試して書きます。AR空間共有がARKit2から可能になり、従来の共有できないARエクスペリエンスが共有でき、AR空間内で共同作業等の重要な応用の実現ができるようになりました。ARKit上AR空間共有機能のキーワードは「マルチピアコネクティビティ(MultipeerConnectivity)」、「ARワールドマップ(ARWordMap)」及び「リローカライズ(Relocalize)」です。 AR空間共有の流れを入る前にまずこれらの概念から見てみましょう。
マルチピアコネクティビティ(MultiConnectivity)
マルチピアコネクティビティは、近くのデバイスによって提供されるサービスの検出をサポートし、メッセージベースのデータ、ストリーミングデータ、およびリソース(ファイルなど)を介してそれらのデバイスとの通信をサポートするフレームワークです。 iOSでは、基盤となるインフラストラクチャWi-Fiネットワーク、ピアツーピアWi-Fi、およびブルートゥースパーソナルエリアネットワークを使用し、 macOSおよびtvOSでは、インフラストラクチャWi-Fi、ピアツーピアWi-Fi、およびイーサネットを使用します。ARワールドマップ(ARWorldMap)
ARワードマップは、ARKitが実際の空間でユーザーのデバイスのポーズを推定するのに使用するすべての空間内の特徴の情報のスナップショットです。特徴の情報をARフレームのrawFeaturePointsプロパティを経由して確認することができます。//ワールドマップに記録された空間内の特徴のデータ var rawFeaturePoints: ARPointCloud
リローカライズ(Relocalize)
受信デバイスは、マルチピアセッションに参加している別の参加者から送信されたワールドマップの情報を受信した後、そのワールドマップ内に自分の位置と向きを再推定することを指します。開発環境
・Xcode 11.0・iOS 12.4.1
・iPhoneX 2台
・Swift 4
AR空間共有の流れ
1. ピアデバイスに接続する
アプリを起動するときに、MainViewControllerオブジェクトはマルチピアセッション(MultipeerSession)インスタンスを作成します。インスタンスが作成された後、MCNearbyServiceAdvertiserオブジェクトの実行を開始して、近くのデバイスにマルチピアセッションに参加する招待をブロードキャストします。MCNearbyServiceBrowserオブジェクトはインフラストラクチャWi-Fiネットワーク、ピアツーピアWi-Fi、およびブルートゥースパーソナルエリアネットワークを利用して近くのデバイスから提供されているサービスを検知します。session = MCSession(peer: myPeerID, securityIdentity: nil, encryptionPreference: .required) session.delegate = self serviceAdvertiser = MCNearbyServiceAdvertiser(peer: myPeerID, discoveryInfo: nil, serviceType: MultipeerSession.serviceType) serviceAdvertiser.delegate = self serviceAdvertiser.startAdvertisingPeer() serviceBrowser = MCNearbyServiceBrowser(peer: myPeerID, serviceType: MultipeerSession.serviceType) serviceBrowser.delegate = self serviceBrowser.startBrowsingForPeers()マルチピアセッションでは、すべての参加者が同等です。 デバイスをホスト(サーバー又はマスター)とゲスト(クライアント又はスレーブ)の役割に明示的に分離されません。
2. ARセッションを実行してARコンテンツを配置する
アプリの画面を表示した時点でARセッションが開始されます。デフォルトの設定はワールドトラッキングです。ワールドトラッキングはデバイス内のセンサー(慣性センサー、加速センサー等)及びデバイスの周囲にあるオブジェクト、画像などの特徴を利用して、リアルタイムにデバイスの位置と向きを特定するトラッキング技術です。(詳細はこののURLに参考)override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) ... // ARセッションを開始 let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = .horizontal sceneView.session.run(configuration) // 平面アンカーをトラッキングするためにデリゲートを設定 sceneView.session.delegate = self
3. ARワールドマップをキャプチャして送信する
ワールドマップを取得するために、アプリはgetCurrentWorldMap(completionHandler :)関数を呼び出して、実行中のARセッションからワールドマップをキャプチャし、NSKeyedArchiverでデータオブジェクトにシリアル化し、マルチピアセッション内の他のデバイスに送信します。sceneView.session.getCurrentWorldMap { worldMap, error in guard let map = worldMap else { print("Error: \(error!.localizedDescription)"); return } guard let data = try? NSKeyedArchiver.archivedData(withRootObject: map, requiringSecureCoding: true) else { fatalError("can't encode map") } self.multipeerSession.sendToAllPeers(data) }ARKitは、ワールドマップをキャプチャするのに適したタイミングであるかどうか(若しくは環境をより多くマッピングするまで待ったほうがよいかどうか)を示すworldMappingStatus値により判断できます。
worldMappingStatusの主な状態の値は以下の通りです。
・ notAvailable: ARワールドマップがまだ利用できない状態
セッションはカメラから見えるデバイスの周りの現実空間の内部マップ(特徴群)を持っていない状況で、このときに呼び出すと、エラーが発生してしまうので、利用させないような工夫が必要です。
・ mapped: マッピング状況が高品質で取得できる状態
ここから新たに別のデバイスやセッションで読み込みを行うことで、保存したワールドマップのような位置や状態が再度読み込めることができるはずです。
4. 共有されたワールドマップを受信してリローカライズする
デバイスがマルチピアセッションの別の参加者から送信されたデータを受信した時に、アプリはNSKeyedUnarchiverを使用してARワールドマップオブジェクトをデシリアライズし、そのワールドマップのデータを自分のARセッションの初期のワールドマップとして使用して新しいARWorldTrackingConfigurationを作成および実行します。if let worldMap = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARWorldMap.self, from: data) { // 受信したワールドマップを用いて自分のARセッションを開始 let configuration = ARWorldTrackingConfiguration() configuration.planeDetection = .horizontal configuration.initialWorldMap = worldMap sceneView.session.run(configuration, options: [.resetTracking, .removeExistingAnchors]) }
5. ARコンテンツとユーザーアクションを共有する
AR空間共有のアプリ系では、受信デバイスがもらったワールドマップにリローカライズした後すぐに、送信デバイスで配置したすべての仮想コンテンツを受信デバイスに表示させる必要となります。ピア間でARアンカー(ARAnchor)オブジェクトを共有することにより、仮想コンテンツの位置と向きを他のデバイスに伝えることができます。 ユーザーがシーンをタップすると、アプリはHitTestを行い、平面などと交わったところにアンカーを作成してローカルARセッション(ARSession)に追加します。その後、DataオブジェクトにそのARアンカーをシリアル化し、マルチピアセッションの他のデバイスに共有します。// アンカーを作成 let anchor = ARAnchor(name: "duck", transform: hitTestResult.worldTransform) sceneView.session.add(anchor: anchor) // アンカー情報をピアに送信して、同じコンテンツを配置できるようにします guard let data = try? NSKeyedArchiver.archivedData(withRootObject: anchor, requiringSecureCoding: true) else { fatalError("can't encode anchor") } self.multipeerSession.sendToAllPeers(data)
受信デバイスがマルチピアセッションからデータを受信すると、そのデータにアーカイブされたアンカー情報を取り出して自分のARセッションにアンカーを追加します。 if let anchor = try NSKeyedUnarchiver.unarchivedObject(ofClass: ARAnchor.self, from: data) { // アンカーを追加 sceneView.session.add(anchor: anchor) }どの仮想コンテンツをアンカーに紐つけられたのかを判断するために、ARAnchorのnameプロパティをチェックしたら分かります。
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) { if let name = anchor.name, name.hasPrefix("duck") { node.addChildNode(loadDuckModel()) } } } private func loadDuckModel() -> SCNNode { let sceneURL = Bundle.main.url(forResource: "duck", withExtension: "scn", subdirectory: "Assets.scnassets")! let referenceNode = SCNReferenceNode(url: sceneURL)! referenceNode.load() return referenceNode }受信デバイスがもらったワールドマップに対してリアルタイムにリローカライズを行うことにより、再度特定した自分の位置と向きを仮想コンテンツへ反映され、送信デバイスと同じ空間に存在し、送信デバイスと一緒に仮想コンテンツを見ているような感覚を頂けます。