JINMUSOFTWARE

RealityKitで3Dモデルを表示してスワイプで回転させる方法【SwiftUI】

「RealityKit」「RealityView」を使用して3Dモデル(.usdz形式)を表示し、さらにスワイプ操作で回転させる方法を調べました。

目的

3Dモデルをスワイプで回転させます。

補足:

  • 3Dモデルは「.usdz」ファイルです。
  • RealityKitを使用します。
  • RealityViewを使用して3Dモデルを表示します。

実行環境

開発環境:

  • Mac mini 2024 Apple M4
  • macOS Tahoe 26.5.2
  • xcode 26.6

動作環境:

  • iOS 26.5 xcode Preview
  • iOS 26.5 iPhone 13 Pro Max Simulator
  • iOS 18.7.9 iPhone Xs
  • iOS 26.5.2 iPhone 12

条件:

RealityViewを使用するのでiOS18以上が必要となります。

RealityView – Apple Developer Documantation

3D Model(.usdz)

使用する3DモデルはBlenderで作成した立方体です。1辺0.7m。

実装

全コード

コメントがシロウティーですが気にしないでください。

Swift
import RealityKit
import SwiftUI

/// 3Dモデルの表示とスワイプによる回転操作のサンプルです。
///
/// === Note ===
/// 座標の指定方法は[x, y, z]。回転の場合は軸になる。
/// 型がSIMD3<Float>であることに注意。
/// 例)let init_position: SIMD3<Float> = [0, 0, 0]
/// 3次元上での回転角の名称
///  yaw 左右の首振り、垂直軸
///  pitch 上下を向く傾き
///  roll 左右の傾き、前後軸の回転
/// スワイプ量がOvershootして回転がjumpする現象は実機でのみ発生した。
struct Case6: View {
    @State private var modelEntity: Entity?
    @State private var yaw: Float = 0  // 左右
    @State private var pitch: Float = 0  // 上下
    @State private var dragFrameCount: Int = 0
    @State private var lastTranslation: CGSize = .zero

    var body: some View {
        // 3Dモデルの表示
        RealityView { content in
            // USDZ読み込み
            if let entity = try? await Entity(named: "cube") {
                // シーンへ追加
                content.add(entity)
                // entity保存
                modelEntity = entity
            }
        }
        // スワイプで回転させるためのジェスチャーを追加
        .gesture(
            // minimumDistanceを0にすると、なんか不安定
            // 8フレーム目付近でもジャンプがあった。
            // minimumDistanceを1以上にすると安定する。
            // 大体3フレーム目までにジャンプがある。
            // coordinateSpaceはよくわからない。
            // DragGesture(minimumDistance: 0, coordinateSpace: .local)
            DragGesture(minimumDistance: 2)
                .onChanged { value in
                    // Entityがあるか確認
                    guard let entity = modelEntity else { return }

                    // フレーム数をカウント
                    // 先頭フレームを1〜3枚捨てたいのでカウントしている
                    dragFrameCount += 1

                    // 直前のフレームからの「差分(delta)」を計算
                    // 差分を使うことで、Overshoot気味のフレームを捨てることができる。
                    // translationのwidthとheightは、ドラッグ開始位置からの累積値
                    // なので、そのまま使うとジャンプする。
                    // 差分を取る必要がある。
                    let deltaX = Float(
                        value.translation.width - lastTranslation.width
                    )
                    let deltaY = Float(
                        value.translation.height - lastTranslation.height
                    )

                    // 現在の translation を次のフレームのために記憶
                    lastTranslation = value.translation

                    // DEBUG: フレーム数と差分を表示して確認
                    // 1〜3フレームあたりで移動量に大きい値があることに気づける。
                    // ゆっくりスワイプしてみるとわかる。
                    // 回転のジャンプは実機のみ発生した。
                    print("\(dragFrameCount):\(deltaY)")

                    // Overshoot気味のフレームを捨てる
                    // きっとiOSのドラッグジェスチャーの仕様なのだろう。
                    if dragFrameCount <= 3 {
                        // ジャスチャー開始時の大きめの値は使わずに捨てる
                        // minimumDistanceの値と関連するようだ。
                        // minimumDistanceを1にすると安定し、1〜3フレーム目に
                        // Overshootが収まるようになった。
                        // なので3フレーム目までは捨てることにした。
                        return
                    }

                    // 回転量調整、0.01がちょうどいい感じ。
                    yaw += deltaX * 0.01
                    pitch += deltaY * 0.01

                    // 上下回転を90°までに制限
                    // 上下回転に制限があったほうが見やすいらしい。
                    pitch = max(
                        -.pi / 2 + 0.01,
                        min(.pi / 2 - 0.01, pitch)
                    )

                    // Y軸回転
                    let yRotation = simd_quatf(
                        angle: yaw,
                        axis: [0, 1, 0]
                    )

                    // X軸回転
                    let xRotation = simd_quatf(
                        angle: pitch,
                        axis: [1, 0, 0]
                    )

                    // 回転を適用
                    entity.transform.rotation = yRotation * xRotation
                }
                .onEnded { _ in
                    // 指を離したらリセット
                    dragFrameCount = 0
                    lastTranslation = .zero
                    // DEBUG:コンソールはクリアできないので。切れ目を入れてLogを見やすくする
                    print("---")
                }
        )
    }
}

#Preview {
    Case6()
}

Previewの様子

スワイプしているところの動画
  • 動画の前半はとってもゆっくりドラッグしています。カクついてい無いことを確認しています。
  • 後半は上下90°までしか回転しないことを確認しています。

解説

3Dモデルの表示

3Dモデルの表示はとても簡単です。

usdzファイルをProjectNavigatorのツリーに追加するだけです。Assetの追加ではありません。

Swift
RealityView { content in
    // Loading USDZ file.
    if let entity = try? await Entity(named: "cube") {
        content.add(entity)
    }
}

この他に必要に応じて、ライト、カメラ、モデルの初期位置などを設定します。

今回のコードでは、読み込んだモデルを使い回すために、`modelEntity`に保存しています。

Swift
modelEntity = entity

モデルを回転させる

回転させるのも簡単です。

entity.transform.rotationを更新すればいいだけです。

Swift
entity.transform.rotation = yRotation * xRotation

型は`simd_quatf`です。

simd_quatf型同士をかけてもsimd_quatf型のようです。縦横の移動量が合成できますね。

回転のカクつきを抑える 其の一

previewで確認すると、回転開始時にカクつきました。

カクつき対応1:minimumDistanceの値を0にする

Swift
DragGesture(minimumDistance: 0)

iPhoneの画面に指が触れた時、それがタッチなのかドラッグなのかを判断をしているようです。

この値はそのマージンのようです。

デフォルト値は10のようです。0にするとカクつきは消えました。

previewとsimulatorではカクつかないことを確認できました。

回転のカクつきを抑える 其の二

実機テストではカクつくことが判明しました。

minimumDistanceの値を0にしても、実機でテストするとカクつきました。タッチとスワイプの判定がSimulatorとは違うのかな?

以前のコードでは回転の移動量(value.translation.width)をそのまま回転量としてsimd_quatfの引数angleに渡していました。

これだと、value.translation.widthは移動量を積算しているので、最初の回転でJumpしたようにカクついてしまいます。

カクつき対応2

移動量の積算値だと回転初期に大き値も拾ってしまいます。よって、移動量の差分に基づいて回転させるようにします。そして、回転開始時にカクつきを発生させるような大きな回転量を捨てることにします。

調整の結果、次のようになりました。

  • minimumDistanceの値は1〜2
  • 捨てるフレーム数は2〜3

minimumDistanceを0にすると大きい回転量の発生が1〜10フレームにばらつきました。

minimumDistanceは1か2ぐらいにすると、大きい回転量の発生が1〜3フレームに収まりました。

カクつき対応1は本質的ではなかったようです。

説明が地獄で申し訳ありません。

おわり

関連記事

SwiftUIで3Dモデルを表示する

参考