本节我们开始初探在visionOS中添加动画效果,我们的入口文件和ContentView
和Day 6中并没有什么区别,所以重点来看ViewModel
和ImmersiveView
。
首先是ViewModel.swift
文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 |
import SwiftUI import RealityKit class ViewModel: ObservableObject { private var contentEntity = Entity() func setupContentEntity() -> Entity { return contentEntity } func getTargetEntity(name: String) -> Entity? { return contentEntity.children.first { $0.name == name } } func addCube(name: String, position: SIMD3<Float>, color: UIColor) -> ModelEntity { let entity = ModelEntity( mesh: .generateBox(size: 0.5, cornerRadius: 0), materials: [SimpleMaterial(color: color, isMetallic: false)], collisionShape: .generateBox(size: SIMD3<Float>(repeating: 0.5)), mass: 0.0 ) entity.name = name entity.position = position entity.components.set(InputTargetComponent(allowedInputTypes: .indirect)) entity.components.set(CollisionComponent(shapes: [ShapeResource.generateBox(size: SIMD3<Float>(repeating: 0.5))], isStatic: true)) entity.components.set(HoverEffectComponent()) contentEntity.addChild(entity) return entity } func playAnimation(entity: Entity) { let goUp = FromToByAnimation<Transform>( name: "goUp", from: .init(scale: .init(repeating: 1), translation: entity.position), to: .init(scale: .init(repeating: 1), translation: entity.position + .init(x:0 ,y: 0.4, z: 0)), duration: 0.2, timing: .easeOut, bindTarget: .transform ) let pause = FromToByAnimation<Transform>( name: "pause", from: .init(scale: .init(repeating: 1), translation: entity.position + .init(x:0 ,y: 0.4, z: 0)), to: .init(scale: .init(repeating: 1), translation: entity.position + .init(x:0 ,y: 0.4, z: 0)), duration: 0.1, bindTarget: .transform ) let goDown = FromToByAnimation<Transform>( name: "goDown", from: .init(scale: .init(repeating: 1), translation: entity.position + .init(x:0 ,y: 0.4, z: 0)), to: .init(scale: .init(repeating: 1), translation: entity.position), duration: 0.2, timing: .easeOut, bindTarget: .transform ) let goUpAnimation = try! AnimationResource.generate(with: goUp) let pauseAnimation = try! AnimationResource.generate(with: pause) let goDownAnimation = try! AnimationResource.generate(with: goDown) let animation = try! AnimationResource.sequence(with: [goUpAnimation, pauseAnimation, goDownAnimation]) entity.playAnimation(animation, transitionDuration: 0.5) } } |
通过前面的学习我们已经知道setupContentEntity
用于初始化实体对象,getTargetEntity
用于根据模型的名称查找到指定模型。addCube(name: String, position: SIMD3<Float>, color: UIColor)
中包含三个参数,name
用于指定所创建盒子的名称,position
用于指定盒子所处的位置,同样是按人所处位置为参照坐标,color
用于指定盒体的颜色。
接下来就是本节的一个重要的知识点了,playAnimation
用于创建动画效果,传入的参数entity
就是要实现动画效果的模型。
结构体FromToByAnimation
可用于实现实体对象和场景的动画,方式为逐渐修改参数值。from
值表示动画属性的起始值,to
表示动画结束时的属性值,也可以通过by
值让框架来计算动画结束时的值。duration
顾名思义是指动画的时长,timing
参数通过AnimationTimingFunction
结构体中的定时函数进行指定,包含的值有:
- default:生成默认过渡曲线的定时函数,不指定时此即为默认值。
- linear:生成线性过渡效果的定时函数。
- easeIn:生成淡入效果的定时函数。
- easeOut:生成淡出效果的定时函数。
- easeInOut:生成淡入淡出效果的定时函数。
bindTarget
参数指定为tranform
时表示动画作用于对象本身。
我们定义了三个动画函数,分别为goUp
、pause
和goDown
,实现在纵坐标上完成上下移动0.4个单位的动画效果。接下来通过这三个动画定义来生成动画资源并指定动画的顺序,最后对实体对象调用playAnimation
来播放动画,这里的transitionDuration
参数与三个动画函数中用时的总和相同。
接下来是我们的ImmersiveView
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
import SwiftUI import RealityKit struct ImmersiveView: View { @State var model = ViewModel() @State var cube1 = ModelEntity() @State var cube2 = ModelEntity() var body: some View { RealityView { content, attachments in content.add(model.setupContentEntity()) cube1 = model.addCube(name: "Cube1", position: SIMD3<Float>(x: 1, y: 1, z: -2), color: .red) cube2 = model.addCube(name: "Cube2", position: SIMD3<Float>(x: -1, y: 1, z: -2), color: .blue) if let attachment = attachments.entity(for: "cube1_label") { attachment.position = [0, -0.35, 0] cube1.addChild(attachment) } if let attachment = attachments.entity(for: "cube2_label") { attachment.position = [0, -0.35, 0] cube2.addChild(attachment) } } attachments: { Attachment(id: "cube1_label") { Text("Cube1") .font(.system(size: 48)) } Attachment(id: "cube2_label") { Text("Cube2") .font(.system(size: 48)) } } .gesture( SpatialTapGesture() .targetedToEntity(cube1) .onEnded { value in print(value) model.playAnimation(entity: cube1) } ) .gesture( SpatialTapGesture() .targetedToEntity(cube2) .onEnded { value in print(value) model.playAnimation(entity: cube2) } ) } } |
cube1
为红色,位于我们的右侧,cube2
为蓝色,位于我们的左侧,为方便标识,我们通过RealityView
的AttachmentContentBuilder
(即attachments
参数)来创建附属视图,但附属视图并不会自动添加到RealityView
的对象上。我们需要显式地进行指定,这里attachments.entity
中的for
与Attachment
的id
相对应,我们只添加了两个文本视图,并通过position
指定了相对于父对象的位置。
接着通过SpatialTapGesture
指定在点击相应盒子时执行前面所定义的playAnimation
。
运行应用,点击Show ImmersiveSpace
会显示两个盒子,盒子下方分别显示一个附属视图,点击盒体会执行上下移动的动画。
示例代码:GitHub仓库
其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记