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

Android XR でジオメトリとSkyboxを表示させる方法

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

はじめに

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

こんにちは、NTTコノキューの中矢です。業務では、Unity, C#、最近はKotlinと格闘しています。

本記事では、Android XRで使われるJetpack XR SDKを利用して、Android XRアプリ上でジオメトリSkyboxを表示させるにはどうすればよいかを解説します。

この記事が少しでも、Android XRに挑戦しようとする方々の参考になれば幸いです。

環境

  • MacBook Apple M4

  • Android Studio Narwhal 4 Feature Drop | 2025.1.4 Canary 2

  • Jetpack XR SDK 1.0.0-alpha07

Android XRとJetpack XR SDK

Android XRとはARやVRデバイス(以後、XRデバイス)に搭載されるAndroid OSのことです。スマートフォンのAndroid OSが、XRデバイス用になったものと考えるとわかりやすいです。

Gemini標準搭載がものすごくピックアップされている印象ですが、いちプログラマーとしては、AndroidスマートフォンのNativeアプリのように、KotlinでXRアプリを作ることができることが特徴的かなと思います。

もちろん、Android OSがXRデバイスに搭載されたってだけですので、今まで通りUnityで作ったアプリをAndroidスマートフォンにインストールするのと同様に、Android XRデバイスでもUnityで作ったアプリをインストールできます。本当にただのAndroidスマートフォンのXRデバイス版!みたいな感じです。

ここで、Android XRでKotlinでアプリを作るために、Jetpack XR SDKなるものがアルファ版でリリースされており、本記事ではJetpack XR SDKを利用して、アプリ上にジオメトリ(風景用の3Dモデル)とSkybox(天球の空)を表示させる方法を解説します

ジオメトリとSkyboxの表示のサンプルアプリ

本記事の例は以下です。ジオメトリ表示とSkybox表示をそれぞれ解説します。

(1) ジオメトリ表示(風景の3Dモデル表示)

(2) Skybox表示(天球の空の表示)

ジオメトリ表示

ジオメトリ表示、Skybox表示ともに、フルスペースモードでのみ動きます。そのため、ジオメトリ表示を行うには、以下のようにコンポーザブル関数上で、フルスペースモードとホームスペースモードの条件分岐を行い、フルスペースモードで記述します。

ここでは、公式ドキュメントを参考に、処理を解説します。


@Composable
fun VirtualWorldScreen() {
    val spatialConfiguration = LocalSpatialConfiguration.current
    if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
        // フルスペースモードの画面を記述
        VirtualWorld3DScreen()
    } else {
        // ホームスペースモードの画面を記述
    }
}

また、ホームスペースモードの画面では、フルスペースモードへ移行するボタンを用意します。方法は公式チュートリアルを参考にすると良いです。

VirtualWorld3DScreen()メソッドはコンポーザブル関数で、ここでジオメトリの処理を記述します。


// Volumeを利用するために記述
@OptIn(ExperimentalSubspaceVolumeApi::class)
@Composable
fun VirtualWorld3DScreen(
    modifier: SubspaceModifier = SubspaceModifier
) {
    // Subspace配下に記載
    Subspace {
        val session = checkNotNull(LocalSession.current)
        val scope = rememberCoroutineScope()

        if (!LocalSpatialCapabilities.current.isAppEnvironmentEnabled) return@Subspace

        Volume(
            modifier = modifier
        ) {
            // 並列処理でジオメトリ表示
            scope.launch {
                // ジオメトリモデルの読み込み
                val geometryModel =
                GltfModel.create(session, Path("models/test_room.glb"))
       // ジオメトリの表示
            session.scene.spatialEnvironment.preferredSpatialEnvironment =
                SpatialEnvironmentPreference(null, geometryModel)
            }
        }
    }
}

ジオメトリ表示はVolume内のコルーチン内で記述します。上記の例の以下の記述がジオメトリ表示処理を行っています。


// ジオメトリモデルの読み込み
val geometryModel = GltfModel.create(session,  
    Path("models/test_room.glb"))
// ジオメトリの表示
session.scene.spatialEnvironment.preferredSpatialEnvironment =    
    SpatialEnvironmentPreference(null, geometryModel)

上記のGltfModel.createメソッドでは、assets/modelsフォルダに配置された「test_room.glb」モデルの読み込みを行っています。ここで注意ですが、Android XRではglTF 2.0をサポートしており、3Dモデルは.gltf ファイルまたは .glb ファイルを利用しなければいけません。

また、glbモデルは公式によると役80MBがオススメと記載ありますが、ちょうど80MBくらいですと、私のMacBookではエミュレーターが固まりました。35MB程度であればエミュレーターでも動作したので、80MBギリギリのサイズは控えたほうがいいかもしれません。

GltfModel.createメソッドで読み込みが終わったら、session.scene.spatialEnvironment.preferredSpatialEnvironmentに、SpatialEnvironmentPreference(null, geometryModel)を代入することで3Dモデルを描画します。(第一引数のnullは後述します)

これでジオメトリが表示できるようになります。

Skyboxの表示

Skyboxの表示はジオメトリ表示とほぼ同じ記述で動作します。 変更すべき場所は以下です。


scope.launch {
    // Skyboxの読み込み
    val skybox = ExrImage.createFromZip(session, 
        Path("textures/test_skybox.zip"))
    // Skyboxの適用
    session.scene.spatialEnvironment.preferredSpatialEnvironment =
       SpatialEnvironmentPreference(skybox, null)
}

さきほどのジオメトリ表示で利用したGltfModel.createメソッドがExrImage.createFromZipメソッドに置き換わり、ジオメトリ表示ではSpatialEnvironmentPreferenceの第二引数に値を設定しましたが、Skyboxでは第一引数に値を設定しています。

ここで、SpatialEnvironmentPreferenceの第一引数はSkybox,第二引数がジオメトリであることがわかります。つまり、第一引数と第二引数を両方設定すると、skyboxとジオメトリ両方を設定することができます。

また、ExrImage.createFromZipメソッドでは、assets/texturesフォルダ配下のtest_skybox.zipファイルを読み込んでいます。

ここでskyboxの画像はzipファイルにしなくてはならず、方法は次章で説明します。

以上が、ジオメトリとSkyboxをそれぞれ描画する方法です。

Skyboxのzipファイル作成方法

Skyboxを設定するときに、Skyboxの画像はzipファイルにする必要があります。本章では公式ドキュメントを参考にzipファイルの作成方法を解説します。

サンプルで利用するEXR画像はここで手に入れています。 また、公式ドキュメントに記載がありますが、EXR画像の解像度は1,024 x 512(アスペクト比は 2:1 で、サイズは 2 のべき乗である必要がある)くらいが良いようです。

まずzipファイル作成をするツールである「cmgen」を手に入れます。 これは、Filamentリポジトリから手に入れます。 Macbookの場合は「filament-v1.67.0-mac.tgz」をダウンロードすると良いです。

ダウンロードすると、「filament/bin/」配下にcmgenがあります。

公式では、追加で真っ黒なpng画像を用意してzipファイルを作成していますが、今回は真っ黒な画像を用意せずにzipファイルを作成します。

「filament/bin/」配下のフォルダに利用したいEXR画像を配置し、以下コマンドを叩きます。ここでEXR画像のファイル名は「test_skybox.exr」とし、出力するzipファイルは「test_skybox.zip」とします。


# SkyboxのIBLファイルの作成
./cmgen --format=rgb32f --size=128 --deploy=./skybox_ibl --ibl-ld=. --ibl-samples=1024 --extract-blur=0.0 --sh-irradiance --sh-shader --sh-output=./skybox_ibl/sh.txt ./test_skybox.exr

cd ./skybox_ibl

# IBLファイルのzip化
zip -q test_skybox.zip test_skybox/*

cd ..

上記コマンドで作成したzipファイルをJetpack XR SDKで以下のように読み込めば、Skyboxの表示ができます。


// Skyboxの読み込み
val skybox = ExrImage.createFromZip(session, 
    Path("textures/test_skybox.zip"))

まとめ

本記事では、Android XR開発で利用するJetpack XR SDKを用いて、ジオメトリとSkyboxをフルスペースモードで表示する方法を解説しました。

Skyboxのzip素材を用意するのが少しわかりにくいですが、それを突破できれば、比較的簡単に風景の表示ができるようになるかなと思います。

ただ現状は機能が少なく、Unityで作ったアプリのようにガッツリVRを作ることは難しそうというのが所感ですが、今後のJetpack XR SDKの発展に期待です。

補足:SpatialPanelについて

本サンプルアプリでは「綺麗な景色ですね!」と書かれたパネルが表示されていますが、これはJetpack XRで追加されたSpatialPanelと呼ばれるものです。

使い方は公式チュートリアルに記載がありますが、本記事でも少しだけ触れようと思います。

SpatialPanelを配置するには、以下のようにSubspace内に書く必要があります。SubspaceとはSpatialPanelなどの空間UIや3Dモデルを置くためのパーティションです。


Subspace {
        SpatialRow(
            alignment = SpatialAlignment.Center,
            modifier = SubspaceModifier
        ) {
            SpatialPanel(
                dragPolicy = MovePolicy(),
                resizePolicy = ResizePolicy(),
                modifier = SubspaceModifier
                    .height(700.dp)
                    .width(900.dp)
            ) {
                // パネルの内容を記述する
            }

SubspaceだけでもSpatialPanelは置けますが、本記事ではSpatialRowを使っています。これは普通のJetpack ComposeでのRow()の空間UI版です。(もちろんSpatialColumnもあります)

また、普通のJetpack ComposeではModifierになるところが、SubspaceModifierになっているため、注意してください。これを使うことで、Subspace内の空間UIの位置決め、回転、動作を設定できます。

続いて、SpatialPanelでは先述のSubspaceModifierだけでなく、dragPolicyとresizePolicyを設定できます。本記事では以下のように設定しています。


SpatialPanel(
    dragPolicy = MovePolicy(),
    resizePolicy = ResizePolicy(),
    modifier = SubspaceModifier
                    .height(700.dp)
                    .width(900.dp)
    ) {
        // パネルの内容を記述する
    }

ここで、dragPolicyはSpatialPanelをドラッグで移動できるかどうかの設定、resizePolicyはSpatialPanelを拡大縮小できるかの設定です。本設定を加えることで、SpatialPanelの基本的な動作は実装できます。