AR场景实践(2 ) - RealityKit探索

前言

2019年苹果推出了一个新的ARKit版本(ARKit3)和专门为AR场景研发的RealityKit。从WWDC视频和官网介绍上可以看出新的ARKit3 + RealityKit组合带来了很多新的应用场景以及显著提高了开发体验和使用体验。

所有这些功能基本上都需要最新系统(iOS13)和较新的设备(A12芯片以上),另外,RealityKit需要Swift开发,对于已转Swift开发的同学非常友好。

Reality是全新的框架,为AR量身定制,能够提供逼真的图像渲染、相机特效、动画、物理特效等等。借助原生ARKit整合提供物理的超逼真渲染、变换和骨骼动画、空间音频和刚体物理等。再结合Reality Composer工具,就能构建出一个体验非常良好的AR APP。下面我们就是用ARKit3 + RealityKit框架,结合Reality Composer工具,开发一个简单的AR应用。

Reality Composer

Reality Composer在MAC、iPhone、iPad三个平台都可以使用,同时可以进行无缝切换。它内建AR资源库,也可以自己导入一些新的第三方资源。常规的动画或者物理特性已经完整内嵌在工具里,只需选择想使用的动画或者动作,比如旋转或者摆动等等,还可以以导出为轻量级 AR Quick Look 体验,以便用户放置和预览内容。所以,不管你是开发者还是产品还是设计师都可以无门槛的创造属于你自己的AR场景。

如上图(图1 - 3)所以,我们可以选择一个Reality Composer内建的AR模型没入场景,这里我们可以看到,Reality Composer内建的模型是非常多的,开发者尝试AR开发时,再也不用为寻找模型发愁了。模型搭建非常简单,模型拖入场景,就算完成了。

导出usdz文件,这就是xcode需要的文件,我们将该文件导入xcode工程,此时,我们就可以进行coding了。不过在开始coding前,我们需要先掌握最基本的框架知识,RealityKit。

RealityKit

介绍一下要实现一个最基本的AR场景,需要掌握的Api。

ARView

ARView是能够使用 RealityKit 显示 AR 体验的视图,

Entity

RealityKit 中所有的虚拟对象都继承自 Entity ,RealityKit 定义了 Entity 的一些具体子类,它们提供常用的功能。

  • Entity - RealityKit 场景的一个元素,可以将提供实体外观和行为特征的组件附加到该元素。
  • Anchor entity - 将实体束缚到场景的锚点。
  • Model entity - RealityKit渲染的物理对象
BodyTrackedEntity

BodyTrackedEntity和ModelEntity一样,都是继承Entity,这里为什么会把BodyTrackedEntity单独拎出来讲,BodyTrackedEntity相对比较特殊,ModelEntity用来渲染物理对象,而BodyTrackedEntity是用于通过跟踪真人来为 AR 场景中的虚拟角色设置动画的实体:

  • 它代表了一个人体
  • 包含骨骼和位置
  • 每帧都会更新
  • 将骨骼系统应用到.usdz 模型上

这提到可以将骨骼系统应用到模型上,那怎么应用呢?

看上图(图7),3D骨骼关节,包含的关节点共有 91 个,只要你的模型中包含这些场景名称,RealityKit 就会自动应用,再建立模型,RealityKit 就会自动驱动模型。

ARBodyAnchor

ARBodyAnchor就是在后置摄像头中跟踪人体位置和运动的锚点,它包含了自身的 Transform 和内部的骨骼系统Skeleton 。整个骨骼系统中髋关节Hip joint 就是根节点Root,如下图(图8)所示:

每一个节点包含内部的父子关系及关节名称Joint Name。其中绿色节点是被追踪出来的,而黄色节点,如手指,则是固定在离它最近的绿色节点上,跟随运动的。如下图(图9)箭头所示,

如果要获取右手处的位置(图10),有两种方法: 相对于父节点的 localTransform 和相对于根节点的 modleTransform

Swift 复制代码
func localTransform(for jointName: ARSkeleton.JointName) -> simd_float4x4?
Swift 复制代码
func modelTransform(for jointName: ARSkeleton.JointName) -> simd_float4x4?

ModelEntity实现简单AR场景

我们通过RealityKit实现一个AR场景时,加载一个AR模型,通常首先创建 AnchorEntity 的实例来锚定我们的内容,并将锚点添加到场景的锚点集合中。 然后,实例化一个 ModelEntity 来表示场景中的物理对象,并将其作为子实体添加到锚点。 这样就完成了实体的实例化,具体的代码量不多,如下:

Swift 复制代码
private func loadModelEntity() {
    do {
        let plane = try ModelEntity.load(named: "character/car")
        
        // 将模型放置在水平面上
        let anchor = AnchorEntity(plane: .horizontal, minimumBounds: [0.15, 0.15])
        arView.scene.anchors.append(anchor) //锚点
        anchor.children.append(car) //场景
    } catch {
        fatalError("加载失败")
    }
}

实例化ARView,用以承载实体场景

Swift 复制代码
private lazy var arView: ARView = {
    let arView = ARView(frame: self.view.bounds)
    return arView
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(arView)
}

至此,一个简单的AR模型实景展示的例子我们已经完成了,我们跑起来看看效果,此时,玩具汽车出现在了我的办公桌上。

ARCoachingOverlayView

这个视图是引导用户去寻找合适的AR场景,整个过程都进行了良好的封装,全部自动识别和判定。引导视图也比较直接明了,是可以直接接入到我们的应用中的,我们先看看引导效果

实现调用的代码不多

Swift 复制代码
    private let guidanceOverlay = ARCoachingOverlayView()
    /// 添加ARCoachingOverlayView,引导用户调整设备姿态
    func setOverlay(automatically: Bool, forDetectionType goal: ARCoachingOverlayView.Goal) {
        //1. 将 GuidanceOverlay 链接到我们当前的会话
        self.guidanceOverlay.session = self.arView.session
        self.guidanceOverlay.delegate = self
        self.arView.addSubview(self.guidanceOverlay)
        
        //2. 设置约束
        NSLayoutConstraint.activate([
          NSLayoutConstraint(item:  guidanceOverlay, attribute: .top, relatedBy: .equal, toItem: self.view, attribute: .top, multiplier: 1, constant: 0),
          NSLayoutConstraint(item:  guidanceOverlay, attribute: .bottom, relatedBy: .equal, toItem: self.view, attribute: .bottom, multiplier: 1, constant: 0),
          NSLayoutConstraint(item:  guidanceOverlay, attribute: .leading, relatedBy: .equal, toItem: self.view, attribute: .leading, multiplier: 1, constant: 0),
          NSLayoutConstraint(item:  guidanceOverlay, attribute: .trailing, relatedBy: .equal, toItem: self.view, attribute: .trailing, multiplier: 1, constant: 0)
          ])
        guidanceOverlay.translatesAutoresizingMaskIntoConstraints = false
        
        //3. 启用叠加层,根据用户偏好自动激活
        self.guidanceOverlay.activatesAutomatically = automatically
        
        //4. 根据用户偏好设置叠加层的用途
        self.guidanceOverlay.goal = goal
    }
    .......
    ///调用setOverlay即可实现引导视图

我们再看看实现引导视图后的使用体验(图14 - 17)

可以看到,ARCoachingOverlayView引导视图的加入,可以让用户更容易的调整设备到锚点的场景,也就更容易展现出我们预设的AR场景内容,所以,这个还是比较推荐使用的。

AR交互场景实践

上面我们已经通过Reality Composer工具和RealityKit框架完成了一个基本的AR模型展示效果,如果我们想进一步实现有交互的AR场景,我们该怎么做?

我们接着来看Reality Composer工具,它可以给我们提供这些能力支持,我们给刚才的小汽车场景,再添加一个同样的小汽车,并且给这个小汽车,添加一个自定义的行为。如下图18所示

接着 我们给小汽车添加一个轻点的触发行为,并且给这个轻点触发,添加一个施力操作,我们选择对向另一个小汽车,调整力度到5公里/时,至于为什么是5公里,这里我试了更大的速度,力度太大,撞飞了,力度太小,效果不明显。我们可以运行下看看实际的碰撞效果,如下图(图20),我们可以看到碰撞行为正常,我们将场景保存为.rcproject,导出到Xcode中,使用RealityKit加载呈现看看是否可以正常呈现效果。

如上图21所示,当我们点击下方小汽车时,它按照我们指定的方向和力度运动,并且撞到了另一个小汽车,当我们继续点击的时候他继续按照指定的方向和力度运动。

至此,我们使用Reality Composer工具制作的AR场景功能已经实现,Reality Composer工具+RealityKit框架实现AR场景非常的轻松和快捷。

AR动画&运动轨迹

上面一节内容我们介绍了如何用Reality Composer实现AR行为交互,本节我们来探索下,如何使用代码定制AR模型行为动画。

首先,我们通过Reality Composer导出带动画的.usdz模型文件,如果Reality Composer没有你心仪的动画模型,也可以通过导入外部模型文件来实现想法,此处我们使用Reality Composer自带的两个模型来实现我们的想法

接下来,我们来实现具体的效果

Swift 复制代码
private func loadBody() {
    do {
        let gushou = try ModelEntity.load(named: "character/gushou")
        
        // 将模型放置在水平面上
        let anchor = AnchorEntity(plane: .horizontal, minimumBounds: [0.15, 0.15])
        arView.scene.anchors.append(anchor) //锚点
        anchor.children.append(gushou) //场景
        
        // 执行动画
        gushou.playAnimation(gushou.availableAnimations[0].repeat(count: 10), transitionDuration: 0.5, startsPaused: false)
    } catch {
        fatalError("加载失败")
    }
}

我们将demo运行,我们看看实际效果(图25),我们可以看到gushou模型开始打鼓。

这就是在实体上播放给定的动画 ,实现的核心就是实体 ModelEntityplayAnimation方法

Swift 复制代码
/// 实体上播放给定的动画  
/// Parameter animation:要播放的动画   
/// Parameter transitionDuration:动画的持续时间
/// Parameter startsPaused:控制动画播放的bool值
/// Return AnimationPlaybackController:用于启动的动画播放控制器, 并停止动画
public func playAnimation(_ animation: AnimationResource, transitionDuration: TimeInterval, startsPaused: Bool) -> AnimationPlaybackController

另外,还有一个playAnimation方法,是用来使用指定选项来播放动画, 相比上面方面,这个方案增加了几个选项参数

Swift 复制代码
/// 使用指定选项来播放动画 
/// Parameter animation:要播放的动画   
/// Parameter transitionDuration:动画的持续时间
/// Parameter blendLayerOffset:当播放多个动画时应用动画,指定混合层的顺序
/// Parameter separateAnimatedValue:动画完成时动画进度是否重置
/// Parameter clock:使用自定义时间刻度驱动动画
/// Return AnimationPlaybackController:用于启动的动画播放控制器, 并停止动画
public func playAnimation(_ animation: AnimationResource, transitionDuration: TimeInterval = 0, blendLayerOffset: Int = 0, separateAnimatedValue: Bool = false, startsPaused: Bool = false, clock: CMClockOrTimebase? = nil) -> AnimationPlaybackController

那我们用上面所列的动画方法来实现模型AutoRobot按轨迹移动的动画,模型加载的代码可以参考上面的案例,此处我们直接来看动画实现部分的代码

Swift 复制代码
// 执行位移动画
robot.move(to: Transform(translation: [0,0,20]), relativeTo: robot, duration: 4, timingFunction: .easeInOut)

我们可以看到robot向着新的位置移动,并在到达给定的位置后不再移动,按照上面的动画方法,我们还可以给模型设置旋转,那么我们可以给他设置先左90度的旋转,那么这样的位移+方向旋转,持续下来,会让 AutoRobot走出一个圈,这就好玩多了,我们来实现试试,我们现将动画分解:

  • AutoRobot 朝指定的位置移动
  • AutoRobot停止位移,此时对AutoRobot进行旋转动画
  • 向左旋转90度后,停止旋转,此时对AutoRobot进行位移动画
  • 以上的无论旋转,还是位移,其实都是重复的,我们最关键的就是要get动画完成的时机

这些我们可以通过监听scene来实现

Swift 复制代码
// 订阅动画播放完成事件
animationEnd = arView.scene.subscribe(to: AnimationEvents.PlaybackCompleted.self, { [self] event in
    moveFoward.toggle()
    if moveFoward {
        // 执行位移动画
        robot.move(to: Transform(translation: [0,0,20]), relativeTo: robot, duration: 4, timingFunction: .easeInOut)
    } else {
        // 执行旋转动画
        robot.move(to: Transform(rotation: simd_quatf(angle: 90 * .pi / 180, axis: [0,1,0]) ), relativeTo: robot, duration: 3, timingFunction: .easeInOut)
    }
})

至此,对于模型的位移、旋转、重复动画,我们先介绍到这里,至于更加复杂的动画,其实是可以通过组合以上三类动画来实现的。

AR追踪人体运动

上面章节我们已经介绍了ARBodyAnchor和BodyTrackedEntity,这是实现追踪人体的关键类。ARBodyAnchor是 ARAnchor 的子类,负责跟踪单个人的运动。关联的配置配置就是ARbodyTrackingConfiguration

当 ARKit 识别出后置摄像头画面中的人物时,它会使用 ARBodyAnchor 调用委托的 session(_:didAdd:) 函数。 身体锚点的变换位置定义了身体髋关节的位置。

原理和相关的类我们已经搞清楚了,现在我们来实现,首先我们初始化相关的类

Swift 复制代码
/// 要显示的的3D角色
var character: BodyTrackedEntity?
/// 骨骼位置
let characterOffset: SIMD3<Float> = [-1, 0, 0]
let characterAnchor = AnchorEntity()

接下来我们进行相关视图、代理和配置的设置

Swift 复制代码
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    arView.session.delegate = self
    
    // 设备是否支持
    guard ARBodyTrackingConfiguration.isSupported else {
        fatalError("需要A12芯片以上的iOS设备")
    }
    
    // run身体跟踪配置
    let configuration = ARBodyTrackingConfiguration()
    arView.session.run(configuration)
    arView.scene.addAnchor(characterAnchor)
    
    // 异步加载3D角色
    loadBody()
}

加载骨骼模型的部分,前面已经有相关的代码示例了,这里再不罗列

剩下的功能逻辑,我们只需要在代理方法内部实现就好:

Swift 复制代码
/// ARSession代理,当目标位置更新时调用
func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
    for anchor in anchors {
        guard let bodyAnchor = anchor as? ARBodyAnchor else { continue }
        
        // 更新角色锚点位置
        let bodyPosition = simd_make_float3(bodyAnchor.transform.columns.3)
        characterAnchor.position = bodyPosition + characterOffset
        // 身体锚点旋转
        characterAnchor.orientation = Transform(matrix: bodyAnchor.transform).rotation

        // 检测主体锚点并且角色已加载
        if let character = character, character.parent == nil {
            //将角色附加到其锚点
            characterAnchor.addChild(character)
        }
    }
}

现在运行看看效果(图28)

总结

本节内容,简单介绍了Reality Composer工具的使用和RealityKit框架的基础应用,简单的场景实践,可能并不能应用到业务场景,但是我们看到了苹果AR生态升级后,更好的AR场景实践的可能性,这还是很让人期待的。

今年的WWDC23苹果发布了头显系统Vision OS,而Reality Composer工具的迭代版本Reality Composer Pro 和RealityKit以及ARKit3,都是Vision OS的重要技术栈,所以,这块技术的深入研究还是很有应用空间的。

相关推荐
大熊猫侯佩12 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(三)
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(二)
数据库·swiftui·swift
大熊猫侯佩2 天前
用异步序列优雅的监听 SwiftData 2.0 中历史追踪记录(History Trace)的变化
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(一)
数据库·swiftui·swift
season_zhu2 天前
iOS开发:关于日志框架
ios·架构·swift
大熊猫侯佩2 天前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩2 天前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩2 天前
使用令牌(Token)进一步优化 SwiftData 2.0 中历史记录追踪(History Trace)的使用
数据库·swift·apple
大熊猫侯佩2 天前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple