👓

【Swift】Vision OS で自作のオブジェクトを移動させてみる

2023/11/17に公開

初めに

今回は以下の記事の続きとして、自作オブジェクトを配置するだけでなく、ユーザーの入力に応じた変更をしてみたいと思います。
https://zenn.dev/koichi_51/articles/c2766f3728c3d6

記事の対象者

  • Swift, SwiftUI 学習者
  • Vision OS に触れてみたい方

完成イメージ

完成イメージとしては以下のように、アニメーションを含む飛行機のオブジェクトを配置し、飛行機の進む方向を変更したり、飛行機のオブジェクトの大きさを大きくするなどの変更を行います。

完成イメージ

実装

Blenderからのモデルの取り込み

今回は以下のようにアニメーションを含むモデルを Blender からエクスポートして使ってみたいと思います。Blender からのエクスポートについてはこちらの記事をご覧ください。

Blender上のオブジェクトプレビュー

なお、以下のリンクに今回使用する plane オブジェクトを USDZ形式で保存しておきました。リンクを押すと Dropbox が開きます。プレビューはできませんが、ダウンロードして使用できるかと思います。
ダウンロードした USDZ形式のファイルは Xcode のプロジェクトにドラッグ&ドロップすればそのまま使用できます。

今回使用する plane オブジェクトのダウンロード

App の設定

App のコードの全文は以下のようになります。

SampleApp
import SwiftUI

@main
struct SampleApp: App {
    var body: some Scene {
        WindowGroup {
            PlaneView()
        }
        .windowStyle(.volumetric)
	.defaultSize(width: 2.5, height: 1, depth: 2.5, in: .meters)
    }
}

今回実装する画面は飛行機を表示させる PlaneView() のみなので、他の画面の実装がなくシンプルです。
なお、今回表示させる飛行機のオブジェクトはアニメーションを含むため、defaultSize としてウィンドウのサイズを指定しています。こうすることで、オブジェクトが見切れることがなくなるかと思います。

PlaneView の作成

次に PlaneView の実装を行います。
最終的なコードの全文は以下のようになります。

PlaneView
import SwiftUI
import RealityKit
import RealityKitContent

struct PlaneView: View {

    @State var enlarge = false
    @State var rotate = false
    let attachmentID = "attachmentID"

    var body: some View {
        VStack {
            RealityView { content, attachments in
                if let scene = try? await Entity(named: "PlaneScene", in: realityKitContentBundle) {
                    let animation = scene.availableAnimations[0]
                    scene.playAnimation(animation.repeat())
                    content.add(scene)
                }
                
                if let sceneAttachment = attachments.entity(for: attachmentID) {
                    sceneAttachment.position = SIMD3<Float>(0, -0.2, 0.9)
                    sceneAttachment.transform.rotation = simd_quatf(angle: -0.5, axis: SIMD3<Float>(1,0,0))
                    content.add(sceneAttachment)
                }
            
            } update: { content, attachments in
                if let scene = content.entities.first {
                    let ix:Float = 0.0
                    let iy:Float = rotate ? 0.707107 : 0.0
                    let iz:Float = 0.0
                    let r:Float  = rotate ? 0.707107 : 0.0
                    let uniformScale: Float = enlarge ? 1.5 : 1.0
                    let uniformRotate = simd_quatf(ix: ix, iy: iy, iz: iz, r: r)
                    scene.transform.scale = [uniformScale, uniformScale, uniformScale]
                    scene.transform.rotation = uniformRotate
                }
            } placeholder: {
                ProgressView()
                    .progressViewStyle(.circular)
                    .controlSize(.large)
            } attachments: {
                Attachment(id: attachmentID) {
                    VStack {
                        Toggle("Enlarge RealityView Content", isOn: $enlarge)
                            .toggleStyle(.button)
                        Toggle("Rotate RealityView Content", isOn: $rotate)
                            .toggleStyle(.button)
                    }
                    .padding()
                    .glassBackgroundEffect()
                }
            }
        }
    }
}

#Preview {
    PlaneView()
}

上記のコードをより詳しくみるために、まずは以下のコードから実装してみます。

PlaneView
import SwiftUI
import RealityKit
import RealityKitContent

struct PlaneView: View {
    @State var enlarge = false
    @State var rotate = false

    var body: some View {
        VStack {
            RealityView { content in
                if let scene = try? await Entity(named: "PlaneScene", in: realityKitContentBundle) {
                    let animation = scene.availableAnimations[0]
                    scene.playAnimation(animation.repeat())
                    content.add(scene)
                }
            }
        }
    }
}

#Preview {
    PlaneView()
}

上記のコードを実行するとこちらのように飛行機が奥側から手前側に飛んでくることがわかります。

処理の内容としては、まず RealityView を定義し、content にモデルを追加していく処理になります。なお、PlaneScene は先ほどの飛行機のモデルをダウンロードして、Xcodeのプロジェクト内に配置した後、適切な場所に移動させたモデルになります。この辺りは多少微調整が必要になるかと思います。

以下の部分では PlaneScene に登録されているアニメーションを取り出して、それをリピートする形で付与しています。

let animation = scene.availableAnimations[0]
scene.playAnimation(animation.repeat())

余談ですが、自分が試したところ、Blender で登録されたアニメーションに関しては Blender > Reality Converter > Xcode の流れを経ても消えないことがわかりました。
Blender などの3Dモデリングツールでアニメーションまで実装して、それをSwiftUIでループさせるか、モデルのみをモデリングツールで実装して、SwiftUIでアニメーションを実装するか、どちらのツールが使いやすいかは個人の好みで分かれるかと思います。
この辺りのメリット、デメリットは調べてみたいところではあります。

次に以下のように、attachments を追加してみましょう。

PlaneView
import SwiftUI
import RealityKit
import RealityKitContent

struct PlaneView: View {
+   @State var enlarge = false
+   @State var rotate = false
+   let attachmentID = "attachmentID"

    var body: some View {
        VStack {
            RealityView { content, attachments in
                if let scene = try? await Entity(named: "PlaneScene", in: realityKitContentBundle) {
                    let animation = scene.availableAnimations[0]
                    scene.playAnimation(animation.repeat())
                    content.add(scene)
                }
                
+               if let sceneAttachment = attachments.entity(for: attachmentID) {
+                   sceneAttachment.position = SIMD3<Float>(0, -0.2, 0.9)
+                   sceneAttachment.transform.rotation = simd_quatf(angle: -0.5, axis: SIMD3<Float>(1,0,0))
+                   content.add(sceneAttachment)
+               }
+           } attachments: {
+               Attachment(id: attachmentID) {
+                   VStack {
+                       Toggle("Enlarge RealityView Content", isOn: $enlarge)
+                           .toggleStyle(.button)
+                       Toggle("Rotate RealityView Content", isOn: $rotate)
+                           .toggleStyle(.button)
+                   }
+                   .padding()
+                   .glassBackgroundEffect()
+               }
+           }
        }
    }
}

#Preview {
    PlaneView()
}

上記のコードを実行するとこちらのようになります。
画面の手前側に二つの Toggle ボタンが表示されるようになりましたが、まだタップしても変化はありません。

以下のコードではそれぞれ、飛行機のモデルを大きくするかどうか、回転させるかどうかの状態を保持している変数と、attachment を実装する際に必要になるIDを保持する変数を定義しています。

@State var enlarge = false
@State var rotate = false
let attachmentID = "attachmentID"

以下のコードでは attachment の位置の調整、追加を行なっています。

if let sceneAttachment = attachments.entity(for: attachmentID) {
    sceneAttachment.position = SIMD3<Float>(0, -0.2, 0.9)
    sceneAttachment.transform.rotation = simd_quatf(angle: -0.5, axis: SIMD3<Float>(1,0,0))
    content.add(sceneAttachment)
}

以下の部分では attachment の位置を調整しています。
この場合の位置は attachment が付属するオブジェクト、つまり今回の場合だと飛行機のオブジェクトの中心点に対する相対的な位置で決められます。
以下の場合では飛行機のオブジェクトの中心から下側に -0.2, 手前側に 0.9 としています。
sceneAttachment.position = SIMD3<Float>(0, -0.2, 0.9)

以下の部分では attachment を回転させています。
回転させる大きさは -0.5 であり、回転の軸は x軸となっています。
x軸はユーザーから見て左右に走っている軸であり、このように設定することで、ユーザーの高い目線から attachment が見やすくなっています。
sceneAttachment.transform.rotation = simd_quatf(angle: -0.5, axis: SIMD3<Float>(1,0,0))

attachment を横から見ると以下の画像のように少し傾いていることがわかります。このようにすることで上から見た時により見やすくなります。

以下のコードでは具体的な attachment の実装を行なっています。
先ほどの if let sceneAttachment = attachments.entity(for: attachmentID) で指定したIDと同様のIDを指定することで、attachments の位置などを適用させることができます。

PlaneView
attachments: {
    Attachment(id: attachmentID) {
        VStack {
            Toggle("Enlarge RealityView Content", isOn: $enlarge)
                .toggleStyle(.button)
            Toggle("Rotate RealityView Content", isOn: $rotate)
                .toggleStyle(.button)
         }
         .padding()
         .glassBackgroundEffect()
    }
}

最後に以下の部分を追加していきます。

PlaneView
import SwiftUI
import RealityKit
import RealityKitContent

struct PlaneView: View {
    @State var enlarge = false
    @State var rotate = false
    let attachmentID = "attachmentID"

    var body: some View {
        VStack {
            RealityView { content, attachments in
                if let scene = try? await Entity(named: "PlaneScene", in: realityKitContentBundle) {
                    let animation = scene.availableAnimations[0]
                    scene.playAnimation(animation.repeat())
                    content.add(scene)
                }
                
                if let sceneAttachment = attachments.entity(for: attachmentID) {
                    sceneAttachment.position = SIMD3<Float>(0, -0.2, 0.9)
                    sceneAttachment.transform.rotation = simd_quatf(angle: -0.5, axis: SIMD3<Float>(1,0,0))
                    content.add(sceneAttachment)
                }
            } update: { content, attachments in
+               if let scene = content.entities.first {
+                   let ix:Float = 0.0
+                   let iy:Float = rotate ? 0.707107 : 0.0
+                   let iz:Float = 0.0
+                   let r:Float  = rotate ? 0.707107 : 0.0
+                   let uniformScale: Float = enlarge ? 1.5 : 1.0
+                   let uniformRotate = simd_quatf(ix: ix, iy: iy, iz: iz, r: r)
+                   scene.transform.scale = [uniformScale, uniformScale, uniformScale]
+                   scene.transform.rotation = uniformRotate
+               }
+           } placeholder: {
+               ProgressView()
+                   .progressViewStyle(.circular)
+                   .controlSize(.large)
            } attachments: {
                Attachment(id: attachmentID) {
                    VStack {
                        Toggle("Enlarge RealityView Content", isOn: $enlarge)
                            .toggleStyle(.button)
                        Toggle("Rotate RealityView Content", isOn: $rotate)
                            .toggleStyle(.button)
                    }
                    .padding()
                    .glassBackgroundEffect()
                }
            }
        }
    }
}

#Preview {
    PlaneView()
}

以下の部分ではオブジェクトの大きさと回転の変更を行なっています。
大きさに関しては enlarge の三項演算子を用いて、大きくするかどうかを切り替えています。
回転に関しては simd_quatf を使って四次元数で表現しています。この辺りの値は別途計算する必要があり複雑だったので、今回は省かせていただきます。
enlarge 同様に rotate の三項演算子を用いて、回転をするかどうかを切り替えています。

PlaneView
if let scene = content.entities.first {
    let ix:Float = 0.0
    let iy:Float = rotate ? 0.707107 : 0.0
    let iz:Float = 0.0
    let r:Float  = rotate ? 0.707107 : 0.0
    let uniformScale: Float = enlarge ? 1.5 : 1.0
    let uniformRotate = simd_quatf(ix: ix, iy: iy, iz: iz, r: r)
    scene.transform.scale = [uniformScale, uniformScale, uniformScale]
    scene.transform.rotation = uniformRotate
}

以下の部分では placeholder として、モデルを読み込んでいる際に表示させるビューを指定しています。今回は ProgressView を表示させています。

PlaneView
placeholder: {
    ProgressView()
        .progressViewStyle(.circular)
        .controlSize(.large)
}



以上のコードで完成イメージと同じようなビューが作成でき、飛行機のモデルの大きさ、回転方向も変更できるようになっているかと思います。

完成イメージ

まとめ

最後まで読んでいただいてありがとうございました。

オブジェクトを配置するだけでなく、大きさや回転、位置なども変更できると VisionOS で表現できることの幅が格段に広がるかと思います。
どの機能を実装する場合もオブジェクトの位置や大きさ、回転などを実装することはあると思うので、早く使用方法に慣れていきたいと思います。

Discussion