2019年WWDC的《 Session 610 - Building Collaborative AR Experiences 》 主要内容速览:
- 多人协作会话(Collaborative Session)
- 多用户 AR 的 ARAnchor 最佳实践
- SwiftStrike : 一个新的多人 AR 游戏
多人协作会话(Collaborative Session)
在介绍这个之前,先来回顾一下去年 ARKit 2 中推出的世界地图保存和加载。去年的 ARKit 2 需要一个主用户扫瞄建立世界地图,然后自己通过代码经过网络分享给其他用户。在此之后不管是主用户 User1 还是其他用户 User2 ,所有人新扫瞄到的特征点,新建立的世界地图部分,都只能在自己本地使用,无法共享出去。
而今年的 Collaborative Session 就是为了解决这些问题。它可以自动建立网络连接,自动管理其他用户的加入,自动共享新特征点,新地图扩展。
- 实时多用户 AR 体验
- 持续共享 ARAnchors 和地图数据
- 无需区分主用户与其他用户
比如下面的 Demo,一开始左边和右边的两个不同用户,看到了不同场景,各自添加虚拟物体后,也只能看到自己添加的物体。 互相移动后,看到对方的场景, Collaborative Session 会自动同步世界地图和锚点信息,以及别人添加的 3D 虚拟物体。
这样两个用户的世界地图就合并成了同一个,两个用户也处于同一个网络中,后续再进行任何添加操作,对方也能自动看到。
这其中的原理,如下图所示:一开始,两个用户建立了各自的世界坐标和世界地图,并通过网络进行了交流,没有发现共同之处。当一个用户扫瞄到另一个用户扫瞄过的地方时,世界地图的内容就自动进行了合并。但是,每个用户自己地图的坐标原点仍然在原处,没有改变,只是增加了地图范围和其他用户的锚点信息,并持续更新
在 ARKit 3 中,想要用上 Collaborative Session ,首先,你必须让这些设备处于同一个网络层中(MultipeerConnectivity 或者其他网络技术)。然后你需要启用自己的 Collaboration,并传输相关数据到其他用户那里
在 ARKit 3中,只需要下面的代码就可以了。
// Collaborative Session
// Create world tracking configuration
let config = ARWorldTrackingConfiguration()
// Enable collaborative session
config.isCollaborationEnabled = true
// Run the configuration
session.run(config)
如果你用的不是 RealityKit,那还需要实现另外两个方法:
// ARSession delegate function to output ARCollaborationData
func session(_ session: ARSession,
didOutputCollaborationData data: ARSession.CollaborationData) {
// Transmit Data representation of the data to all other participants using MPC
do {
try self.mpcSession.send(data.data, toPeers: self.peerIds, with: .reliable)
} catch {
// Re-transmit the data if failed
}
}
// MPC delegate function when receiving collaboration data from other users
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
// Pass the received data to ARSession
self.arSession.update(data: ARSession.CollaborationData(data: data))
}
Collaborative Session 中的 ARAnchor
ARAnchor 具有:
- 同步的生命周期:当你添加或删除锚点时,其他用户也会同步添加或删除
- 带有会话标识(Session identifier):用来区分是谁创建了锚点
- ARAnchor 的子类不会被共享:只共享用户创建的锚点,不包括 ARImageAnchor 等,也不包括用户自己实现的子类,比如用来携带某些数据等
那么在代码中,如何使用 ARAnchor?我们需要区分是谁在操作锚点:
// ARAnchor in collaborative session
// ARSession delegate function when an ARAnchor is added.
func session(_ session: ARSession, didAdd anchors: [ARAnchor]) {
for anchor in anchors {
// Use session identifier to determine creator of the ARAnchor
if anchor.sessionIdentifier == session.identifier {
// Self-placed ARanchor
} else {
// ARAnchor from another participant
}
}
}
// ARSession delegate function when an ARAnchor is removed.
func session(_ session: ARSession, didRemove anchors: [ARAnchor]) {
for anchor in anchors {
// Use session identifier to determine creator of the ARAnchor
if anchor.sessionIdentifier == session.identifier {
// Self-placed ARanchor
} else {
// ARAnchor from another participant
}
}
}
ARParticipantAnchor
为了解决在多人 AR 中,如何显示其他用户的问题,我们今年引入了ARParticipantAnchor类型:
- 代表其他用户的位置
- 以高帧率更新
- 在本地化其他用户的地图后,才会创建(这意味着,我们可以用它来确定多人共享是否已经开始了)
关于本地化其他用户,我们有一些建议
多人共享 AR 能够开启的前提,是大家看到了同一块区域。并且,如果你想要加快识别速度,最好是让多人处于相近的视角中。
还有就是,保持地图追踪状态
ARFrame.WorldMappingStatus.mapped
。它会让追踪到的 3D 地图数据不断更新,范围更大,准确度也更高。以便后面的其他用户匹配到同一个区域。
多用户 AR 的 ARAnchor 最佳实践
首先我们需要先了解一下 ARWorldMap:
- 包含了压缩后的 3D 地形(Landmark)
- 包含了相机姿态(特征点和地形被观察到时的相机姿态)
地形数据是根据相机姿态,分成不同组的,如下图 View 0,View 1,View 2 等
同时还要注意的是,ARAnchor 的绝对位置是依赖于世界坐标系的,即相对图中 World 原点的。同时这个位置又是相对世界地图的,即相对于整个地形中特征点的相对位置。
了解了这些原理后,我们就可以采取措施,优化 ARAnchor 的使用:
- 实现 anchor 更新的代理方法:这样随着相机的移动,地形特征点也会发生校准移动,Anchor 的位置也会随着离它最近的 View 里面的地形数据而调整
- 将 3D 内容放置靠近在 ARAnchor 的地方:如果离得太远,当 Anchor 随着地形数据更新时,3D 内容的位置会发生很大的移动
- 对于每个独立的 3D内容,使用独立的不同的锚点:除非几个 3D 物体本身离得不远,且需要保持严格的相对距离,这样才能共用一个锚点
SwiftStrike : 一个新的多人 AR 游戏
这是今年的一个新的多人 AR 游戏,是受到去年的 SwiftShot 多人游戏启发而形成的。
下面主要讲解一下,RealityKit 中的网络、物理效果、游戏性设计等方面。
RealityKit 网络
RealityKit 中的网络特点:
- 基于实体-组件架构
- 所有数据同步,包括物理效果!
- 自定义逻辑组件
- 使用 MultipeerConnectivity 作为网络层
- 创建一个网络会话,移交给 ARView 详情参考Building Apps with RealityKit
不同角色
虽然从网络角度,所有接入的用户都是一样的。但在 SwiftStrike 游戏中,我们需要将第一个设备作为“host”,用它来控制游戏状态,物理模拟等。其它设备都是参与者。
在 RealityKit 中自定义组件
定义你自己的组件来储存应用程序的状态
在初始化 ARView 之前就注册组件
实现Codable
以启用同步功能
用例--启动游戏
匹配物体追踪,是否有足够玩家准备开始游戏 状态是保存在 host 上,同步到 client 上 组件维护了全部日志
如下图,当两个玩家进入指定位置后,游戏才会触发开始,球体才会出现。
代码如下:
// Custom component for game start
struct MatchStateComponent: Component, Codable {
struct Transition: Codable {
var date: Date
var state: MatchOutput
}
var transitions = [Transition]()
}
// Registering the component, in application(_:didFinishLaunchingWithOptions:)
MatchStateComponent.self.registerComponent()
// On client(这里用到新的 Combine 框架)
class MatchObserver {
var matchOutputEvents: AnyPublisher<MatchOutput, Never>
}
我们这个项目和去年的 SwiftShot 相比非常类似,因此也从去年的项目中借用了大量代码。但是如下图所示,因为 RealityKit 能完成组件状态和物理效果的自动同步,因此大量的网络同步类代码就不需要了;同时,因为物理效果自动同步,游戏状态和信息也只在游戏开始时同步一次,这样一来所有的 BitStream 编码相关代码也就不需要了。
最终,我们通过使用 RealityKit 减少了 15000 行代码。
物理模拟
物理状态同步是由 RealityKit 处理的。通过组件来配置物理属性:
- 刚体形体(Rigid body)
- 碰撞掩码
- 质量,摩擦,回弹
在 SwiftStrike 游戏中,Host 设备拥有着 simulation 的所有权。Client 设备则根据更新结果进行插值显示。
比如,游戏中保龄球瓶的设计,外观非常复杂,有很多曲线和曲面连接,下面是模型的线框图
但是,在物理引擎中进行物理效果模拟时,这样的形状就太复杂了,严重消耗了物理引擎的计算能力。所以,我们用内置的基础几何形状进行组合来形成物体的物理形状,如下图,使用球体,圆锥体等组合
这样,外观看上去是精美的球瓶,在处理碰撞等效果时,又不会有复杂运算。
游戏设计
SwiftStrike 的游戏设计体现在:
- 人体遮挡预先设计
- 可现场体验
- 控制机制
当了解了今年 ARKit 3 中的人体遮挡后,我们在设计游戏时,就想到了充分利用这些特性,来做一个完整的游戏。
游戏从一开场,就启用了人体遮挡技术,如下图:
人体遮挡技术的运用,也让游戏不再像以前一样,需要小心避免有人出现在手机和游戏场景之间,去年的 SwiftShot 就是因此只能在桌子上玩。今年的 SwiftStrike 则可以直接在地面上玩,它是全尺寸的游戏。
下面图中,就是我们的进行游戏的场景,木质纹理地板有助于 AR 平面的识别,地面上的贴纸则是图片锚点,用来标定球出现的位置。
下面讲控制机制: 手机就是你的控制器,更快的移动意味着更大的推力。场景中的物理形体设定:
- 球会弹开人体
- 球瓶和球互弹
- 球瓶不会和人体碰撞
要完成这些功能,我们需要给游戏玩家设置一个不可见的圆柱体,让这个圆柱体能够和球、球瓶互相碰撞,但圆柱体之间不会互相碰撞。
但是,还有一个问题:如何处理其他玩家的输入呢?也就是说,物理模拟是在 Host 用户手机上进行的,那么当其他玩家与球发生碰撞时, Host 该如何获取并处理这些事件呢?
这就是下面要说的玩家操控杆的所有权问题。
玩家操控杆的所有权(Ownership of Player Paddle)
其中的原理如下图所示:当 Host 开启游戏时,会创建一个AnchorEntity
,并拥有对此的所有权,AR 中的所有实体都会是它的子元素。而当另外的玩家 Client加入游戏时,会创建一个PlayerLocationEntity
并拥有所有权,这个 entity 会根据玩家 Client 的位置自动更新。
我们还需要创建另一个 Client 的子元素:PaddleEntity
。它的父实体就是玩家 Client,但它的所有权却是归 Host 所有的。
这样,就完美解决所有的冲突。尽管物理效果是在 Host 上处理的,但是每个人都能看到正确的游戏显示
最终完成的游戏效果如下:
黑暗模式
今年的 iOS 引入了黑暗模式,我们在制作游戏时,也考虑到了充分利用这些特性。给游戏也加入了黑暗模式,效果如下:
SwiftStrike 这个游戏代码的下载链接叫Creating a Game with Reality Composer,本文末尾已给出
参考资料
相关视频,WWDC2019