この記事の内容は?
WWDC24のセッションの内容を試して以下の動画のようなアプリを実装した記事になっています。
基本的にはWWDC24の内容の通りやった形ですが、アニメーション周りの準備に関して少しだけ追記をしています(が、記事を間に合わせようと力技でやったので、あんまり良い方法ではないと思います)。
はじめに
visionOS Advent Calendar 2024 22日目の記事です。
こんにちは、こうたと言います。
今回は、WWDC24の内容をもとに、3DキャラクターをUIの世界から、私たちの世界に召喚してみようと思います!
何をしているか?
UI的な表現と没入的な表現を行き来することで、キャラクターがまるで自分たちの元にきてくれるような生き生きとした表現を実現しています。
具体的には、Volume とImmersive Spaceの二つのシーンタイプを行き来することで演出をしています。
Volumeは、限られた空間内に3Dオブジェクトを描画できるシーンとなっています。こちらを使うことで、箱庭的な空間にキャラクターがいる表現を実現できます。
そして、Immersive Spaceは、用意した3D空間に入ることができるので、より没入的な空間で目の前までキャラクターが来てくれるような感覚を表現できます。
具体的な方法
シーンの用意
まずは、VolumeとImmersiveの両空間を用意します
@main
struct ImmersiveCharactorApp: App {
@State private var appModel = AppModel()
var body: some Scene {
WindowGroup {
VolumeticView()
.ornament(attachmentAnchor: .scene(.topBack)) {
OrnamentView()
.environment(appModel)
}
.environment(appModel)
}
.windowStyle(.volumetric)
ImmersiveSpace(id: appModel.immersiveSpaceID) {
ImmersiveView()
.environment(appModel)
.onAppear {
appModel.immersiveSpaceState = .open
}
.onDisappear {
appModel.immersiveSpaceState = .closed
}
}
.immersionStyle(selection: .constant(.progressive), in: .progressive)
}
}
そして、以下で空間の切り替えを制御してます。
VolumeのOrnamentにButtonを配置し、Immersive空間のON/OFFを制御できるようにしています。
struct OrnamentView: View {
@Environment(AppModel.self) private var appModel
@Environment(\.dismissImmersiveSpace) private var dismissImmersiveSpace
@Environment(\.openImmersiveSpace) private var openImmersiveSpace
var body: some View {
VStack {
Button {
Task { @MainActor in
switch appModel.immersiveSpaceState {
case .open:
appModel.immersiveSpaceState = .inTransition
await dismissImmersiveSpace()
case .closed:
appModel.immersiveSpaceState = .inTransition
switch await openImmersiveSpace(id: appModel.immersiveSpaceID) {
case .opened:
break
case .userCancelled, .error:
fallthrough
@unknown default:
appModel.immersiveSpaceState = .closed
}
case .inTransition:
break
}
}
}
}
}
...
@MainActor
@Observable
class AppModel {
let immersiveSpaceID = "ImmersiveSpace"
enum ImmersiveSpaceState {
case closed
case inTransition
case open
}
var immersiveSpaceState = ImmersiveSpaceState.closed
...
空間の行き来について
Immersive空間を表示できるようになりました。
次に、VolumeのキャラクターをImmersive空間に移動させたいと思います。
WWDC2024の20:52で具体的な解説がされているので、ここでは具体的な解説は割愛します。
内容としては、Volumeの空間でアフィン変換行列を取得し、そちらを利用してImmersive空間に写像しています。
struct VolumeticView: View {
@Environment(AppModel.self) private var appModel
var body: some View {
RealityView { content in
if let scene = try? await Entity(named: "Volumetirc", in: realityKitContentBundle) {
content.add(scene)
appModel.content = content
appModel.setCharactor(entity: scene);
}
} update: { content in
guard appModel.convertingRobotFromVolume else { return }
appModel.convertRobotFromRealityKitToImmersiveSpace(content: content)
}
}
}
struct ImmersiveView: View {
@Environment(AppModel.self) var appModel
var body: some View {
RealityView { content in
if let immersiveContentEntity = try? await Entity(named: "Immersive", in: realityKitContentBundle) {
content.add(immersiveContentEntity)
}
content.add(appModel.immersiveSpaceRoot)
// Immersiveを表示した時にONにしている
// appModel内で制御したいが、解説用にここで制御
appModel.convertingRobotFromVolume = true
appModel.content = content
} update: { content in
guard appModel.convertingRobotToImmersiveSpace else { return }
appModel.convertRobotFromSwiftUIToRealityKitSpace(content: content)
}
}
func convertRobotFromRealityKitToImmersiveSpace(content: RealityViewContent) {
immersiveSpaceFromRobot =
content.transform(from: charactor, to: .immersiveSpace)
charactor.setParent(immersiveSpaceRoot)
convertingRobotFromVolume = false
convertingRobotToImmersiveSpace = true
}
func convertRobotFromSwiftUIToRealityKitSpace(content: RealityViewContent) {
let realityKitSceneFromImmersiveSpace =
content.transform(from: .immersiveSpace, to: .scene)
let realityKitSceneFromRobot =
realityKitSceneFromImmersiveSpace * immersiveSpaceFromRobot
charactor.transform = Transform(realityKitSceneFromRobot)
convertingRobotToImmersiveSpace = false
startJump()
}
Immersive空間で没入感のある表現の追加
VisionOS2.0以降で、onImmersionChange
を使うことでDegitalCrownによるImmersive度の変更を取れるようになりました。
こちらをつかうことで、没入度が増えた時にこちら側へ向かってくる表現をすることでより没入感を増した表現ができます。
こちらも、WWDCのビデオで解説されているので詳細は割愛します。
struct ImmersiveView: View {
@Environment(AppModel.self) var appModel
@State var immersionAmount: Double?
var body: some View {
RealityView { content in
...
} update: { content in
...
}
.onImmersionChange { oldContext, newContext in
immersionAmount = newContext.amount
}
.onChange(of: immersionAmount) { oldValue, newValue in
handleImmersionAmountChanged(newValue: newValue, oldValue: oldValue)
}
}
func handleImmersionAmountChanged(newValue: Double?, oldValue: Double?) {
guard let newValue, let oldValue else {
return
}
if newValue > oldValue {
appModel.moveCharactorOutward()
} else if newValue < oldValue {
appModel.moveCharactorInward()
}
}
...
キャラクターのアニメーションについて
WWDCでは、その他の様々な表現が紹介されており、それらを活用することでアプリに没入感を足すことが足せそうです。
これらの表現をコードで制御することができた一方で、アニメーションも生き生きとした表現には欠かせません。こちらの準備については、解説がないので、自分が用意した方法を軽く触れたいと思います
アニメーションの準備
まず、キャラクター用のアニメーションの用意をします。
Mixamoには豊富なアニメーションが用意されており、個人的にはおすすめです。もしくは、BoothやUnityAssetStoreなどでもアニメーションが存在しているので探してみるのも良さそうです。
アニメーションの変換
VisionProで扱うためには、USDやUSDZといったフォーマットにする必要があります。
Reality Converterを使うと、お手軽に変換できるので、こちらで用意したFBXなどを変換して扱うことが可能になります。
もしくは、Unityでも変換用のパッケージがあり、こちらでも変換が手軽にできます。
コード上でのアニメーションの制御
VisionOS2.0以降ではAnimationLibraryを使うことで、Entityのアニメーションにアクセスし、書き出しなどを行うことができるようになります。
たとえば、Animationをあるディレクトリにまとめておき、読み取りまとめて書き出すこともできます(下記コードは実行できてないので動くかは保証できてません)。Entityインスタンスをそのまま活用しても良さそうです。
読み込んだアニメーションはEntity.playAnimationで再生できます。
if let charactor = try? await Entity(named: "Volumetirc", in: realityKitContentBundle) {
let animationDirectory = "animations/\(アニメーションのUSDZの名前)"
if let rootEntity = try? await Entity(named: animationDirectory, in: realityKitContentBundle) {
if let _entity = rootEntity.findEntity(named: "アニメーションがあるEntityの名前") {
if let animationLibraryComponent = await _entity.components[AnimationLibraryComponent.self] {
guard var _animationLibrary = entity.components[AnimationLibraryComponent.self] else { return }
_animationLibrary.animations[animationName.rawValue] = animationLibraryComponent.defaultAnimation
}
}
}
charactor.components.set(animationLibrary)
charactor.write(to: fileURL)
}
最後に
WWDCの内容を活用して、3Dキャラクターを没入的に登場させるやり方を試してみました。
VisionOS2.0からAPIが増えて、さまざまな情報をとることができるようになり、インタラクティブな表現が試せるようになったことを感じられます。
アニメーションの設定の時間が足りず途中になってしまったのですが、それでも飛び出してくるような表現があると臨場感を感じることができました。
WWDCにはたくさんのビデオが公開されているので、他のセッションも是非とも試してみたいところです。
使用アセット
3Dキャラクター:猫山苗(販売終了)
待機モーション:マヌカ(https://booth.pm/ja/items/5058077)![アップロードできるファイル容量は各10MBまでです。]()