【Swift】RealityComposerPro のアニメーションを実行する
初めに
今回は RealityComposerPro の Timeline で作成したアニメーションを SwiftUI のコード側で実行するための手順を共有したいと思います。 今回は visionOS で実行してみたいと思います。
実は以前も以下の記事で配置したオブジェクトにアニメーションを加える実装は行なっていました。しかし、この記事ではほぼすべてコードベースで実装を行なっていました。今回の記事ではこれとは異なる実装方法でアニメーションを追加していきます。
記事の対象者
- SwiftUI 学習者
- visionOS でアニメーションを実装したい方
- Reality Composer Pro の Timeline を活用したい方
目的
今回の目的は、上記の通り RealityComposerPro で作成したアニメーションを visionOS 上で実行することです。モデルの追加から実際にアニメーションを実行するまでの手順を一通り確認することが今回の目的となります。最終的には以下の二つの動画のようなアニメーションを含む実装を行いたいと思います。
実装
実装は以下の手順で行います。
- モデルの取り込み
- モデルの配置
- アニメーションの追加
- SwiftUI からの実行
1. モデルの取り込み
今回は以下のような掃除ロボットのモデルを Blender から取り込んでいきます。
今回使用するモデルは、それぞれのパーツに応じて適応させたいアニメーションが異なるため、各パーツごとでエクスポートしていきます。
Apple の3Dアセットに関する解説動画ではエクスポートの形式は .usdc
形式でエクスポートしていますが、今回はテクスチャの読み込みが安定しなかったため、アニメーションを含むものに関しては、.fbx
形式、アニメーションを含まないものに関しては .glb
形式でエクスポートを行いました。
事前にエクスポートしたいコンポーネントを選択しておき、エクスポートの設定で「Limit to Selected Objects」にすることで、選択したコンポーネントを個別にエクスポートすることができるようになります。
これでエクスポートは完了です。次にプロジェクト内でコンポーネントを使用するためにファイル形式の変換を行う必要があります。これは「Reality Converter」を用いて行います。この操作は比較的簡単で、 Reality Converter に先ほどエクスポートした .glb
, fbx
形式のファイルを読み込み、「書き出す」の項目を選択すれば形式が変換されたファイルが書き出されます。書き出されたファイルをXcodeのプロジェクト内で使用するためには、プロジェクト内の Packages > RealityKitContent > Sources > RealityKitContent >
RealityKitContent の配下に配置すれば完了です。なお、Apple公式の解説動画の通りに .usdc
形式でコンポーネントをエクスポートした場合は「Reality Converter」を用いた変換は不要で、そのままプロジェクトに配置できるかと思います。
これでモデルの取り込みは完了です。
2. モデルの配置
次にモデルの配置を行います。
今までですべてのパーツのプロジェクトへの配置が完了しているとします。次に必要なのはモデルの配置です。
配置したいパーツをドラッグ&ドロップで配置することもできますが、モデルを右クリックして「Add to Scene」を選択するとエクスポートした際の座標をそのまま反映させて追加することができるため、非常に便利です。
なお、注意点として、 Blender と Reality Composer Pro では座標軸の向きが異なります。 Blender では座標空間における「上方向に移動する」とは、「z軸がプラスの方向に移動する」ことを指します。一方で Reality Composer Pro における「上方向に移動する」とは、「y軸がプラスの方向に移動する」ことを指します。
この問題点の解決方法としては、以下のような方法が考えられます。
- 各コンポーネントの Rotation の y軸の値を「-90」に設定して、Blenderと同じ向きに直す
- Blenderからコンポーネントをエクスポートする際(特に
.fbx
形式など)に、forward
の項目で軸を変更しておく
これでモデルの配置は完了です。
3. アニメーションの追加
次はアニメーションの追加です。
まずはアニメーションの実装の前に Timelines に含まれる機能の簡単な解説から入ります。必要なければ読み飛ばしてください。なお、各部分の名前は正式名称ではないので、詳しくは解説動画をご覧ください。
- Timelines タブ (画像赤枠)
アニメーションの設定は Reality Composer Pro の画面下部の「Timelines」タブで行います。
アニメーションがまだ存在しない場合は「Create Timelines」で新たに作成することができます。
- Timelines (画像青枠)
画面左側のサイドバーでは複数のアニメーションの管理をすることができます。アニメーションの名前を変更したり、+ボタンで新たに追加したりすることができます。
- Actions (画像緑枠)
画面右側の Actions ではさまざまなアニメーションを選択することができます。アニメーションの種類については後述しますが、画面下部中央にドラッグ&ドロップすることでアニメーションを追加することができます。
- アニメーション詳細設定 (画像黄枠)
画面右上では選択しているアニメーションの詳細設定を行うことができます。具体的には、アニメーションの長さやアニメーションが始まるタイミング、アニメーションの対象となるコンポーネントなどが指定できます。
上記の画像では Timelines のサイドバーで「Step1」が選択されており、 Actions からアニメーションをドラッグ&ドロップすることで、「Step1」にまとめてアニメーションを追加できます。
ここではいくつか Actions を紹介してみたいと思います。
Transform To
Transform To はターゲットとなっているコンポーネントを指定した位置、回転、大きさまで変化させることができます。単純にコンポーネントをある地点から別の地点に移動させたい際に使用します。
「Duration」でアニメーションの秒数を指定することができます。具体的には、「B地点まで移動するのに○秒かけて移動する」といったことが可能になります。
「Timing Function」を設定することで、移動させる際のアニメーションを調整することができます。具体的には、一定の速度で移動させたり、最初は早く移動して最後は遅く移動したりなど複数の設定が可能です。
Spin
Spin はターゲットとなっているコンポーネントを回転させることができます。
Duration, Timing Function は Transform To と同じような効果です。
Revolutions では回転する量を決定します。1 に指定することでコンポーネントは1回転します。 0.5 なら 180°、 0.25 なら 90° 回転します。
Spin Direction では回転が時計回りか反時計回りかを指定することができます。
Axis で回転の軸を指定することができます。
Hide
Hide はターゲットとなっているコンポーネントをユーザーから見えなくします。
Transform To と同様に Duration, Timing Function で何秒で見えなくなるかやどのような早さで見えなくなるのかなどが調整できます。
不要になったコンポーネントに対して Hide を当てることで、必要ないコンポーネントを見えなくし、ユーザーを別のコンポーネントに集中させることができるようになります。
Actions にある「Show」は隠されたコンポーネントを再び見えるようにするものだと思われます。
Disable Entity
Disable Entity はターゲットとなっているコンポーネントを見えなくします。これだけだと Hide と同様の効果ですが、 Disable Entity の場合はアニメーションすることなく一瞬で消えます。また、一度 Disable Entity を当てたコンポーネントは再度見えるようにしたりアニメーションを当てたりすることができなくなります。
Actions にある「Enable Entity」は Disable になった Entity を再度操作可能にするために使用すると思われます。
今回紹介したアニメーション以外にも多くの種類のアニメーションがあり、基本的なものであれば手軽に実装できます。 Blender などを使用したことがある方などであれば操作も似ているため扱いやすいかもしれません。
それぞれのアニメーションを追加し終わったら、各モデルに対してどのTimelineを適応させるかを指定していきます。アニメーションを実行したいターゲットをまず選択します。
以下の画像では lib_animation というモデルが選択されています。
次に選択したモデルのサイドバーで「Add Component」を押下します。
ここで「Behavior」を選択して追加します。
追加した「Behavior」で + ボタンを押し、「OnNotification」を選択します。
この時選択する項目は「OnNotification」以外にも以下の項目があり、用途はそれぞれです。
- OnTap: オブジェクトがタップされた際にアニメーションを実行
- On Collision: オブジェクトが別オブジェクトとの衝突判定が生じた時にアニメーションを実行
- On Added To Scene: オブジェクトが画面に追加された時にアニメーションを実行
次に「Notification Name」でアニメーションを呼び出す際の名前、「Action」で実行するアクションを選択します。今回は Notification Name と Action は一致させて「Step1」としています。
最後にそれぞれのアニメーションをモデルに適応させます。このプロジェクトの場合では Step2, Step3 があるのでそれぞれをモデルに適応させておきます。
これでこのステップは完了です。
4. SwiftUI からの実行
最後に今まで作成してきたアニメーションを実行していきたいと思います。
アニメーションを含むモデルの画面コードは以下の通りです。
import SwiftUI
import RealityKit
import RealityKitContent
struct RobotCleanerDustBox: View {
@Environment(\.realityKitScene) var scene
@Environment(\.openWindow) private var openWindow
@Environment(\.dismissWindow) private var dismissWindow
@State private var currentIndex = 1
let rkcb = realityKitContentBundle
let rknt = "RealityKit.NotificationTrigger"
fileprivate func notify(scene: RealityKit.Scene, animationName: String) {
let notification = Notification(
name: .init(rknt),
userInfo: ["\(rknt).Scene" : scene,"\(rknt).Identifier" : animationName])
NotificationCenter.default.post(notification)
}
// Reality Composer Pro で定義しているアニメーションの命名が1番から始まるため
let descriptions: [String] = ["", "", "上部のフタを開ける", "フィルターがついた状態で\nダストボックスを取り出す", "ゴミ箱を用意して\nダストボックス内のゴミを取り出してください"]
var body: some View {
RealityView { content, attachments in
if let robotCleanerEntity = try? await Entity(named: "DustBox", in: realityKitContentBundle) {
robotCleanerEntity.scale = SIMD3<Float>(1.5, 1.5, 1.5)
robotCleanerEntity.position = robotCleanerEntity.position(relativeTo: robotCleanerEntity) + SIMD3<Float>(0, -0.2, 0.1)
content.add(robotCleanerEntity)
if let sceneAttachment = attachments.entity(for: "RobotCleanerDustBox") {
sceneAttachment.position = sceneAttachment.position(relativeTo: robotCleanerEntity) + SIMD3<Float>(0, -0.5, 0.3)
content.add(sceneAttachment)
}
}
} attachments: {
Attachment(id: "RobotCleanerDustBox") {
if currentIndex < descriptions.count {
VStack {
Text(descriptions[currentIndex])
Spacer()
.frame(height: 30)
if currentIndex == 4 {
Button("完了") {
currentIndex += 1
openWindow(id: "Content")
}
} else {
Button("Step\(currentIndex)へ") {
if let scene { notify(scene: scene, animationName: "Step\(currentIndex)") }
currentIndex += 1
}
}
Spacer()
.frame(height: 30)
Button {
openWindow(id: "Content")
dismissWindow(id: "RobotCleanerDustBox")
} label: {
HStack {
Image(systemName: "xmark")
Text("閉じる")
}
}
}
}
}
}
.onAppear {
dismissWindow(id: "Content")
}
}
}
それぞれ詳しく見ていきます。
まずは以下の部分です。
この部分で、Reality Composer Pro に設定されているアニメーションを呼び出す処理を記述しています。
先程のステップでアニメーションの発火条件を「OnNotification」としたため、Notification
を用いてアニメーションを発火させます。
具体的には、NotificationCenter
に対して現在のシーンと発火させたいアニメーションの名前を渡すことでアニメーションが発火します。
let rkcb = realityKitContentBundle
let rknt = "RealityKit.NotificationTrigger"
fileprivate func notify(scene: RealityKit.Scene, animationName: String) {
let notification = Notification(
name: .init(rknt),
userInfo: ["\(rknt).Scene" : scene,"\(rknt).Identifier" : animationName]
)
NotificationCenter.default.post(notification)
}
次に以下では DustBox
という名前の Entity を見つけて、その大きさや位置を調節して content
に追加しています。また、アタッチメントに関しても DustBox
の Entity の位置に合わせて調整して content
に追加しています。
if let robotCleanerEntity = try? await Entity(named: "DustBox", in: realityKitContentBundle) {
robotCleanerEntity.scale = SIMD3<Float>(1.5, 1.5, 1.5)
robotCleanerEntity.position = robotCleanerEntity.position(relativeTo: robotCleanerEntity) + SIMD3<Float>(0, -0.2, 0.1)
content.add(robotCleanerEntity)
if let sceneAttachment = attachments.entity(for: "RobotCleanerDustBox") {
sceneAttachment.position = sceneAttachment.position(relativeTo: robotCleanerEntity) + SIMD3<Float>(0, -0.5, 0.3)
content.add(sceneAttachment)
}
}
以下ではアタッチメントの実装を行なっています。
今回のプロジェクトではステップごとの説明を表示させているので、 currentIndex
によって表示内容を切り替えています。
ここで先程定義した notify
を実行しています。 Step1, Step2, Step3 のように Step\(currentIndex)
で表せるアニメーション名にしており、 animationName
に渡しています。
Attachment(id: "RobotCleanerDustBox") {
if currentIndex < descriptions.count {
VStack {
Text(descriptions[currentIndex])
Spacer()
.frame(height: 30)
if currentIndex == 4 {
Button("完了") {
currentIndex += 1
openWindow(id: "Content")
}
} else {
Button("Step\(currentIndex)へ") {
if let scene { notify(scene: scene, animationName: "Step\(currentIndex)") }
currentIndex += 1
}
}
Spacer()
.frame(height: 30)
Button {
openWindow(id: "Content")
dismissWindow(id: "RobotCleanerDustBox")
} label: {
HStack {
Image(systemName: "xmark")
Text("閉じる")
}
}
}
}
}
今回作成したプロジェクトは以下のGitHubリンクで公開しています。
実行すると以下の動画のようになっているかと思います。
まとめ
最後まで読んでいただいてありがとうございました。
今回 Reality Composer Pro を使っていて比較的簡単にアニメーションを実装できた実感がありました。恥ずかしながら、今回引用した動画や Timeline について知る前は座標を一つ一つ指定して全てコードで実装していました。しかし今回の内容でコードからはアニメーションを呼び出すだけに抑えられたので実装のコストが非常に小さくなったかと思います。
誤っている点やもっと良い方法があればご指摘いただければ幸いです。
参考
Discussion