【iOS ARKit】协作 Session 实例

news/2024/7/20 21:19:15 标签: ios

协作 Session 使用注意事项

      协作 Session 是在 ARWorldMap 基础上发展起来的技术,ARWorldMap 包含了一系列的地标、ARAnchor 及在观察这些地标和 ARAnchor 时摄像机的视场(View)。如果用户在某一个位置新创建了一个 ARAnchor,这时这个 ARAnchor位置并不是相对于公共世界坐标系的(实际上此时用户根本就不知道是否还有其他参与者),而是被存储成离这个 ARAnchor 最近视场的相对坐标,这些信息也会一并存入到用户的ARWorldMap 中并被发送到其他用户。

      由于 ARAnchor 是相对于 View 的坐标,而这些 View 会分组存储到 ARWorldMap 中,亦即是说,ARAnchor 与任何设备的世界坐标系都没有关系,不管这些ARAnchor 是被本机设备解析到本机场景中,还是通过网络发送到其他设备而被解析到其他用户的场景中,都不会改变 ARAnchor 与 View 之间的相互关系。因此,即使其他用户使用了不同的世界坐标系,他们也能在相同的真实环境位置中看到这个 ARAnchor。

       从以上原理可以看到,ARAnchor 对共享AR体验起到了非常关键的作用,所以为了更好地共享AR体金,开发人员应当在开发时注意以下几点:

     (1)跟踪 ARAnchor 的更新。在ARKit探索环境时,随着采集的特征点信息越来越多,对环境的理解会越来越精准,ARKit会通过对之前的摄像机视场(View)进行微调来优化与调整地标信息,因此,与某一摄像机视场(View)相关联的 ARAnchor 姿态也会随之发生调整,所以应当保持对 ARAnchor 的跟踪以确保ARAnchor 发生更新时能及时反映到当前用户场景中。

    (2)虚拟物体应靠近 ARAnchor。在 ARAnchor 发生更新时,连接到其上的虚拟物体也会发生更新,离ARAnchor 远的虚拟物体在更新时可能会出现误差而导致偏离真实位置。所以连接到ARAnchor 的虚拟对象应当靠近对应的 ARAnchor 以减少误差带来的影响。

    (3)处理好 ARAnchor 与虚拟物体的关系。独立的虚拟物体应当使用独立的ARAnchor,这样每一个独立虚拟物体都可以尽量靠近 ARAnchor,并且在存储时可以存储到 ARWorldMap 相同分组中。对若干个距离较近并且希望保持相互之间位置关系的虚拟物体应当使用同一个 ARAnchor,因为在 ARAnchor 更新时,这些虚拟物体会得到相同的更新矩阵,从而保持相互间的位置关系不发生任何变化。

    (4)使用协作 Session 必须要将 isCollaborationEnabled设置为true,只有设置为 true, ARKit 才会周期性的调用 session(_:didOutputCollaborationData:)方法,也才能将 Collaboration Data 数据发送给所有参与方。

    (5)为更高效可靠地传输 AR 进程数据,ARKit 对 Collaboration Data 数据进行了优先级区分,由ARSession. CollaborationData. Priority 枚举表示,分为两种类型:Critical(关键)和 Optional(可选)。Critical 数据定期更新,对同步 AR体验非常关键,应当被可靠地发送到所有参与设备;Optional 数据产生频率高,几乎每帧产生,重要性不及 Critical 数据,因此有所丢失也不会有太大影响。标记为 Optional 的数据包括设备位置数据。区分优先级可以允许我们对不同的Collaboration Data 数据采取不同的处理策略,提高同步的性能。

    (6) 在使用协作 Session 时,有时我们需要知道某个 ARAnchor 是不是由本机设备生成,ARAnchor 的创建者属于哪个设备,如在某个场合需要在某个参与者退出后清除所有该参与者创建的虚拟物体。

      在ARKit 中,每个 ARSession 在运行时会都会生成一个 UUID(Universally Unique Identifier,全局唯一 ID)用于唯一标识该 Session,同时,在协作 Session 中,每个 ARParticipantAnchor 也都有一个独立且唯一的 Identifier 值标识该参与者,ARParticipantAnchor 与 ARAnchor 都有一个 sessionldentifier 属性,这个sessionldentifier 值与所在设备的ARSession UUID 值相同。因此,利用这些信息我们就可以判断ARAnchor 的创建者,并依据结果进行后续处理,典型的示例代码所示。

   (7)协作 Session 同步从有参与者参与开始,但地图的真正融合开始于参与者物理特征值的匹配,即参与者探索过的物理环境有重叠的部分,一旦地图融合后,每个参与用户都会获得其他参与者探索过的地图,同时会同步所有ARAnchor,所以为了便于 ARKit 更快地融合地图,参与者应当在相同的物理环境中扫描相同的物理区域。

协作 Session 实例

      在 ARKit 中,使用协作 Session 主要利用 session(_:didOutputCollaborationData:)方法跟踪同步所有ARAnchor,其中通过网络收发 Collaboration Data 需要开发人员自行处理,完整代码如下所示。

//
//  CooperationSession.swift
//  ARKitDeamo
//
//  Created by zhaoquan du on 2024/2/27.
//

import SwiftUI
import ARKit
import RealityKit
import MultipeerConnectivity

struct CooperationSession: View {
    static var arView:ARView?
    static var multipeerSession: MultipeerSession?
    var body: some View {
        CooperationSessionContent()
            .onDisappear(perform: {
                CooperationSession.arView?.session.delegate = nil
                CooperationSession.arView?.session.pause()
                CooperationSession.arView = nil
                CooperationSession.multipeerSession?.endConnect()
                CooperationSession.multipeerSession = nil
                print("CooperationSession onDisappear")
            })
            .edgesIgnoringSafeArea(.all)
            .navigationTitle("协作Session")
    }
}


struct CooperationSessionContent:UIViewRepresentable {
    
    func makeUIView(context: Context) -> some ARView {
        let arView = ARView(frame: .zero)
        let config = ARWorldTrackingConfiguration()
        config.isCollaborationEnabled = true
        config.planeDetection = .horizontal
        
        arView.session.run(config,options: [.resetTracking,.removeExistingAnchors])
        arView.session.delegate = context.coordinator
         
        CooperationSession.arView = arView
        
        context.coordinator.createPlane()
        context.coordinator.addGesture()
        
        return arView
    }
    
    func updateUIView(_ uiView: UIViewType, context: Context) {
        
    }
    
    func makeCoordinator() -> Coordinator {
        Coordinator()
    }
    
    class Coordinator: NSObject, ARSessionDelegate {
        var multipeerSession: MultipeerSession?{
            return CooperationSession.multipeerSession
        }
        var planeEntity : ModelEntity? = nil
        var raycastResult : ARRaycastResult?
        var arView: ARView? {
            return CooperationSession.arView
        }
        
        func createPlane(){
            CooperationSession.multipeerSession = MultipeerSession(serviceType: "cooper-session", receivedDataHandler: receiveData(data:from:), peerJoinedHandler: peerJoined(_:), peerLeftHandler: peerLeft(_:), peerDiscoveredHandler: peerDiscovered(_:))
            let planeMesh = MeshResource.generatePlane(width: 0.15, depth: 0.15)
            let planeMaterial = SimpleMaterial(color:.white,isMetallic: false)
            planeEntity = ModelEntity(mesh: planeMesh, materials: [planeMaterial])
            let planeAnchor = AnchorEntity(plane: .horizontal)
            
            do {
                let planeMesh = MeshResource.generatePlane(width: 0.15, depth: 0.15)
                var planeMaterial = SimpleMaterial(color: SimpleMaterial.Color.red, isMetallic: false)
                planeMaterial.color =  try SimpleMaterial.BaseColor(tint:UIColor.yellow.withAlphaComponent(0.9999), texture: MaterialParameters.Texture(TextureResource.load(named: "AR_Placement_Indicator")))
                planeEntity = ModelEntity(mesh: planeMesh, materials: [planeMaterial])
                
                planeAnchor.addChild(planeEntity!)
                
                arView?.scene.addAnchor(planeAnchor)
            } catch let error {
                print("加载文件失败:\(error)")
            }
        }
        func addGesture(){
            let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
            arView?.addGestureRecognizer(tap)
        }
        @objc func handleTap(_ sender: UITapGestureRecognizer? = nil) {
            guard let raycastResult = raycastResult else {
                print("还未检测到平面")
                return
            }
            let anchor = ARAnchor(name: "objectAnchor", transform: raycastResult.worldTransform)
            arView?.session.add(anchor: anchor)
        }
        
        //ARSessionDelegate
        func session(_ session: ARSession, didUpdate frame: ARFrame) {
            guard let arView = arView,  let result = arView.raycast(from: arView.center, allowing: .estimatedPlane, alignment: .horizontal).first else{
                return
            }
            raycastResult = result
            planeEntity?.setTransformMatrix(result.worldTransform, relativeTo: nil)
        }
        
        func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
            guard let arView = arView else {
                return
            }
            for anchor in anchors {
                if anchor.name == "objectAnchor"{
                   
                    let box = ModelEntity(mesh: MeshResource.generateBox(size: 0.1), materials: [SimpleMaterial.init(color: .green, isMetallic: false)])
                    box.position = [0,0.05,0]
                    let anchorEntity = AnchorEntity(anchor: anchor)
                    anchorEntity.addChild(box)
                    arView.scene.addAnchor(anchorEntity)
                }
            }
        }
        
        func session(_ session: ARSession, didOutputCollaborationData data: ARSession.CollaborationData) {
            guard let multipeerSession = multipeerSession else {
                return
            }
            
            if !multipeerSession.connectedPeers.isEmpty {
                
                do {
                    let encodeData = try NSKeyedArchiver.archivedData(withRootObject: data, requiringSecureCoding: true)
                    
                    multipeerSession.sendToAllPeers(encodeData, reliably: data.priority == .critical)
                } catch  {
                    print("encode data faile")
                }
            }
        }
        
        func receiveData(data:Data,from peer: MCPeerID){
            
           
            if let data = try? NSKeyedUnarchiver.unarchivedObject(ofClass: ARSession.CollaborationData.self, from: data){
                
                if data.priority == .critical {
                    arView?.session.update(with: data)
                    print(" data updated")
                }
                
            }
            
        }
        
        
        func peerDiscovered(_ peer: MCPeerID) -> Bool {
            guard let multipeerSession = multipeerSession else {
                return false
            }
            
            if multipeerSession.connectedPeers.count > 3 {
                return false
            }else{
                return true
            }
            
        }
        
        func peerJoined(_ peer: MCPeerID) {
        }
        func peerLeft(_ peer: MCPeerID) {
        }
        
        
    }
}

#Preview {
    CooperationSession()
}

代码清单8-6 中代码实现的功能如下:

     (1)进行平面检测,在检测到可用平面时实例化一个指示图标用于指示放置位置。

     (2)添加屏幕单击手势,在平面可用时单击屏幕会在指示图标位置放置一个 ARAnchor,注意这个ARAnchor 的名字(name 属性),稍后会详细说明。

    (3)检查所有添加到场景中的 ARAnchor,当ARAnchor 名字(name 属性)为指定值时在 ARAnchor 位置生成一个立方体。

    (4)周期性地向所有参与设备发送本设备的 AR 进程数据(Collaboration Data)。

    (5)接收来自其他设备的Collaboration Data 数据并更新到本设备的 ARSession 中。

      在第(2)项功能中,即 handleTap()方法中的代码,利用命中点的坐标生成了一个 ARAnchor,并将其添加到 ARSession 中,这里 ARAnchor 的name 属性很重要,因为我们后续需要利用该 ARAnchor 的名字来恢复虚拟元素。

     在第(3)项功能中,即 session(:didAdd:)方法中代码,遍历所有添加的 ARAnchor,这里的 ARAnchor既包括本设备的ARAnchor,也包括从其他设备同步过来的 ARAnchor,当 ARAnchor 名字(name 属性)为功能2中指定值时在 ARAnchor 位置生成一个立方体。利用该方法既会生成本设备自身的立方体,也会生成其他设备共享的立方体,即实现了操作同步。

     在第(4)项功能中,即 session(_:didOutputCollaborationData:)方法中代码,首先确保通信可用且有参与者,然后利用 let collaborationData = try? NSKeyedUnarchiver. unarchivedObject (ofClass: ARSession.CollaborationData. self, from: data) 语句获取 Collaboration Data 数据并序列化之,设置数据通信优先级后将数据发送到所有参与者。

     在第(5)项功能中,即 receivedData()方法中代码,利用 let collaborationData = try?NSKeyedUnarchiver.unarchivedObject(ofClass: ARSession. CollaborationData. self, from: data) 语何获取 Collaboration Data数据并反序列化之,然后将其更新到本设备的 ARSession中。

     与 ARWorldMap一样,Collaboration Data 数据也不包含虚拟元素本身,因此,需要人工恢复虚拟元素,因为虚拟元素总是与ARAnchor 关联,利用 ARAnchor 的名字(name)属性我们就可以恢复关联的虚拟元素,通过这种方式,可以逐一地恢复所有的虚拟元素,从而恢复整个场景,达到所有参与方看到完全相同 AR场景的效果,即实现了 AR体验的同步。

      在两台设备A 和B上同时运行本案例(确保两台设备连接到同一个 WiFi网络或者都打开蓝牙),在A设备检测到的平面上单击添加立方体,在AB连接顺畅的情况下可以看到B设备也会同步出现该立方体,并且立方体所在物理世界中的位置与A设备中的一致,反之亦然,在B设备检测到的平面上单击添加立方体,A设备也会同步出现该立方体,并且立方体所在物理世界中的位置与B设备中的一致,效果如上图 所示。

具体代码地址:GitHub - duzhaoquan/ARkitDemo


http://www.niftyadmin.cn/n/5399379.html

相关文章

Linux中df和du命令

当涉及到在Linux系统中管理磁盘空间时,df和du命令是非常有用的工具。除了基本用法外,它们还具有一些高级用法,可以提供更详细和定制化的磁盘信息。下面是Linux中df和du命令的十个常用的高级用法,附带相应的代码和输出。 df -i - 显…

AI推荐算法的演进之路

推荐算法 基于大数据和AI技术,提供全流程一站式推荐平台,协助企业构建个性化推荐应用,提升企业应用的点击率留存率和永久体验。目前,主要的推荐方法包括:基于内容推荐、协同过滤推荐、基于关联规则推荐、基于效用推荐…

02| JVM堆中垃圾回收的大致过程

如果一直在创建对象,堆中年轻代中Eden区会逐渐放满,如果Eden放满,会触发minor GC回收,创建对象的时GC Roots,如果存在于里面的对象,则被视为非垃圾对象,不会被此次gc回收,就会被移入…

如何将字体添加到 ONLYOFFICE 桌面编辑器8.0

作者:VincentYoung 为你写好的文字挑选一款好看的字体然而自带的字体列表却找不到你喜欢的怎么办?这只需要自己手动安装一款字体即可。这里教你在不同的桌面操作系统里的多种字体安装方法。 ONLYOFFICE 桌面编辑器 ONLYOFFICE 桌面编辑器是一款免费的办…

WiFi模块赋能智能手表:拓展功能与提升连接性

随着科技的不断进步,智能手表正逐渐成为现代人生活中不可或缺的智能配饰。其中,WiFi模块的应用为智能手表带来了更多强大的功能和更高的连接性,为用户提供了更为便捷、智能化的使用体验。本文将深入探讨WiFi模块在智能手表中的应用。 远程通信…

忙碌生活下的技术适应力:应对新应用学习带来的困扰与挑战

在当今信息时代,各类应用程序(APP)渗透到我们生活的方方面面,无论是工作效率提升,还是日常生活便利,都离不开它们的支持。然而,对于在快节奏工作中疲于奔命的现代人来说,当因为生活所…

Qt中信号和槽解决了什么问题

Qt 中的信号和槽机制是一种用于处理对象之间通信的重要机制,它解决了以下几个问题: 对象之间的解耦(Decoupling): 问题: 在一个系统中,如果对象之间直接调用彼此的方法,就会形成紧密…

MATLAB练习题:判断整数是否为回文数

​讲解视频:可以在bilibili搜索《MATLAB教程新手入门篇——数学建模清风主讲》。​ MATLAB教程新手入门篇(数学建模清风主讲,适合零基础同学观看)_哔哩哔哩_bilibili 给定一个1到1亿之间的整数,请判断这个整数是否为回…