2024/12/24
Android XR アプリ開発 | Scene Viewer による3Dモデル表示の構築
- はじめに
- 事前準備
- プロジェクト作成
- XR : Basic Headset Activity
- 入力情報
- 実装
- Scene Viewer の 処理
- Scene Viewer の呼び出しボタン
- Scene Viewer の呼び出しボタンを配置
- 全ソースコード
- ビルド
- bundle.gradle.kts(Module :app)の修正
- Clean Project
- Sync Project with Gradle Files
- 実行
- まとめ
- 作成したプロジェクト
- 参考情報
- Android XRとは|できること、デバイス、開発ツールなど解説
- Android XR 技術ブログ
- 空間コンピューティングやARアプリ開発に対する弊社の取り組み
- お問い合わせ
- https://1planet.co.jp/contact
はじめに
今回は、Scene Viewer の機能を使って3Dモデルを表示するAndroid XRアプリのサンプルアプリを作成します。
(この記事は、Android XR Advent Calendar 2024 の25日目です。)
事前準備
Android StudioによるAndroid XRアプリ開発の環境構築を事前に実施しておいてください。
https://1planet.co.jp/tech-blog/android-xr-oneplanet-241214-1-pre-start-develop
プロジェクト作成
XR : Basic Headset Activity
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-1798x1364_v-fms_webp_0e6ddf67-be5c-4ae0-bce9-96b88b892ef9.png)
プロジェクトを作成するテンプレートはXRのBasic Headset Activityになります。選択後、Nextボタンをクリックします。
入力情報
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-1804x1358_v-frms_webp_c0482887-b521-421b-8d21-077c4dc787ea.png)
Name、Package name、Save locationは上記以外の情報でも問題ありません。入力完了後、Finishボタンをクリックします。
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-1804x1360_v-frms_webp_b18e0a32-4ef0-4577-848b-47f75d418fec.png)
プロジェクト作成完了後、Finishボタンをクリックします。
実装
MainActivity.ktのソースコードを修正します。
Scene Viewer の 処理
class MainActivity : ComponentActivity() {
@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
OnePlanetAndroidXRSceneViewerSampleTheme {
val session = LocalSession.current
if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
Subspace {
MySpatialContent(onRequestHomeSpaceMode = { session?.requestHomeSpaceMode() })
}
} else {
My2DContent(onRequestFullSpaceMode = { session?.requestFullSpaceMode() })
}
}
}
}
fun launchSceneViewer() {
val THREED_MODEL_URL = "https://modelviewer.dev/shared-assets/models/RobotExpressive.glb"
val MIME_TYPE = "model/gltf-binary"
val sceneViewerIntent = Intent(Intent.ACTION_VIEW)
val intentUri = Uri.parse("https://arvr.google.com/scene-viewer/1.2")
.buildUpon()
.appendQueryParameter("file", THREED_MODEL_URL)
.build()
sceneViewerIntent.setDataAndType(intentUri, MIME_TYPE)
startActivity(sceneViewerIntent)
}
}
MainActivity に launchSceneViewer というメソッドを追加します。launchSceneViewer内でGLBファイルの3Dモデルを読み込み、Scene Viewer のインテントを作成後、startActivityにセットします。(モデルは以下を使用しました。)
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-1082x1552_v-fms_webp_b7577a84-4dc3-4065-92b0-40b7f795bb93.png)
https://modelviewer.dev/examples/animation/index.html
Scene Viewer の呼び出しボタン
@Composable
fun SceneViewerButton() {
val context = LocalContext.current
Button(onClick = {
(context as? MainActivity)?.launchSceneViewer()
}) {
Text(text = "3Dモデル表示")
}
}
SceneViewerButtonのコードを追加します。
Scene Viewer の呼び出しボタンを配置
@Composable
fun MainContent(modifier: Modifier = Modifier) {
Box(
Modifier
.background(color = Color.LightGray)
.height(1280.dp)
.width(800.dp),
contentAlignment = Alignment.Center
) {
SceneViewerButton()
}
}
テンプレートが自動生成したMainContentの中身を変更します。先程、実装したSceneViewerButtonを配置します。
全ソースコード
package jp.co.oneplanet.androidxr.sample.oneplanetandroidxrsceneviewersample
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.Button
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp
import androidx.xr.compose.platform.LocalHasXrSpatialFeature
import androidx.xr.compose.platform.LocalSession
import androidx.xr.compose.platform.LocalSpatialCapabilities
import androidx.xr.compose.spatial.Subspace
import androidx.xr.compose.subspace.SpatialPanel
import androidx.xr.compose.subspace.layout.SubspaceModifier
import androidx.xr.compose.subspace.layout.height
import androidx.xr.compose.subspace.layout.movable
import androidx.xr.compose.subspace.layout.resizable
import androidx.xr.compose.subspace.layout.width
import jp.co.oneplanet.androidxr.sample.oneplanetandroidxrsceneviewersample.ui.theme.OnePlanetAndroidXRSceneViewerSampleTheme
class MainActivity : ComponentActivity() {
@SuppressLint("RestrictedApi")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
OnePlanetAndroidXRSceneViewerSampleTheme {
val session = LocalSession.current
if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
Subspace {
MySpatialContent(onRequestHomeSpaceMode = { session?.requestHomeSpaceMode() })
}
} else {
My2DContent(onRequestFullSpaceMode = { session?.requestFullSpaceMode() })
}
}
}
}
fun launchSceneViewer() {
val THREED_MODEL_URL = "https://modelviewer.dev/shared-assets/models/RobotExpressive.glb"
val MIME_TYPE = "model/gltf-binary"
val sceneViewerIntent = Intent(Intent.ACTION_VIEW)
val intentUri = Uri.parse("https://arvr.google.com/scene-viewer/1.2")
.buildUpon()
.appendQueryParameter("file", THREED_MODEL_URL)
.build()
sceneViewerIntent.setDataAndType(intentUri, MIME_TYPE)
startActivity(sceneViewerIntent)
}
}
@SuppressLint("RestrictedApi")
@Composable
fun MySpatialContent(onRequestHomeSpaceMode: () -> Unit) {
SpatialPanel(SubspaceModifier.width(1280.dp).height(800.dp).resizable().movable()) {
Surface {
MainContent(
modifier = Modifier
.fillMaxSize()
.padding(48.dp)
)
}
}
}
@SuppressLint("RestrictedApi")
@Composable
fun My2DContent(onRequestFullSpaceMode: () -> Unit) {
Surface {
Row(
modifier = Modifier.fillMaxSize(),
horizontalArrangement = Arrangement.SpaceBetween
) {
MainContent(modifier = Modifier.padding(48.dp))
if (LocalHasXrSpatialFeature.current) {
FullSpaceModeIconButton(
onClick = onRequestFullSpaceMode,
modifier = Modifier.padding(32.dp)
)
}
}
}
}
@Composable
fun MainContent(modifier: Modifier = Modifier) {
Box(
Modifier
.background(color = Color.LightGray)
.height(1280.dp)
.width(800.dp),
contentAlignment = Alignment.Center
) {
SceneViewerButton()
}
}
@Composable
fun SceneViewerButton() {
val context = LocalContext.current
Button(onClick = {
(context as? MainActivity)?.launchSceneViewer()
}) {
Text(text = "3Dモデル表示")
}
}
@Composable
fun FullSpaceModeIconButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
IconButton(onClick = onClick, modifier = modifier) {
Icon(
painter = painterResource(id = R.drawable.ic_full_space_mode_switch),
contentDescription = stringResource(R.string.switch_to_full_space_mode)
)
}
}
@Composable
fun HomeSpaceModeIconButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
FilledTonalIconButton(onClick = onClick, modifier = modifier) {
Icon(
painter = painterResource(id = R.drawable.ic_home_space_mode_switch),
contentDescription = stringResource(R.string.switch_to_home_space_mode)
)
}
}
@PreviewLightDark
@Composable
fun My2dContentPreview() {
OnePlanetAndroidXRSceneViewerSampleTheme {
My2DContent(onRequestFullSpaceMode = {})
}
}
@Preview(showBackground = true)
@Composable
fun FullSpaceModeButtonPreview() {
OnePlanetAndroidXRSceneViewerSampleTheme {
FullSpaceModeIconButton(onClick = {})
}
}
@PreviewLightDark
@Composable
fun HomeSpaceModeButtonPreview() {
OnePlanetAndroidXRSceneViewerSampleTheme {
HomeSpaceModeIconButton(onClick = {})
}
}
ビルド
bundle.gradle.kts(Module :app)の修正
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-2400x1669_v-frms_webp_0e105fa3-2cd9-4326-bfc0-41154573c2cf.png)
minSdk = 34 に修正します。
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "jp.co.oneplanet.androidxr.sample.oneplanetandroidxrsceneviewersample"
compileSdk = 35
defaultConfig {
applicationId = "jp.co.oneplanet.androidxr.sample.oneplanetandroidxrsceneviewersample"
minSdk = 34
targetSdk = 35
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.runtime)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.compose)
implementation(libs.runtime)
implementation(libs.androidx.scenecore)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
Clean Project
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-1966x884_v-frms_webp_36e7e569-b226-406a-b81a-ebbf662d1b8f.png)
Clean Projectを実行します。
Sync Project with Gradle Files
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-2400x257_v-frms_webp_a38266c0-765b-45b2-9dd5-b4c062de2838.png)
Sync Project with Gradle Filesを実行します。
実行
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-750x276_v-fs_webp_f988121f-79c3-40cb-804c-c26e3301d02c.png)
XR Deviceを選択した状態で▷(Run 'app')を選択するとAndroid XR エミュレーターが起動します。
3Dモデル表示ボタンをクリックするとScene Viewerに切り替わり、GLBファイルの3Dモデルが表示されます。
まとめ
今回は、Scene Viewerを利用して3Dモデルを表示するAndroid XRアプリのサンプルを作成する手順を解説しました。基本的なプロジェクト作成から、Scene Viewerを呼び出すボタンの実装まで、具体的なコード例を交えて解説しました。
Scene Viewerは、簡単なインテント呼び出しでGLB形式の3Dモデルを表示できる強力なツールです。今回のサンプルを参考に、より魅力的なXR体験を提供するアプリを開発してください。
作成したプロジェクト
作成したプロジェクトはgithubにあります。
https://github.com/tokufxug/OnePlanetAndroidXRSceneViewerSample
参考情報
Android XRとは|できること、デバイス、開発ツールなど解説
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-1654x1174_v-fms_webp_477587b0-db78-46c5-b2ec-1f503ede42b4.webp)
Android XR の情報について以下の記事をご参照ください。
https://ar-marketing.jp/android-xr-what-it-can-be-used-for-devices-tools/
Android XR 技術ブログ
https://1planet.co.jp/tech-blog/category/AndroidXR
空間コンピューティングやARアプリ開発に対する弊社の取り組み
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-2400x1809_v-frms_webp_4cad6005-c4dd-423b-87ee-7d02a5dd5e11_regular.webp)
空間コンピューティングやARアプリ開発の取り組みについては、こちらをご覧ください。
https://1planet.co.jp/work/consulting
お問い合わせ
AR関するご依頼・ご相談など、お気軽にお問い合わせください。
![](https://storage.googleapis.com/studio-cms-assets/projects/Kwa5KvdJOX/s-2400x661_v-frms_webp_f90b480e-4115-4084-a237-a48250c83f25_middle.webp)
https://1planet.co.jp/contact
XR エンジニア
徳山 禎男
SIerとして金融や飲料系など様々な大規模プロジェクトに参画後、2020年にOnePlanetに入社。ARグラスを中心とした最先端のAR技術のR&Dや、法人顧客への技術提供を担当。過去にMagic Leap 公式アンバサダーを歴任。
View More