协作 Session 可以很方便地实现多用户之间的AR体验实时共享,但开发者需要自行负责并确保AR场景的完整性,自行负责虚拟物体的创建与销毁。为简化同步操作,RealityKit 内建了同步机制,RealityKit 同步机制基于 Multipeer Connectivity,当设置好 MultipeerConnectivityService 属性之后,RealityKit 会自动在参与者之间同步实体对象(Entity)。
RealityKit 同步服务机制
在 RealityKit 中,组成场景(Scene)的基本元素是实体(Entity),实体由其所挂载的组件(Component)定义外观和行为,RealityKit 所有的实体类都继承自 Entity 基类,如 AnchorEntity、ModelEntity,Entity 基类包含了两个组件:Transtorm 组件和 Synchronization 组件,Transform 组件用于空间定位,而Synchronization 组件用于同步。因此,RealityKit 中所有的实体对象都默认带有 Synchronization 组件,即都可以通过网络进行同步,这也是 RealityKit 同步的技术基础。
虽然 RealityKit 网络数据传输仍然依赖于 Multipeer Connectivity,但相对于协作 Session,在 RealityKit中,开发人员不再需要自行处理数据的发送与接收处理工作,RealityKit 会自动进行相关操作,从而大大简化了开发流程。
使用 RealityKit 的同步服务功能只需要两步操作:(1)使用 Multipeer Connectivity 设置好 MCSession,并生成一个 MultipeerConnectivityService 对象。(2) 将生成的 MultipeerConnectivityService 对象赋给 ARView.scene 的 synchronizationService 属性,场景中所有的实体对象都将继承该值。在进行这两步操作之后,后续的所有同步操作完全由 RealityKit 自动处理,使用 RealtiyKit 同步服务的完整代码如下所示,稍后我们会对代码进行详细解析。
//
// SyncARSession.swift
// ARKitDeamo
//
// Created by zhaoquan du on 2024/2/28.
//
import SwiftUI
import ARKit
import RealityKit
import MultipeerConnectivity
struct SyncARSession: View {
static var arView: ARView!
static var multipeerSession: MultipeerSession?
var body: some View {
SnycARSessionContent()
.onDisappear {
SyncARSession.arView.session.delegate = nil
SyncARSession.arView.session.pause()
SyncARSession.arView = nil
SyncARSession.multipeerSession?.endConnect()
SyncARSession.multipeerSession = nil
print("SyncARSession onDisappear")
}
.edgesIgnoringSafeArea(.all).navigationTitle("ARSession同步")
}
}
struct SnycARSessionContent: UIViewRepresentable {
func makeUIView(context: Context) -> some ARView {
let arView = ARView(frame: .zero)
arView.automaticallyConfigureSession = false
let config = ARWorldTrackingConfiguration()
config.planeDetection = .horizontal
config.isCollaborationEnabled = true
arView.session.run(config,options: [.resetTracking,.removeExistingAnchors])
arView.session.delegate = context.coordinator
SyncARSession.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 arView: ARView? {
return SyncARSession.arView
}
var multipeerSession: MultipeerSession? {
return SyncARSession.multipeerSession
}
var planeEntity : ModelEntity? = nil
var raycastResult : ARRaycastResult?
func createPlane(){
SyncARSession.multipeerSession = MultipeerSession(serviceType: "sync-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)
planeAnchor.synchronization = nil
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)
arView?.scene.synchronizationService = multipeerSession?.syncService
} catch let error {
print("加载文件失败:\(error)")
}
}
func randomColor() -> UIColor{
return UIColor(red:CGFloat(arc4random()%256)/255.0,green:CGFloat(arc4random()%256)/255.0,blue: CGFloat(arc4random()%256)/255.0,alpha: 1.0 )
}
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 box = ModelEntity(mesh: MeshResource.generateBox(size: 0.1), materials: [SimpleMaterial.init(color: randomColor(), isMetallic: false)])
box.position = [0,0.05,0]
let anchorEntity = AnchorEntity(raycastResult: raycastResult)
anchorEntity.addChild(box)
arView?.scene.addAnchor(anchorEntity)
}
//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]) {
}
func receiveData(data:Data,from peer: MCPeerID){
}
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 {
SyncARSession()
}
代码比上一节代码中的代码要清爽很多,一方面是RealityKit 的同步服务机制简化了人工干预,代码详细功能如下:
(1) 初始化 Multipeer Session 类,设置 RealityKit 的同步服务。
(2)进行平面检测,在检测到可用平面时实例化一个指示图标用于指示放置位置。
(3)添加屏幕单击手势,在平面可用时单击屏幕会在指示图标位置放置一个颜色随机的立方体。
在第(1)项功能中,即 createPlane()方法中的代码,除了在检测到平面时创建一个指示图标,还初始化了
MultipeerSession 类,传入的是一个用于区分网络服务的 serviceName,然后使用语句 self.scene. synchronizationService = multipeerSession?.syncService设置 RealityKit 的同步服务功能。
在第(3)项功能中,即 handTap()方法中代码,我们直接在指示图标位置生成了一个颜色随机的立方体。
需要注意的是,这里并没有生成 ARAnchor 对象,因为在 RealityKit 中,所有的 AnchorEntity 类都会自动进行同步。
在两台设备 A和B上同时运行本案例(确保两台设备连接到同一个WiFi网络或者都打开蓝牙),在A设备检测到的平面上单击添加立方体,在A、B 连接顺畅的情况下可以看到B设备也会同步出现该立方体,并且立方体所在物理世界中的位置与A设备中的一致,同理,在B设备检测到的平面上单击添加立方体,A设备也会同步出现该立方体,并且立方体所在物理世界中的位置与B设备中的一致,效果如图所示。
使用 RealityKit 同步服务注意事项
Reality Kit 同步服务让 AR体验实时共享变得前所未有地方便,虚拟元素可以实时地共享到所有参与方,而这主要归功于 Synchronization组件,该组件的主要功能就是通过网络在不同设备间实时同步实体对象,其主要属性有俩个。identifierisOwner:每一个实体对象在网络中的唯一标识符布尔值,用于标识本设备是否拥有该实体对象的所有权。ownershipTransferMode:所有权转移类型,为 SynchronizationComponent. OwnershipTransferMode 枚举值,该枚举共有两个值:autoAccept 为自动授受所有权转移;manual需要使用者进行所有权转移授权。
可以看出,Synchronization 组件可以对实体对象进行非常严格的所有权控制,防止不经授权对其他设备生成的实体对象进行操作。所有权是 Synchronization 组件中重要的概念,每一个创建实体对象的ARSession 拥有对该实体对象的所有权,只有实体对象的所有者才有权修改该实体对象(如修改尺寸、修改材质、旋转、移动等),修改结果尔后会同步到所有参与者。非实体对象所有者可以修改其本机场景中的实体对象,但无法同步到其他参与者,如果需要同步修改结果,可以向实体对象所有者申请授权,得到授权后就可以成为该实体对象的所有者,修改结果可以同步到所有的参与者,如图8–16所示。
RealityKit 这么处理的原因是为了防止未授权用户擅自修改其他参与方场景中的虚拟元素,影响其他人的使用体验,保证共享场景中的虚拟元素放置都符合预期。在图8-16中,假设用户①与用户②已经通过 RealityKit 的同步服务进行了同步,①号立方体由用户①创建,②号立方体由用户②创建,这时用户①与用户②都可以看到这两个立方体,此时用户①可以对①号立方体进行任何修改,修改结果会实时地同步到用户②,用户①也可以对②号立方体进行修改,但修改结果并不会被同步。
如果用户①希望能修改②号立方体并且同步到用户②,那么用户①可以申请所有权,在用户②同意授权后,用户①对②号立方体所作的修改就能够同步到用户②。需要注意的是,这时②号立方体的所有权已经转移到用户①,如果用户②要对②号立方体进行修改操作,用户②也需要向用户①申请授权,即一个实体对象的所有者在同一时刻只有一个。进行实体对象操控授权的典型代码如下所示。
public extension HasSynchronization {
func EntityManipulation() {
if isOwner {
//拥有某个实体的所有权,可以进行处理
} else {
requestOwnership { failure in
if failure == .granted {
//没有某个实体的所有权,进行所有权申请,得到授权后可以进行处理
}
}
}
}
/// Execute the escaping completion if you are the entity owner, once you receive ownership
/// or call result failure if ownership cannot be granted to the caller.
/// - Parameter completion: completion of type Result, success once ownership granted, failure if not granted
func runWithOwnership(
completion: @escaping (Result<HasSynchronization, Error>) -> Void
) {
if self.isOwner {
// If caller is already the owner
completion(.success(self))
} else {
self.requestOwnership { (result) in
if result == .granted {
completion(.success(self))
} else {
completion(
.failure(result == .timedOut ?
MHelperErrors.timedOut :
MHelperErrors.failure
)
)
}
}
}
}
}
在代码中,对实体对象进行操作时,首先检查是否拥有该实体对象的所有权,如果有则进行操作,如果没有则向实体对象所有者申请授权,如果授权申请通过则可以进行相应操作。
实体对象所有者可以设置实体授权模式,RealityKit 支持两种实体授权模式,其值由SynchronizationComponent. OwnershipTransferMode 枚举定义,该枚举共有两个枚举值:autoAccept(自动授权)和 manual(手动授权)。默认授权模式为 autoAccept,即实体对象所有权会自动授权给任何参与者对该实体的所有权申请。设置为 manual 时,当有参与者申请所有权时会触发 SynchronizationEvents.OwnershipRequest 事件,我们需要在该事件的accept()回调方法中对授权进行自定义处理,事件处理可参阅第2章相关内容。
在一些场合下,我们可能不想某些实体对象或者某些操作被共享,这时候可以将该实体对象的同步组件设置为nil,设置为 nil 的实体对象及其子对象将不会被共享。
具体代码地址:GitHub - duzhaoquan/ARkitDemo