- はじめに
- 環境
- Android XRとJetpack XR SDK
- ジオメトリとSkyboxの表示のサンプルアプリ
- ジオメトリ表示
- Skyboxの表示
- Skyboxのzipファイル作成方法
- まとめ
- 補足:SpatialPanelについて
はじめに
この記事は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の基本的な動作は実装できます。