37-用 Reality Composer 构建 AR 体验

798 阅读9分钟

ARKit系列文章目录

2019年WWDC的《 Session 609 - Building AR Experiences with Reality Composer 》 主要内容速览:

  • Reality Composer 简介
  • 构建场景
  • 添加行为
  • 使用物理效果
  • 构建应用程序

Reality Composer 简介

Reality Composer 是苹果新推出的 AR 和 3D 内容创作工具,支持 Mac(与 Xcode 集成) 和 iOS 平台。苹果声称他的特点有:

  • 进入 AR 和 3D 的良好开端
  • 布局和视觉化预览
  • 自带内容库
  • 交互简单
  • Xcode集成

用 Reality Composer 创建一个简单的 AR 应用只需要 4 步:构建场景、添加动作、使用物理效果、构建应用程序。

示例 Demo 展示的动画效果如下:启动后就开始旋转,镜头接近就跳动,手指点击跳动,点击播放 usdz 动画,点击播放各种强调动画 还有一个例子,是一个小岛的展示,点击开始后会播放大海的声音,鸟儿开始飞翔。再点击出现的直升机,它也会按照固定路线飞起来。另外,点击各个点的标识,会展示出景点名称和图片 需要说明的是,整个场景中,只有小岛和直升机是导入的外部素材,其他都是用自带素材和动作生成的。 比如,这一排小鸟,其实只是用几个三角形拼接起来的,再加上启动翅膀的动画,和环绕岛飞行的动画就可以了。因为它们实在太小了,而且创建起来相当麻烦。 下面展示的是,直升机出现的动画

构建场景

先说一下什么是场景(Scene)

场景剖析

一个场景文件内部,实际包含了锚点、物体、行为、物理世界等

锚定场景

创建时要选择一种锚点类型。如果是在桌面上进行的游戏,就用Horizontal类型。如果是在墙面上玩的游戏,就用Vertical类型。如果是图片锚点,就用Image类型。如果是人脸面部增强效果,就用Face类型。

创建场景

首先,点击**+**号按钮,添加一个球体 然后长按复制出三个,拖动到图中位置,并缩放改变大小

然后添加文字和箭头。先添加一个文字:Autumn 再添加箭头,你会注意到:随着镜头位置的移动,箭头周围的圆环也会改变,始终保持与镜头垂直的方向 接下来,双击锁定操纵圆环的位置,然后点击左上方,倒数第 2 个按钮,使用吸附对齐功能,将箭头与太阳和文字对齐。再点击左上方最后一个按钮,编辑物体,改变箭头的形状和比例 最后,替换太阳和地球,并将月球设置成灰色。需要说明一下,替换后,原来已经设置的动画和各种属性,也会保留,不用重新设置。 这样,基本的场景就搭建完成了。剩余的部分大家可以自行美化完善。

添加动作

动作

动作有两部分组成:触发器和动作序列

触发器

共有 5 种触发器:

  • Start:场景一启动就触发
  • Tap:用户点击后触发
  • Proximity:摄像机/手机接近后触发
  • Collision:两个物体碰撞后触发
  • Notification:代码通知触发(放在最后与 Xcode集成部分介绍)

动作序列

主要有动作组(如下图 Action 2 与 Action 3),与动作循环

独占性动作序列

所谓独占性动作序列,就是一次只能播放一个的动作序列。
当一个独占性动作序列启动时,已经在播放中的独占性动作序列会被暂停,而非独占性则不受影响。
反之,当一个非独占性动作序列启动时,其它独占性、非独占性动作序列都不受影响。

可见性动作

是指物体出现和消失时的动作

动画动作

有强调动画,旋转动画,环绕动画,usdz 动画

移动动作与面向(Look At)动作

有相对移动与绝对移动。另外 Look At 可以记物体始终朝向摄像机(即手机或其他 AR 设备)

音频动作

音频有三种:

  • Play Sound:从 3D 物体发出的声音,即有近大远小效果,场景回声效果,物体遮挡效果
  • Play Ambient:场景中的环境音,受到场景中物体的影响
  • Play Music:从手机中发出的音乐,完成不受 AR 场景的影响

下面还要给场景添加各种动作,包括太阳自转,地球自转,地球公转,月球绕地球转,箭头的显示与隐藏等

使用物理效果

场景搭建完成后,就需要添加一些物理效果了。 如下图,包括碰撞,重力,等

物理材质

首先要选择的是物理材质,自带支持 6 种物理材质如下:

碰撞

默认情况下,物体是没有碰撞效果的。如果只开启碰撞,则物体可以互相碰撞,但不会受到碰撞的影响。想要真实的物理效果,还需要开启模拟(Simulates)

构建应用程序

真正构建一个 app,需要 Xcode 的协作。共有三种方式:

  • 直接用 Xcode的 AR 或游戏模版生成 app
  • 用 Reality Composer 创建一个新工程
  • 从 Reality Composer 导出 Reality File 然后添加到 app 中 下面我们着重介绍后两种

Reality Composer Project

Reality Composer Project 是:

  • Reality Composer 的项目文件
  • 包含在 ReaityKit AR 和游戏模版中
  • 可以在 Xcode 中预览
  • Xcode 自动输出为 Reality File(在 Xcode 中 build 时自动生成)

Reality文件

Reality File 包含了用于渲染和模拟的所有数据,且对 RealityKit 进行了专门优化。它可以:

  • 从 Reality Composer 中导出
  • 用 Xcode 中自动输出
  • 在应用中被直接引用
  • 在 AR Quick Look 中预览 更多详情可以参考Advances in AR Quick Look

Reality File 结构如下,包含了多个场景(Scene),和内部的 3D 物体模型 而实际上,在 RealityKit 和 Xcode 中,场景就是锚点(Anchor),而 3D 物体就是实体(Entity)

Xcode 中的代码生成

当有了 Relity 文件后,实际上已经可以通过代码和字符串来访问里面的 3D 物体了,但是为了让 Reality 文件更好用,Xcode 还会自动生成 Swift 类文件

那么这个自动生成的 Swift 类文件中包含什么呢?是一些 app 相关的 API,如:

  • 场景(Scene)
  • 已命名的实体(Named entities)命名见下图
  • 通知动作
  • 通知触发器

转换前后的对应关系如下图:

同时苹果也提供两种方法来加载 Reality 文件,首先是同步方法加载 Reality 文件:

let seasonsChapter = try? SolarSystem.loadSeasonsChapter()
 // Use the loaded anchor here


异步加载方法:

SolarSystem.loadSeasonsChapterAsync { result in
 switch result {
 case .success(let anchor):
 // Use loaded anchor here
 case .failure(let error):
 // Handle failure
 }
}

访问实体(Entity):

// Load Reality File anchor, `seasonsChapter`, above...
let sun = seasonsChapter.sun
let earth = seasonsChapter.earth
let moon = seasonsChapter.moon
// Load the “Seasons Chapter” scene from the “SolarSystem” Reality File
let seasonsChapter = try! SolarSystem.loadSeasonsChapter()
// Add the seasons chapter anchor into the scene
arView.scene.anchors.append(seasonsChapter)

以上是将 Reality 文件拖动到 Xcode 中,自动生成相关 Swift 类的加载方法。如果一个 Reality 已经在 Bundle 中,且没有 Swift 文件,或者是从网络下载的,那要怎么处理呢?看下面 同步加载方法:

guard let url = Bundle.main.url(forResource: "SolarSystem", withExtension: "reality") else { return
}
let anchor = try? Entity.loadAnchor(contentsOf: url, withName: "SeasonsChapter") // Use the loaded anchor here...

异步加载:

guard let url = Bundle.main.url(forResource: "SolarSystem", withExtension: "reality") else { return
}
let loadRequest = Entity.loadAnchorAsync(contentsOf: url, withName: "SeasonsChapter") _ = loadRequest.sink(receiveCompletion: { completion in
// Handle completion state
}, receiveValue: { anchor in
 // Use loaded anchor here
})
// Load Reality File anchor above...
let sun = anchor.findEntity(named: "Sun") 
let earth = anchor.findEntity(named: "Earth") 
let moon = anchor.findEntity(named: "Moon")
// Use fetched entities below...

通知动作

前面讲触发器,特地强调了通知动作和通知触发器是用代码来实现的。

  • 通知动作是在 Reality Composer 中预先建立的
  • 会在动作序列中被调用
  • 可以在 app 代码中用闭包来设置
  • 可以在代码中通过名称来访问
seasonsChapter.actions.displayEarthDetails.onAction = { entity in
 // Display details about Fall
}

需要说明的是,通知动作和通知触发器也是包含在生成 Swift 文件中的

通知触发器

通知触发器也是类似,它可以用来自定义触发条件,用代码决定何时触发

  • 通知触发器是在 Reality Composer 中预先建立的
  • 会启动动作序列
  • 可以在 app 代码中发送
  • 可以在代码中通过名称来访问

seasonsChapter.notifications.showGoldStar.post()
// Replaces targets in the action sequence named `originalTarget.name` with `newTarget`
seasonsChapter.notifications.showGoldStar.post(overrides: [originalTarget.name: newTarget])

Demo

此处继续上面的例子,完善加载逻辑

func loadSizeChapter() {
 SolarSystemLesson.loadSizeChapterAsync { (loadedAnchor, error) in
  guard let sizeAnchor = loadedAnchor, error == nil else {
   return
  }
  // 为点击触发器设置碰撞形状
  sizeAnchor.genrateCollisionShapes(recursive: true)
  // 添加锚点到场景中
  self.arView.scene.anchors.append(sizeAnchor)
  self.sizeChapter = sizeAnchor
  self.setupNotifyActions() // 用来更新 UI 描述,暂未实现
 }
}

运行之后发现,点击月球,地球,太阳后,3D 物体上方出现了相应的简介,但 UI 界面最下方的简介并没有跟着变化(因为通知发出后,Action 没有实现) 所以,我们就需要在加载时,用代码设置通知动作,这样就能在收到点击触发器通知时,触发自定义的动作(点击触发器已经在 Reality Composer 中分别设置好了),更新 UI 界面下方的说明文字。

func setupNotifyActions() {
 let allDisplayActions = sizeChapter.actions.allActions.filter({
  $0.identifier.hasPrefix("Display") })
 for displayAction in allDisplayActions {
  displayAction.onAction = { entity in
   self.displayDetails(for: entity)//更新对应的 UI 描述
  }
 }
}

同时,我们还需要用代码,给 UI 层最上面的 segmentedControl 按键添加逻辑,发送通知来触发设置好的触发器。这样当用户切换显示比例时,会发出通知,3D 模型里(swift 文件里也有)的触发器被触发,就会执行相应的缩小和放大动作。

@IBAction func segmentedControlValueChanged(segmentedControl: UISegmentedControl) {
 if segmentedControl.selectedSegmentIndex == 0 {
  // 缩放到同样大小
  sizeChapter.notifications.scaleToSameSize.post()
 } else if  segmentedControl.selectedSegmentIndex == 1 {
  // 缩放到相对大小
  sizeChapter.notifications.scaleToRelativeSizes.post()
 }
}

类似的,我们还要在结束时,发送通知,以触发相应的结束动画,这里我们假设玩家获得了三星级评价,所以需要根据结果用代码控制星星的数量

@IBAction func didSelectLessonCompletedButton() {
 UIView.animate(withDuration:0.5) {
  self.detailsView.alpha = 0
  self.lessonCompletedButton.alpha = 0
 }
 // 发送通知,触发结束动作
 sizeChapter.notifications.chapterCompleted.post()
 
 DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
  self.displayGoldStars() // 产生星星动画效果
 }
}

func displayGoldStars() {
 guard let star = sizeChapter.specialStar else {
  return
 }
 // 展示金色星星
 let notifications = sizeChapter. notifications
 notifications. showGoldStar.post( )

 // 复制一个星星,在右侧展示
 DispatchQueue. main. asyncAfter ( deadline: .now() + 2.5) {
  let rightStar = star.clone (recursive: true)
  rightStar. setPosition( SIMD3<Float>(0.2, 0.01, 0.0),relativeTo: star)
  self. sizeChapter. children. append (rightStar)
  notifications. showGoldStar.post (overrides: [ star. name: rightStar])
 }

 // 复制一个星星,在左侧展示
 DispatchQueue. main. asyncAfter ( deadline: .now() + 5.0) {
  let leftStar = star.clone (recursive: true)
  leftStar. setPosition( SIMD3<Float>(-0.2, -0.01, 0.0),relativeTo: star)
  self. sizeChapter. children. append (leftStar)
  notifications. showGoldStar.post (overrides: [ star. name: leftStar])
 }
}

参考资料

相关视频,WWDC2019