协作 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 MultipeerConnectivitystruct CooperationSession: View {static var arView:ARView?static var multipeerSession: MultipeerSession?var body: some View {CooperationSessionContent().onDisappear(perform: {CooperationSession.arView?.session.delegate = nilCooperationSession.arView?.session.pause()CooperationSession.arView = nilCooperationSession.multipeerSession?.endConnect()CooperationSession.multipeerSession = nilprint("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 = trueconfig.planeDetection = .horizontalarView.session.run(config,options: [.resetTracking,.removeExistingAnchors])arView.session.delegate = context.coordinatorCooperationSession.arView = arViewcontext.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? = nilvar 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)}//ARSessionDelegatefunc 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 = resultplaneEntity?.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