電通総研 テックブログ

電通総研が運営する技術ブログ

SpatialComputing入門Part2:3DオブジェクトにGestureを追加する

thumbnail
thumbnail
こんにちは!金融ソリューション事業部WEB3グループの山下です。
本記事は「SpatialComputing入門」シリーズのPart2となります。
前回のPart1で追加した3Dオブジェクトに対して複数のGestureによるインタラクションを実装していきます。

「SpatialComputing入門」シリーズ:

動作環境

実装手順

  1. TapGestureの追加
  2. VisionPro動作確認
  3. DragGestureの追加
  4. VisionPro動作確認

1. TapGestureの追加

Part1で作成したVolumeView.SwiftのStructを、以下に修正します。
オブジェクトをTapをすると、3Dコンテンツのスケールを増加する処理を実装します。

struct VolumeView: View {
    @State private var scale: Float = 1.0
    var body: some View {
        RealityView { content in
            // Generate ModelEntity
            let model = ModelEntity(
                mesh: .generateCone(height: 0.1, radius: 0.1),
                materials: [SimpleMaterial(color: .lightGray, isMetallic: true)]
            )
            
            // Add components
            model.components.set(InputTargetComponent())
            model.components.set(CollisionComponent(shapes: [.generateSphere(radius: 0.1)]))
            
            // Add ModelEntity to RealityView content
            content.add(model)
        } update: { content in
            if let model = content.entities.first {
                model.transform.scale = [scale, scale, scale]
            }
        }.gesture(TapGesture().targetedToAnyEntity().onEnded{ _ in
            scale += 0.1
        })
    }

変更点は以下のとおりです。

  • State変数の追加
    • @State private var scale:Float = 1.0 :スケール率を管理するState変数を追加
  • .update:クロージャを追加:
    • RealityViewで最初に取得するcontentに対して、scaleを変更する
      • State変数が変更されたら毎回呼び出される処理
  • .gestureイベントを追加:
    • TapGester()が終了したら、scaleを0.1増加

2. VisionPro動作確認

実行すると、以下のような挙動になります。
オブジェクトを見てTapすると、スケールが増加するインタラクションを確認できます。

3. DragGestureの追加

次に、Dragをすることで3Dコンテンツが回転する処理を実装します。
VolumeView.Swiftを、以下に修正します。

struct VolumeView: View {
    @State private var scale: Float = 1.0
    @State private var rotationAngleX: Float = 0.0
    @State private var rotationAngleY: Float = 0.0
    @State private var lastTranslation: CGSize = .zero
    var body: some View {
        RealityView { content in
            // Generate ModelEntity
            let model = ModelEntity(
                mesh: .generateCone(height: 0.1, radius: 0.1),
                materials: [SimpleMaterial(color: .lightGray, isMetallic: true)]
            )
            
            // Add components
            model.components.set(InputTargetComponent())
            model.components.set(CollisionComponent(shapes: [.generateSphere(radius: 0.1)]))
            
            // Add ModelEntity to RealityView content
            content.add(model)
        } update: { content in
            if let model = content.entities.first {
                model.transform.scale = [scale, scale, scale]
                model.transform.rotation = simd_quatf(angle: rotationAngleY, axis: [0, 1, 0]) * simd_quatf(angle: rotationAngleX, axis: [1, 0, 0])
            }
        }
        .gesture(TapGesture().targetedToAnyEntity().onEnded{ _ in
            scale += 0.1
        })
        .simultaneousGesture(
            DragGesture(minimumDistance: 0)
                .onChanged { value in
                    let translation = value.translation
                    if abs(translation.height) > 0 {
                        rotationAngleX += Float(translation.height - lastTranslation.height) * 0.01
                    }
                    if abs(translation.width) > 0 {
                        rotationAngleY += Float(translation.width - lastTranslation.width) * 0.01
                    }
                    lastTranslation = translation                }
                .onEnded { _ in
                    // ドラッグ終了時に変化量をリセット
                    lastTranslation = .zero
                }
        )
    }

変更点は以下のとおりです。

  • State変数の追加
    • @State private var rotationAngleX: Float = 0.0
    • @State private var rotationAngleY: Float = 0.0
    • @State private var lastTranslation: CGSize = .zero
  • .updateクロージャの修正
    • Dragジェスチャーで取得したrotationAngleを用いて、モデルを回転させる処理を追加
  • .simultaneousGestureイベントの追加
    • Dragジェスチャーの上下/左右の動きに対して、それぞれrotationAngleXとrotationAngleYを割り当てる
    • 既にTapジェスチャーがあるので、非同期でのジェスチャーを有効にする為に.gestureではなく.simultaneousGestureを追加

以下の回転処理のコードが少し特殊なので、補足します。
model.transform.rotation = simd_quatf(angle: rotationAngleY, axis: [0, 1, 0]) * simd_quatf(angle: rotationAngleX, axis: [1, 0, 0])

ここではsimd_quatf(angle:axis)関数を用いてmodelを回転させています。
引数であるangle:では、Float値で回転角度を指定しています。
axis:では回転させる座標軸を指定([0, 1, 0]の場合はY軸周り、[1, 0, 0]の場合はX軸周り)しています。

上記により、DagGestureで取得した上下左右の動きをモデルの回転に変換することで、直接物体をつかんで回転させているようなジェスチャを実現しています。

4. VisionPro動作確認

実行すると、以下のような挙動になります。
Dragしている手の動きに応じて、モデルが回転していることを確認できました。

終わりに

今回はSpatialComputingの入門編Part2として、VisionProにて導入された目および手を用いたインタラクションを実装しました。
今回はシステムが提供するジェスチャーを用いましたが、ARKitを用いることで独自のカスタムジェスチャーを実装することも可能です。ARKitの空間認識機能やアンカー機能を用いるには、今回実装したようなWindowやVolumeではなく、ImmersiveViewでの実装が必要になります。
次回Part3はImmersiveViewの実装方法をご紹介します。

現在、電通総研はweb3領域のグループ横断組織を立ち上げ、web3およびメタバース領域のR&Dを行っております(カテゴリー「3DCG」の記事はこちら)。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください!

私たちは一緒に働いてくれる仲間を募集しています!

電通総研の採用ページ

参考文献

執筆:@yamashita.yuki、レビュー:@handa.kenta
Shodoで執筆されました