NTTドコモR&Dの技術ブログです。

Android XR取り組み紹介(3Dモデルのインタラクティブな操作について)

※ 本記事は 2026/3/31 以前にNTTコノキューにて記載した記事になります

0.はじめに

この記事はNTTコノキュー アドベントカレンダー2025の記事です。

こんにちは。NTTコノキューの乗松です。

本記事はVolumeに配置された3Dモデルのインタラクティブな操作の実装についての解説になります。

1.開発環境

  • MacBook Apple M4
  • Android Studio Narwhal 4 Feature Drop | 2025.1.4 Canary 2
  • Jetpack XR SDK 1.0.0-alpha07

2.アプリ紹介

添付のようなパネルと3Dモデルを組み合わせたアプリを作成しました。

パネルと3Dモデルは連動していて、ページに合わせて3Dモデルが切り替わります。

このアプリの実装の中で、3Dモデルのクリック・回転・移動の操作の実装を行ったので紹介していこうと思います。

3Dモデルの表示に関しては別記事にて解説しているので、そちらを参考にしてください。

3.3Dモデルのクリック

3Dモデルをクリックできるようにする実装を紹介します。

少し分かりづらいですが、3Dモデルをクリックするとパネルのページが切り替わっています。

下記Volumeの実装になります。

Volume(
        modifier = modifier
            .scale(0.1f)
            .offset(x = 50.dp, z = 10.dp)
    ) { parent ->
        scope.launch {
            // 3Dモデルを配置
            val training3dModel = GltfModel.create(session, Paths.get(modelName))
            val entity = GltfModelEntity.create(session, training3dModel)
            val modelPosition = Vector3(0f, 0f, 0f)

            // modelEntityStateに関しては後述の回転の実装で使用します
            modelEntityState.value = entity

            entity.setPose(Pose(modelPosition))

       // 押下できるよう実装
            val executor = Dispatchers.Default.asExecutor()
            val interactableComponent = InteractableComponent.create(session, executor) { event ->
                if (event.action == InputEvent.Action.ACTION_UP) {

                    // ここで押下した時の処理を記述
                    viewModel.onModelClicked()
                }
            }
            entity.addComponent(interactableComponent)
            parent.addChild(entity)
        }
    }

上記に長く書いていますが、クリック操作の追加で大事なのはInteractableComponentの部分です。

InteractableComponentを使用すると、ユーザーの入力イベントをキャプチャすることができます。

InputEvent.Action.ACTION_UPを指定することで、ユーザーの指が離された瞬間=クリックされた時の処理を実装することができます。

InputEventを変更すれば、右手左手をキャプチャできたりするそうです(エミュレータでは難しいですが)。InputEventの細かい種類を知りたい方は公式ドキュメントを参照してください。

4.3Dモデルの移動

続いて、3Dモデルを移動させる実装を紹介します。

クリック追加で紹介したコードのInteractableComponentの実装部分を下記に変更することで実装することができます。

val movableComponent = MovableComponent.createSystemMovable(session, scaleInZ = false)
entity.addComponent(movableComponent)

オプションのscaleInZに関して、trueにするとユーザーからの距離に対して自動でリサイズされるようになるみたいです。(デフォルトはtrue)

scaleInZをtrueにすると、3Dモデルを動かした時にサイズが突然巨大になったりしたのでfalseにしています。

ここで注意ですが、クリック(InteractableComponent)と移動(MovableComponent)をそのまま両方実装するとクリックが優先されて移動できなくなりました。入力イベントが競合していることが原因だと思うので、上手く制御できれば解決するかもしれません。

5.3Dモデルの回転

最後に、モデルの回転について紹介します。

左右の回転ボタンを設置し、90度ずつ回転させるような実装を行いました。

今回は段階に分けて紹介していこうと思います。

5.1 ボタンの追加

RotationPanelという@Composable関数を作成しました。

色やアイコンは好きなようにカスタマイズしてください。

onRotateLeftやonRotateRightはそれぞれのボタンが押下された時のメソッドを設定します。

@Composable
fun RotationPanel(
    onRotateLeft: () -> Unit,
    onRotateRight: () -> Unit,
    modifier: SubspaceModifier = SubspaceModifier,
) {
    SpatialPanel(
        modifier = modifier
    ) {
       // 背景の設定
        Surface(
            color = , // 白を指定
            modifier = Modifier
                .fillMaxSize()
                .padding(8.dp)
                .clip(CircleShape)
        ) {
            // ボタンの配置設定
            Row(
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.Center,
                modifier = Modifier.fillMaxSize()
            ) {
                // 
                IconButton(onClick = onRotateLeft) {
                    Icon(
                        imageVector = Icons.AutoMirrored.Filled.KeyboardArrowLeft,
                        contentDescription = "Rotate Left",
                        tint = // 色を指定
                    )
                }
                IconButton(onClick = onRotateRight) {
                    Icon(
                        imageVector = // アイコンを指定
                        contentDescription = "Rotate Right",
                        tint = // 色を指定
                    )
                }
            }
        }
    }
}

5.2 左右ボタンが押された時の処理を追加

まず、RotationEventというクラスを定義します。

sealed class RotationEvent {
    object RotateLeft : RotationEvent()
    object RotateRight : RotationEvent()
}

上記のRotationEventクラスを利用してrotationEvent変数を監視できるようにします。

private val _rotationEvent = MutableSharedFlow<RotationEvent>()
val rotationEvent = _rotationEvent.asSharedFlow()

左右のボタンが押された時の処理を実装するメソッドを用意します。

fun onRotateLeft() {
    viewModelScope.launch {
        _rotationEvent.emit(RotationEvent.RotateLeft)
    }
}

fun onRotateRight() {
    viewModelScope.launch {
        _rotationEvent.emit(RotationEvent.RotateRight)
    }
}

5.3 3Dモデルの回転を実装

まず、ROTATION_RIGHTとROTATION_LEFTという変数を定義して、90度ずつ回転させるようにします。ここで角度変更や縦回転にもさせることができます。

private val ROTATION_RIGHT = Quaternion.fromEulerAngles(0f, 90f, 0f)
private val ROTATION_LEFT = Quaternion.fromEulerAngles(0f, -90f, 0f)

最後に下記の実装を行うことで完成です。

簡潔に説明すると現在の位置と向きを取得して、そこから新しい位置と向きを計算してsetPoseで3Dモデルを更新するという流れになります。

// 3Dモデルの状態を保持
val modelEntityState = remember { MutableStateFlow<GltfModelEntity?>(null) }
val modelEntity by modelEntityState.collectAsState()

// ViewModelからの回転イベントを監視しモデルを回転させる
LaunchedEffect(modelEntity) {
    val entity = modelEntity?: return@LaunchedEffect
    viewModel.rotationEvent.collect { event ->
        // 現在の位置と向きを取得
        val currentPose = entity.getPose()

        val rotationDelta = when (event) {
            is RotationEvent.RotateLeft -> ROTATION_LEFT
            is RotationEvent.RotateRight -> ROTATION_RIGHT
        }

        // 現在の向きから最終的な向きを計算
        val newOrientation = rotationDelta.times(currentPose.rotation)

        // 計算した新しい向きで姿勢を更新
        entity.setPose(Pose(currentPose.translation, newOrientation))
    }
}

6.まとめ

今回は3Dモデルのインタラクティブな操作(クリック、移動、回転)の実装を紹介しました。

3Dモデルの基本的な操作に関しては割と簡単に実装できるような感想を持ちました。

ですが現段階ではAppleVisionProの方が3Dモデル全般の実装は充実している印象なので、これからのアップデートに期待です。