From 78bf47273bc6a4d2aefdb540539a2d98eb2e89c2 Mon Sep 17 00:00:00 2001 From: azeppenfeld Date: Fri, 24 Oct 2025 16:25:55 -0700 Subject: [PATCH 1/2] Squashed commit of the following: Author: azeppenfeld Date: Fri Oct 24 15:21:29 2025 -0700 Updating for best practices Author: azeppenfeld Date: Thu Oct 16 18:48:28 2025 -0700 Cleaning up viewmodel Author: azeppenfeld Date: Mon Oct 13 14:02:50 2025 -0700 Added ability to change material colors and other properties Author: azeppenfeld Date: Thu Oct 2 18:30:49 2025 -0700 Added ability to rotate, scale, and change the offset of a GLTF Author: azeppenfeld Date: Wed Oct 1 13:26:55 2025 -0700 upgrading to alpha07 and a few other minor changes Adding bugdroid controller Author: azeppenfeld Date: Fri Sep 19 13:23:11 2025 -0700 Added start/stop animation when BugDroid is shown Change-Id: I8101c77c5510413074f57c3fc12261d3445ac067 --- .../example/helloandroidxr/MainActivity.kt | 1 - .../bugdroid/BugdroidController.kt | 78 ++++ .../helloandroidxr/ui/HelloAndroidXRApp.kt | 208 +++++++-- .../ui/components/BugdroidControls.kt | 112 +++++ .../ui/components/BugdroidModel.kt | 138 +++--- .../ui/components/BugdroidSliderControls.kt | 431 ++++++++++++++++++ .../ui/{ => components}/TextPane.kt | 2 +- .../viewmodel/BugdroidViewModel.kt | 205 +++++++++ app/src/main/res/values/strings.xml | 32 +- gradle/libs.versions.toml | 10 +- 10 files changed, 1105 insertions(+), 112 deletions(-) create mode 100644 app/src/main/java/com/example/helloandroidxr/bugdroid/BugdroidController.kt create mode 100644 app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidControls.kt create mode 100644 app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidSliderControls.kt rename app/src/main/java/com/example/helloandroidxr/ui/{ => components}/TextPane.kt (96%) create mode 100644 app/src/main/java/com/example/helloandroidxr/viewmodel/BugdroidViewModel.kt diff --git a/app/src/main/java/com/example/helloandroidxr/MainActivity.kt b/app/src/main/java/com/example/helloandroidxr/MainActivity.kt index 2c4ba2d..af2dec0 100644 --- a/app/src/main/java/com/example/helloandroidxr/MainActivity.kt +++ b/app/src/main/java/com/example/helloandroidxr/MainActivity.kt @@ -24,7 +24,6 @@ import com.example.helloandroidxr.ui.HelloAndroidXRApp import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme class MainActivity : ComponentActivity() { - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() diff --git a/app/src/main/java/com/example/helloandroidxr/bugdroid/BugdroidController.kt b/app/src/main/java/com/example/helloandroidxr/bugdroid/BugdroidController.kt new file mode 100644 index 0000000..a23c04d --- /dev/null +++ b/app/src/main/java/com/example/helloandroidxr/bugdroid/BugdroidController.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.helloandroidxr.bugdroid + +import android.annotation.SuppressLint +import android.content.Context +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.xr.runtime.Session +import androidx.xr.scenecore.GltfModel +import com.example.helloandroidxr.R +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.io.InputStream + +class BugdroidController( + private val xrSession: Session?, + private val context: Context, + private val coroutineScope: CoroutineScope +) { + var gltfModel by mutableStateOf(null) + + init { + loadBugdroidModel() + } + + private fun loadBugdroidModel() { + coroutineScope.launch { + gltfModel = BugdroidGltfModelCache.getOrLoadModel(xrSession, context) + } + } +} + +private object BugdroidGltfModelCache { + private var cachedModel: GltfModel? = null + @SuppressLint("RestrictedApi") + suspend fun getOrLoadModel( + xrCoreSession: Session?, context: Context + ): GltfModel? { + return if (cachedModel == null) { + try { + val inputStream: InputStream = + context.resources.openRawResource(R.raw.bugdroid_animated_wave) + cachedModel = GltfModel.create( + xrCoreSession!!, inputStream.readBytes(), "BUGDROID" + ) + cachedModel + } catch (e: Exception) { + Log.e(TAG, "Error loading GLTF model", e) + null + } + } else { + cachedModel + } + } + + fun clearCache() { + cachedModel = null + } + + const val TAG = "BugdroidGltfModelCache" +} \ No newline at end of file diff --git a/app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt b/app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt index a0676ab..fd729d4 100644 --- a/app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt +++ b/app/src/main/java/com/example/helloandroidxr/ui/HelloAndroidXRApp.kt @@ -43,11 +43,9 @@ import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -59,9 +57,12 @@ import androidx.compose.ui.unit.dp import androidx.window.core.layout.WindowSizeClass import androidx.window.core.layout.WindowWidthSizeClass import androidx.xr.compose.platform.LocalSpatialCapabilities +import androidx.xr.compose.platform.LocalSpatialConfiguration import androidx.xr.compose.spatial.ContentEdge import androidx.xr.compose.spatial.Orbiter import androidx.xr.compose.spatial.Subspace +import androidx.xr.compose.subspace.MovePolicy +import androidx.xr.compose.subspace.ResizePolicy import androidx.xr.compose.subspace.SpatialColumn import androidx.xr.compose.subspace.SpatialPanel import androidx.xr.compose.subspace.SpatialRow @@ -70,34 +71,87 @@ import androidx.xr.compose.subspace.layout.alpha import androidx.xr.compose.subspace.layout.fillMaxSize import androidx.xr.compose.subspace.layout.fillMaxWidth import androidx.xr.compose.subspace.layout.height -import androidx.xr.compose.subspace.layout.movable import androidx.xr.compose.subspace.layout.offset import androidx.xr.compose.subspace.layout.padding -import androidx.xr.compose.subspace.layout.resizable +import androidx.xr.compose.subspace.layout.rotate import androidx.xr.compose.subspace.layout.size import androidx.xr.compose.subspace.layout.width +import androidx.xr.runtime.math.Quaternion import com.example.helloandroidxr.R +import com.example.helloandroidxr.ui.components.BugdroidControls import com.example.helloandroidxr.ui.components.BugdroidModel +import com.example.helloandroidxr.ui.components.BugdroidSliderControls import com.example.helloandroidxr.ui.components.EnvironmentControls import com.example.helloandroidxr.ui.components.SearchBar +import com.example.helloandroidxr.ui.components.TextPane import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme +import com.example.helloandroidxr.viewmodel.BugdroidUiState +import com.example.helloandroidxr.viewmodel.BugdroidViewModel +import com.example.helloandroidxr.viewmodel.ModelMaterialColor +import com.example.helloandroidxr.viewmodel.ModelMaterialProperties +import com.example.helloandroidxr.viewmodel.ModelOffset +import com.example.helloandroidxr.viewmodel.ModelRotation +import com.example.helloandroidxr.viewmodel.SliderGroup import kotlinx.coroutines.launch @Composable fun HelloAndroidXRApp() { + val viewModel = BugdroidViewModel() + val uiState by viewModel.uiState.collectAsState() if (LocalSpatialCapabilities.current.isSpatialUiEnabled) { SpatialLayout( - primaryContent = { PrimaryContent() }, - firstSupportingContent = { BlockOfContentOne() }, - secondSupportingContent = { BlockOfContentTwo() } + primaryContent = { + PrimaryContent( + uiState = uiState, + onShowBugdroidToggle = viewModel::updateShowBugdroid, + onAnimateBugdroidToggle = viewModel::updateAnimateBugdroid + ) + }, + firstSupportingContent = { + BlockOfContentOne( + showBugdroid = uiState.showBugdroid, + onSliderGroupSelected = viewModel::updateShownSliderGroup, + onResetModel = viewModel::resetModel + ) + }, + secondSupportingContent = { + BlockOfContentTwo( + uiState = uiState, + showBugdroid = uiState.showBugdroid, + onScaleChange = viewModel::updateScale, + onRotationChange = viewModel::updateRotation, + onOffsetChange = viewModel::updateOffset, + onMaterialColorChange = viewModel::updateMaterialColor, + onMaterialPropertiesChange = viewModel::updateMaterialProperties + ) + } ) } else { NonSpatialTwoPaneLayout( secondaryPane = { - BlockOfContentOne() - BlockOfContentTwo() + BlockOfContentOne( + modifier = Modifier.height(240.dp), + showBugdroid = uiState.showBugdroid, + onSliderGroupSelected = viewModel::updateShownSliderGroup, + onResetModel = viewModel::resetModel + ) + BlockOfContentTwo( + uiState = uiState, + showBugdroid = uiState.showBugdroid, + onScaleChange = viewModel::updateScale, + onRotationChange = viewModel::updateRotation, + onOffsetChange = viewModel::updateOffset, + onMaterialColorChange = viewModel::updateMaterialColor, + onMaterialPropertiesChange = viewModel::updateMaterialProperties + ) }, - primaryPane = { PrimaryContent() } + primaryPane = { + PrimaryContent( + uiState = uiState, + onShowBugdroidToggle = viewModel::updateShowBugdroid, + onAnimateBugdroidToggle = viewModel::updateAnimateBugdroid + ) + } ) } } @@ -127,18 +181,18 @@ private fun SpatialLayout( SubspaceModifier .alpha(animatedAlpha.value) .size(400.dp) - .padding(bottom = 16.dp) - .movable() - .resizable() + .padding(bottom = 16.dp), + dragPolicy = MovePolicy(isEnabled = true), + resizePolicy = ResizePolicy(isEnabled = true) ) { firstSupportingContent() } SpatialPanel( SubspaceModifier .alpha(animatedAlpha.value) - .weight(1f) - .movable() - .resizable() + .weight(1f), + dragPolicy = MovePolicy(isEnabled = true), + resizePolicy = ResizePolicy(isEnabled = true) ) { secondSupportingContent() } @@ -147,9 +201,9 @@ private fun SpatialLayout( modifier = SubspaceModifier .alpha(animatedAlpha.value) .fillMaxSize() - .padding(left = 16.dp) - .movable() - .resizable() + .padding(left = 16.dp), + dragPolicy = MovePolicy(isEnabled = true), + resizePolicy = ResizePolicy(isEnabled = true) ) { Column { TopAppBar() @@ -279,29 +333,63 @@ private fun TopAppBar() { } @Composable -private fun PrimaryContent(modifier: Modifier = Modifier) { - var showBugdroid by rememberSaveable { mutableStateOf(false) } - val stringResId = if (showBugdroid) R.string.hide_bugdroid else R.string.show_bugdroid - +private fun PrimaryContent( + uiState: BugdroidUiState, + onShowBugdroidToggle: () -> Unit, + onAnimateBugdroidToggle: () -> Unit, + modifier: Modifier = Modifier, +) { if (LocalSpatialCapabilities.current.isSpatialUiEnabled) { + val showStringResId = + if (uiState.showBugdroid) R.string.hide_bugdroid else R.string.show_bugdroid + val animateStringResId = + if (uiState.animateBugdroid) R.string.stop_animation_bugdroid else R.string.animate_bugdroid + val modelTransform = uiState.modelTransform Surface(modifier.fillMaxSize()) { - Box(modifier.padding(48.dp), contentAlignment = Alignment.Center) { - Button( - onClick = { - showBugdroid = !showBugdroid - }, - modifier = modifier - ) { - Text( - text = stringResource(id = stringResId), - style = MaterialTheme.typography.labelLarge - ) + Column(modifier.padding(48.dp), horizontalAlignment = Alignment.CenterHorizontally) { + Box(modifier.padding(48.dp), contentAlignment = Alignment.Center) { + Button( + onClick = onShowBugdroidToggle, + modifier = modifier + ) { + Text( + text = stringResource(id = showStringResId), + style = MaterialTheme.typography.labelLarge + ) + } + } + Box(modifier.padding(48.dp), contentAlignment = Alignment.Center) { + if (uiState.showBugdroid) { + Button( + onClick = onAnimateBugdroidToggle, + modifier = modifier + ) { + Text( + text = stringResource(id = animateStringResId), + style = MaterialTheme.typography.labelLarge + ) + } + } } BugdroidModel( - showBugdroid = showBugdroid, + modelTransform = modelTransform, + showBugdroid = uiState.showBugdroid, + animateBugdroid = uiState.animateBugdroid, modifier = SubspaceModifier .fillMaxSize() - .offset(z = 400.dp) // Relative position from the panel + .rotate( + Quaternion( + x = modelTransform.rotation.x, + y = modelTransform.rotation.y, + z = modelTransform.rotation.z, + w = modelTransform.rotation.w + ) + ) + .offset( + x = modelTransform.offset.x.dp, + y = modelTransform.offset.y.dp, + z = modelTransform.offset.z.dp // Relative position from the panel + ) ) } } @@ -314,13 +402,51 @@ private fun PrimaryContent(modifier: Modifier = Modifier) { } @Composable -private fun BlockOfContentOne(modifier: Modifier = Modifier) { - TextPane(stringResource(R.string.block_of_content_1), modifier = modifier.height(240.dp)) +private fun BlockOfContentOne( + modifier: Modifier = Modifier, + showBugdroid: Boolean, + onSliderGroupSelected: (SliderGroup) -> Unit, + onResetModel: () -> Unit +) { + if (LocalSpatialConfiguration.current.hasXrSpatialFeature && showBugdroid) { + BugdroidControls( + onSliderGroupSelected = onSliderGroupSelected, + onResetModel = { + onResetModel() + onSliderGroupSelected(SliderGroup.NONE) + }, + modifier = modifier + ) + } else { + TextPane(stringResource(R.string.block_of_content_1), modifier = modifier.fillMaxHeight()) + } } @Composable -private fun BlockOfContentTwo(modifier: Modifier = Modifier) { - TextPane(stringResource(R.string.block_of_content_2), modifier = modifier.fillMaxHeight()) +private fun BlockOfContentTwo( + modifier: Modifier = Modifier, + uiState: BugdroidUiState, + showBugdroid: Boolean, + onScaleChange: (Float) -> Unit, + onRotationChange: (ModelRotation) -> Unit, + onOffsetChange: (ModelOffset) -> Unit, + onMaterialColorChange: (ModelMaterialColor) -> Unit, + onMaterialPropertiesChange: (ModelMaterialProperties) -> Unit, +) { + if (LocalSpatialConfiguration.current.hasXrSpatialFeature && showBugdroid) { + BugdroidSliderControls( + visibleSliderGroup = uiState.visibleSliderGroup, + modelTransform = uiState.modelTransform, + onScaleChange = onScaleChange, + onRotationChange = onRotationChange, + onOffsetChange = onOffsetChange, + onMaterialColorChange = onMaterialColorChange, + onMaterialPropertiesChange = onMaterialPropertiesChange, + modifier = modifier + ) + } else { + TextPane(stringResource(R.string.block_of_content_2), modifier = modifier.fillMaxHeight()) + } } @Composable diff --git a/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidControls.kt b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidControls.kt new file mode 100644 index 0000000..3166cfc --- /dev/null +++ b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidControls.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.helloandroidxr.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.helloandroidxr.R +import com.example.helloandroidxr.viewmodel.SliderGroup + +/** + * Controls for changing the Gltf Model's Scale, Position, and Rotation + */ +@Composable +fun BugdroidControls( + onSliderGroupSelected: (SliderGroup) -> Unit, + onResetModel: () -> Unit, + modifier: Modifier = Modifier, +) { + Surface { + Row( + modifier = Modifier + .fillMaxSize(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Column( + Modifier + .padding(25.dp) + .fillMaxHeight(), + verticalArrangement = Arrangement.SpaceAround + ) { + Button( + onClick = { onSliderGroupSelected(SliderGroup.SCALE) }, + modifier = modifier + ) { + Text(text = stringResource(R.string.scale)) + } + Button( + onClick = { onSliderGroupSelected(SliderGroup.ROTATION) }, + modifier = modifier + ) { + Text(text = stringResource(R.string.rotation)) + } + Button( + onClick = { onSliderGroupSelected(SliderGroup.OFFSET) }, + modifier = modifier + ) { + Text(text = stringResource(R.string.offset)) + } + } + Column( + Modifier + .padding(25.dp) + .fillMaxHeight(), + verticalArrangement = Arrangement.SpaceAround + ) { + Button( + onClick = { onSliderGroupSelected(SliderGroup.MATERIAL_COLORS) }, + modifier = modifier + ) { + Text(text = stringResource(R.string.material_color)) + } + Button( + onClick = { onSliderGroupSelected(SliderGroup.MATERIAL_PROPERTIES) }, + modifier = modifier + ) { + Text(text = stringResource(R.string.material_properties)) + } + Button( + onClick = { + onResetModel() + onSliderGroupSelected(SliderGroup.NONE) + }, + modifier = modifier + ) { + Text(text = stringResource(R.string.reset)) + } + } + } + } +} + +@Preview +@Composable +fun BugdroidControlsPreview() { + BugdroidControls(onSliderGroupSelected = {}, onResetModel = {}) +} diff --git a/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidModel.kt b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidModel.kt index d64c73b..b4af8de 100644 --- a/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidModel.kt +++ b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidModel.kt @@ -17,15 +17,14 @@ package com.example.helloandroidxr.ui.components import android.annotation.SuppressLint -import android.content.Context import android.util.Log import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -36,40 +35,93 @@ import androidx.xr.compose.subspace.SceneCoreEntitySizeAdapter import androidx.xr.compose.subspace.layout.SubspaceModifier import androidx.xr.compose.subspace.layout.scale import androidx.xr.compose.unit.Meter -import androidx.xr.runtime.Session -import androidx.xr.scenecore.GltfModel +import androidx.xr.runtime.math.Vector4 import androidx.xr.scenecore.GltfModelEntity -import com.example.helloandroidxr.R -import java.io.InputStream +import androidx.xr.scenecore.KhronosPbrMaterial +import androidx.xr.scenecore.KhronosPbrMaterialSpec +import com.example.helloandroidxr.bugdroid.BugdroidController +import com.example.helloandroidxr.viewmodel.ModelTransform // Bugdroid glb height in meters private const val bugdroidHeight = 2.08f + // The desired amount of the available layout height to use for the bugdroid private const val fillRatio = 0.5f +const val TAG = "BugdroidModel" + +@SuppressLint("RestrictedApi") @Composable -fun BugdroidModel(showBugdroid: Boolean, modifier: SubspaceModifier = SubspaceModifier) { - if (showBugdroid) { - val xrSession = checkNotNull(LocalSession.current) - // Load the GltfModel data before creating the entity. - var gltfModel by remember { mutableStateOf(null) } +fun BugdroidModel( + modelTransform: ModelTransform, + showBugdroid: Boolean, + animateBugdroid: Boolean, + modifier: SubspaceModifier = SubspaceModifier, +) { + val xrSession = LocalSession.current + if (xrSession != null && showBugdroid) { val context = LocalContext.current - - LaunchedEffect(Unit) { - if (gltfModel == null) { - gltfModel = BugdroidGltfModelCache.getOrLoadModel(xrSession, context) - } + val coroutineScope = rememberCoroutineScope() + val bugdroidController = remember(xrSession, context, coroutineScope) { + BugdroidController(xrSession, context, coroutineScope) } - gltfModel?.let { gltfModel -> + val gltfModel = bugdroidController.gltfModel + gltfModel?.let { model -> Subspace { val density = LocalDensity.current - var scale by remember { mutableFloatStateOf(1f) } + var scaleFromLayout by remember { mutableFloatStateOf(1f) } + var pbrMaterial by remember { mutableStateOf(null) } + LaunchedEffect(xrSession) { + try { + val spec = + KhronosPbrMaterialSpec.create( + lightingModel = KhronosPbrMaterialSpec.LightingModel.LIT, + blendMode = KhronosPbrMaterialSpec.BlendMode.OPAQUE, + doubleSidedMode = KhronosPbrMaterialSpec.DoubleSidedMode.SINGLE_SIDED, + ) + pbrMaterial = KhronosPbrMaterial.create(xrSession, spec) + } catch (e: Exception) { + Log.e(TAG, "Error creating material", e) + } + } + LaunchedEffect( + pbrMaterial, + modelTransform.materialColor.x, + modelTransform.materialColor.y, + modelTransform.materialColor.z, + modelTransform.materialColor.w, + modelTransform.materialProperties.ambientOcclusion, + modelTransform.materialProperties.metallic, + modelTransform.materialProperties.roughness, + ) { + pbrMaterial?.setBaseColorFactors( + Vector4( + x = modelTransform.materialColor.x, + y = modelTransform.materialColor.y, + z = modelTransform.materialColor.z, + ) + ) + pbrMaterial?.setAmbientOcclusionFactor(modelTransform.materialProperties.ambientOcclusion) + pbrMaterial?.setMetallicFactor(modelTransform.materialProperties.metallic) + pbrMaterial?.setRoughnessFactor(modelTransform.materialProperties.roughness) + } SceneCoreEntity( factory = { - GltfModelEntity.create(xrSession, gltfModel).also { entity -> - entity.startAnimation( - loop = true, animationName = "Armature|Take 001|BaseLayer" + GltfModelEntity.create(xrSession, model) + }, + update = { entity: GltfModelEntity -> + pbrMaterial?.let { newMaterial -> + entity.setMaterialOverride( + material = newMaterial, + "Droid_Solo:Bugdroid" ) + if (animateBugdroid) { + entity.startAnimation( + loop = true, animationName = "Armature|Take 001|BaseLayer" + ) + } else { + entity.stopAnimation() + } } }, sizeAdapter = SceneCoreEntitySizeAdapter(onLayoutSizeChanged = { size -> @@ -78,51 +130,11 @@ fun BugdroidModel(showBugdroid: Boolean, modifier: SubspaceModifier = SubspaceMo val scaleToFillLayoutHeight = Meter .fromPixel(size.height.toFloat(), density).toM() / bugdroidHeight //Limit the scale to a ratio of the available space - scale = scaleToFillLayoutHeight * fillRatio + scaleFromLayout = scaleToFillLayoutHeight * fillRatio }), - modifier = modifier.scale(scale) - ) - } - } - } - // Clean up the cache when the composable leaves the composition. - DisposableEffect(Unit) { - onDispose { - BugdroidGltfModelCache.clearCache() - } - } -} - -/** - * Singleton object to cache the GltfModel. - */ -private object BugdroidGltfModelCache { - private var cachedModel: GltfModel? = null - - @SuppressLint("RestrictedApi") - suspend fun getOrLoadModel( - xrCoreSession: Session, context: Context - ): GltfModel? { - return if (cachedModel == null) { - try { - val inputStream: InputStream = - context.resources.openRawResource(R.raw.bugdroid_animated_wave) - cachedModel = GltfModel.create( - xrCoreSession, inputStream.readBytes(), "BUGDROID" + modifier = modifier.scale(scaleFromLayout * modelTransform.scale) ) - cachedModel - } catch (e: Exception) { - Log.e(TAG, "Error loading GLTF model", e) - null } - } else { - cachedModel } } - - fun clearCache() { - cachedModel = null - } - - const val TAG = "BugdroidGltfModelCache" } \ No newline at end of file diff --git a/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidSliderControls.kt b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidSliderControls.kt new file mode 100644 index 0000000..4190578 --- /dev/null +++ b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidSliderControls.kt @@ -0,0 +1,431 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.helloandroidxr.ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Slider +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.example.helloandroidxr.R +import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme +import com.example.helloandroidxr.viewmodel.ModelMaterialColor +import com.example.helloandroidxr.viewmodel.ModelMaterialProperties +import com.example.helloandroidxr.viewmodel.ModelOffset +import com.example.helloandroidxr.viewmodel.ModelRotation +import com.example.helloandroidxr.viewmodel.ModelTransform +import com.example.helloandroidxr.viewmodel.SliderGroup + +@Composable +fun BugdroidSliderControls( + visibleSliderGroup: SliderGroup, + modelTransform: ModelTransform, + modifier: Modifier = Modifier, + onScaleChange: (Float) -> Unit, + onRotationChange: (ModelRotation) -> Unit, + onOffsetChange: (ModelOffset) -> Unit, + onMaterialColorChange: (ModelMaterialColor) -> Unit, + onMaterialPropertiesChange: (ModelMaterialProperties) -> Unit, +) { + Surface(modifier = modifier.fillMaxSize()) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.Center, + ) { + when (visibleSliderGroup) { + SliderGroup.SCALE -> { + ScaleSlider( + scale = modelTransform.scale, + onScaleChange = onScaleChange, + modifier = modifier + ) + } + + SliderGroup.ROTATION -> { + RotationSliders( + rotation = modelTransform.rotation, + onRotationChange = onRotationChange, + modifier = modifier + ) + } + + SliderGroup.OFFSET -> { + OffsetSliders( + offset = modelTransform.offset, + onOffsetChange = onOffsetChange, + modifier = modifier + ) + } + + SliderGroup.MATERIAL_COLORS -> { + MaterialColorSliders( + materialColor = modelTransform.materialColor, + onMaterialColorChange = onMaterialColorChange, + modifier = modifier + ) + } + + SliderGroup.MATERIAL_PROPERTIES -> { + MaterialPropertySliders( + materialProperties = modelTransform.materialProperties, + onMaterialPropertiesChange = onMaterialPropertiesChange, + modifier = modifier + ) + } + + else -> { + Text(text = stringResource(R.string.please_select)) + } + } + } + } +} + +@Composable +fun ScaleSlider( + scale: Float, + onScaleChange: (Float) -> Unit, + modifier: Modifier +) { + Text(text = stringResource(R.string.change_scale)) + + Spacer(modifier.padding(25.dp)) + + Text(text = stringResource(R.string.scale)) + Slider( + value = scale, + onValueChange = onScaleChange, + valueRange = .1f..5f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(scale)) +} + +@Composable +fun RotationSliders( + rotation: ModelRotation, + onRotationChange: (ModelRotation) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier.fillMaxHeight(), + verticalArrangement = Arrangement.SpaceEvenly + ) { + Text(text = stringResource(R.string.change_rotation)) + Spacer(modifier = Modifier.padding(10.dp)) + + Text(text = stringResource(R.string.x_rotation)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Slider( + value = rotation.x, + onValueChange = { newX -> onRotationChange(rotation.copy(x = newX)) }, + valueRange = -15f..15f, + modifier = Modifier.weight(.7f) + ) + Text( + text = "%.2f".format(rotation.x) + ) + } + Text(text = stringResource(R.string.y_rotation)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Slider( + value = rotation.y, + onValueChange = { newY -> onRotationChange(rotation.copy(y = newY)) }, + valueRange = -15f..15f, + modifier = Modifier.weight(.7f) + ) + Text(text = "%.2f".format(rotation.y)) + } + + Text(text = stringResource(R.string.z_rotation)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Slider( + value = rotation.z, + onValueChange = { newZ -> onRotationChange(rotation.copy(z = newZ)) }, + valueRange = -50f..50f, + modifier = Modifier.weight(.7f) + ) + Spacer(modifier = Modifier.padding(10.dp)) + Text(text = "%.2f".format(rotation.z)) + } + + Text(text = stringResource(R.string.w_rotation)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Slider( + value = rotation.w, + onValueChange = { newW -> onRotationChange(rotation.copy(w = newW)) }, + valueRange = -5f..5f, + modifier = Modifier.weight(.7f) + ) + Spacer(modifier = Modifier.padding(10.dp)) + Text(text = "%.2f".format(rotation.w)) + } + } +} + +@Composable +fun OffsetSliders( + offset: ModelOffset, + onOffsetChange: (ModelOffset) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier.fillMaxHeight(), + verticalArrangement = Arrangement.SpaceEvenly + ) { + Text(text = stringResource(R.string.change_offset)) + Spacer(modifier = Modifier.padding(10.dp)) + + Text(text = stringResource(R.string.x_offset)) + Slider( + value = offset.x, + onValueChange = { newX -> onOffsetChange(offset.copy(x = newX)) }, + valueRange = -1500f..1500f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(offset.x)) + + Text(text = stringResource(R.string.y_offset)) + Slider( + value = offset.y, + onValueChange = { newY -> onOffsetChange(offset.copy(y = newY)) }, + valueRange = -1500f..1500f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(offset.y)) + + Text(text = stringResource(R.string.z_offset)) + Slider( + value = offset.z, + onValueChange = { newZ -> onOffsetChange(offset.copy(z = newZ)) }, + valueRange = -1500f..1500f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(offset.z)) + } +} + +@Composable +fun MaterialColorSliders( + materialColor: ModelMaterialColor, + onMaterialColorChange: (ModelMaterialColor) -> Unit, + modifier: Modifier, +) { + Column( + modifier.fillMaxHeight(), + verticalArrangement = Arrangement.SpaceEvenly + ) { + Text(text = stringResource(R.string.change_material_color)) + Spacer(modifier = Modifier.padding(10.dp)) + + Text(text = stringResource(R.string.r_material)) + Slider( + value = materialColor.x, + onValueChange = { newX -> onMaterialColorChange(materialColor.copy(x = newX)) }, + valueRange = 0f..1f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(materialColor.x)) + + Text(text = stringResource(R.string.g_material)) + Slider( + value = materialColor.y, + onValueChange = { newY -> onMaterialColorChange(materialColor.copy(y = newY)) }, + valueRange = 0f..1f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(materialColor.y)) + + Text(text = stringResource(R.string.b_material)) + Slider( + value = materialColor.z, + onValueChange = { newZ -> onMaterialColorChange(materialColor.copy(z = newZ)) }, + valueRange = 0f..1f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(materialColor.z)) + } +} + +@Composable +fun MaterialPropertySliders( + materialProperties: ModelMaterialProperties, + onMaterialPropertiesChange: (ModelMaterialProperties) -> Unit, + modifier: Modifier, +) { + Column( + modifier.fillMaxHeight(), + verticalArrangement = Arrangement.SpaceEvenly + ) { + Text(text = stringResource(R.string.change_material_factors)) + Spacer(modifier = Modifier.padding(10.dp)) + + Text(text = stringResource(R.string.ambient)) + Slider( + value = materialProperties.ambientOcclusion, + onValueChange = { newAmbientOcclusion -> + onMaterialPropertiesChange( + materialProperties.copy( + ambientOcclusion = newAmbientOcclusion + ) + ) + }, + valueRange = 0f..1f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(materialProperties.ambientOcclusion)) + + Text(text = stringResource(R.string.metallic)) + Slider( + value = materialProperties.metallic, + onValueChange = { newMetallic -> + onMaterialPropertiesChange( + materialProperties.copy( + metallic = newMetallic + ) + ) + }, + valueRange = 0f..1f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(materialProperties.metallic)) + + Text(text = stringResource(R.string.roughness)) + Slider( + value = materialProperties.roughness, + onValueChange = { newRoughness -> + onMaterialPropertiesChange( + materialProperties.copy( + roughness = newRoughness + ) + ) + }, + valueRange = 0f..1f, + modifier = Modifier.padding() + ) + Text(text = "%.2f".format(materialProperties.roughness)) + } +} + +@Composable +@Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") +@Preview(device = "spec:width=411dp,height=891dp") +fun ScaleSliderPreview() { + HelloAndroidXRTheme { + ScaleSlider( + scale = 1f, + onScaleChange = {}, + modifier = Modifier + ) + } +} + +@Composable +@Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") +@Preview(device = "spec:width=411dp,height=891dp") +fun RotationSlidersPreview() { + HelloAndroidXRTheme { + RotationSliders( + rotation = ModelRotation(0f, 0f, 0f, 1f), + onRotationChange = {} + ) + } +} + +@Composable +@Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") +@Preview(device = "spec:width=411dp,height=891dp") +fun OffsetSlidersPreview() { + HelloAndroidXRTheme { + OffsetSliders( + offset = ModelOffset(0f, 0f, 0f), + onOffsetChange = {} + ) + } +} + +@Composable +@Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") +@Preview(device = "spec:width=411dp,height=891dp") +fun MaterialColorSlidersPreview() { + HelloAndroidXRTheme { + MaterialColorSliders( + materialColor = ModelMaterialColor(0.5f, 0.5f, 0.5f), + onMaterialColorChange = {}, + modifier = Modifier + ) + } +} + +@Composable +@Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") +@Preview(device = "spec:width=411dp,height=891dp") +fun MaterialPropertySlidersPreview() { + HelloAndroidXRTheme { + MaterialPropertySliders( + materialProperties = ModelMaterialProperties(0.5f, 0.5f, 0.5f), + onMaterialPropertiesChange = {}, + modifier = Modifier + ) + } +} + +@Composable +@Preview(device = "spec:width=1920dp,height=1080dp,dpi=160") +@Preview(device = "spec:width=411dp,height=891dp") +fun BugdroidSliderControlsPreview() { + HelloAndroidXRTheme { + BugdroidSliderControls( + visibleSliderGroup = SliderGroup.SCALE, + modelTransform = ModelTransform( + scale = 1f, + rotation = ModelRotation(0f, 0f, 0f, 1f), + offset = ModelOffset(0f, 0f, 0f), + materialColor = ModelMaterialColor(0.5f, 0.5f, 0.5f), + materialProperties = ModelMaterialProperties(0.5f, 0.5f, 0.5f) + ), + onScaleChange = {}, + onRotationChange = {}, + onOffsetChange = {}, + onMaterialColorChange = {}, + onMaterialPropertiesChange = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/helloandroidxr/ui/TextPane.kt b/app/src/main/java/com/example/helloandroidxr/ui/components/TextPane.kt similarity index 96% rename from app/src/main/java/com/example/helloandroidxr/ui/TextPane.kt rename to app/src/main/java/com/example/helloandroidxr/ui/components/TextPane.kt index 52b295d..1e482d8 100644 --- a/app/src/main/java/com/example/helloandroidxr/ui/TextPane.kt +++ b/app/src/main/java/com/example/helloandroidxr/ui/components/TextPane.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.example.helloandroidxr.ui +package com.example.helloandroidxr.ui.components import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding diff --git a/app/src/main/java/com/example/helloandroidxr/viewmodel/BugdroidViewModel.kt b/app/src/main/java/com/example/helloandroidxr/viewmodel/BugdroidViewModel.kt new file mode 100644 index 0000000..b6f7853 --- /dev/null +++ b/app/src/main/java/com/example/helloandroidxr/viewmodel/BugdroidViewModel.kt @@ -0,0 +1,205 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.helloandroidxr.viewmodel + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update + +private const val DEFAULT_SCALE = 1.0f +private const val DEFAULT_X_ROTATION = 0.0f +private const val DEFAULT_Y_ROTATION = 0.0f +private const val DEFAULT_Z_ROTATION = 0.0f +private const val DEFAULT_W_ROTATION = 1.0f +private const val DEFAULT_X_OFFSET = 0.0f +private const val DEFAULT_Y_OFFSET = 0.0f +private const val DEFAULT_Z_OFFSET = 400.0f +private const val DEFAULT_X_MATERIAL_COLOR = 0.0f +private const val DEFAULT_Y_MATERIAL_COLOR = 1.0f +private const val DEFAULT_Z_MATERIAL_COLOR = 0.0f +private const val DEFAULT_W_MATERIAL_COLOR = 0.0f +private const val DEFAULT_AMBIENT_OCCLUSION = 0.5f +private const val DEFAULT_METALLIC = 0.0f +private const val DEFAULT_ROUGHNESS = 0.0f +private const val MIN_COERCE_VALUE = -1500.0f +private const val MAX_COERCE_VALUE = 1500.0f + +// Represents the rotation values for the 3D model +data class ModelRotation( + val x: Float = DEFAULT_X_ROTATION, + val y: Float = DEFAULT_Y_ROTATION, + val z: Float = DEFAULT_Z_ROTATION, + val w: Float = DEFAULT_W_ROTATION, +) + +// Represents the offset values for the 3D model +data class ModelOffset( + val x: Float = DEFAULT_X_OFFSET, + val y: Float = DEFAULT_Y_OFFSET, + val z: Float = DEFAULT_Z_OFFSET, +) + +// Represents the material color values for the 3D model +data class ModelMaterialColor( + val x: Float = DEFAULT_X_MATERIAL_COLOR, + val y: Float = DEFAULT_Y_MATERIAL_COLOR, + val z: Float = DEFAULT_Z_MATERIAL_COLOR, + val w: Float = DEFAULT_W_MATERIAL_COLOR, +) + +// Represents the material properties for the 3D model +data class ModelMaterialProperties( + val ambientOcclusion: Float = DEFAULT_AMBIENT_OCCLUSION, + val metallic: Float = DEFAULT_METALLIC, + val roughness: Float = DEFAULT_ROUGHNESS, +) + +// Represents the transform values for the 3D model +data class ModelTransform( + val scale: Float = DEFAULT_SCALE, + val rotation: ModelRotation = ModelRotation(), + val offset: ModelOffset = ModelOffset(), + val materialColor: ModelMaterialColor = ModelMaterialColor(), + val materialProperties: ModelMaterialProperties = ModelMaterialProperties(), +) + +// Enum to represent which slider group is visible. +// This prevents impossible states, like two groups showing at once. +enum class SliderGroup { + NONE, SCALE, ROTATION, OFFSET, MATERIAL_COLORS, MATERIAL_PROPERTIES +} + +// The single state object for the entire screen +data class BugdroidUiState( + val showBugdroid: Boolean = false, + val animateBugdroid: Boolean = false, + val visibleSliderGroup: SliderGroup = SliderGroup.NONE, + val modelTransform: ModelTransform = ModelTransform(), +) + +class BugdroidViewModel : ViewModel() { + // Private mutable state + private val _uiState = MutableStateFlow(BugdroidUiState()) + + // Public immutable state flow for the UI to observe + val uiState: StateFlow = _uiState.asStateFlow() + + fun updateShownSliderGroup(group: SliderGroup) { + _uiState.update { currentState -> + currentState.copy(visibleSliderGroup = group) + } + } + + fun updateShowBugdroid() { + _uiState.update { currentState -> + currentState.copy(showBugdroid = !currentState.showBugdroid) + } + } + + fun updateAnimateBugdroid() { + _uiState.update { currentState -> + currentState.copy(animateBugdroid = !currentState.animateBugdroid) + } + } + + fun updateScale(newScale: Float) { + _uiState.update { currentState -> + currentState.copy( + modelTransform = currentState.modelTransform.copy( + scale = newScale.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE) + ) + ) + } + } + + fun updateRotation(newRotation: ModelRotation) { + _uiState.update { currentState -> + currentState.copy( + modelTransform = currentState.modelTransform.copy( + rotation = newRotation.copy( + x = newRotation.x.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), + y = newRotation.y.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), + z = newRotation.z.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), + w = newRotation.w.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE) + ) + ) + ) + } + } + + fun updateOffset(newOffset: ModelOffset) { + _uiState.update { currentState -> + currentState.copy( + modelTransform = currentState.modelTransform.copy( + offset = currentState.modelTransform.offset.copy( + x = newOffset.x.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), + y = newOffset.y.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), + z = newOffset.z.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE) + ) + ) + ) + } + } + + fun updateMaterialColor(newMaterialColor: ModelMaterialColor) { + _uiState.update { currentState -> + currentState.copy( + modelTransform = currentState.modelTransform.copy( + materialColor = currentState.modelTransform.materialColor.copy( + x = newMaterialColor.x.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), + y = newMaterialColor.y.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), + z = newMaterialColor.z.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), + w = newMaterialColor.w.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE) + ) + ) + ) + } + } + + fun updateMaterialProperties(newMaterialProperties: ModelMaterialProperties) { + _uiState.update { currentState -> + currentState.copy( + modelTransform = currentState.modelTransform.copy( + materialProperties = currentState.modelTransform.materialProperties.copy( + ambientOcclusion = newMaterialProperties.ambientOcclusion.coerceIn( + MIN_COERCE_VALUE, + MAX_COERCE_VALUE + ), + metallic = newMaterialProperties.metallic.coerceIn( + MIN_COERCE_VALUE, + MAX_COERCE_VALUE + ), + roughness = newMaterialProperties.roughness.coerceIn( + MIN_COERCE_VALUE, + MAX_COERCE_VALUE + ), + ) + ) + ) + } + } + + fun resetModel() { + _uiState.update { currentState -> + currentState.copy( + modelTransform = ModelTransform() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e3dc530..57efc72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,5 +27,35 @@ set passthrough Show bugdroid Hide bugdroid - + Animate Bugdroid + Stop Animation + Scale + Rotation + Offset + Rotate on X axis + Rotate on Y axis + Rotate on Z axis + Rotate on W axis + Offset on X axis + Offset on Y axis + Offset on Z axis + Change the Model\'s Scale + Change the Model\'s Rotation + Change the Model\'s Offset + Reset Model + Material + Material Base Colors + Material Properties + Texture + Change the Model\'s Base Color Material + Change the Model\'s Material Properties + R: + G: + B: + A: + Coming Soon! + Ambient Occlusion + Metallic Factor + Roughness + Please select one of the buttons to choose which properties to adjust. \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 303e80a..b95edb9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] -androidx-runtime = "1.9.0" -agp = "8.12.0" +androidx-runtime = "1.9.2" +agp = "8.13.0" arcore = "1.0.0-alpha06" -compose = "1.0.0-alpha06" -extensionsXr = "1.0.0" -scenecore = "1.0.0-alpha06" +compose = "1.0.0-alpha07" +extensionsXr = "1.1.0" +scenecore = "1.0.0-alpha07" kotlinxCoroutinesGuava = "1.10.2" kotlin = "2.2.0" concurrentFuturesKtx = "1.3.0" From e7f5aa57d15ce8964399ba54a697387f3d736c1d Mon Sep 17 00:00:00 2001 From: azeppenfeld Date: Sat, 25 Oct 2025 14:54:18 -0700 Subject: [PATCH 2/2] minor changes Change-Id: Ie6d09c7d8a2860dc3cbc848a2a5c329a680e7461 --- .../bugdroid/BugdroidController.kt | 6 ++- .../ui/components/BugdroidSliderControls.kt | 44 ++++++++++----- .../viewmodel/BugdroidViewModel.kt | 54 ++++++++++++------- 3 files changed, 69 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/example/helloandroidxr/bugdroid/BugdroidController.kt b/app/src/main/java/com/example/helloandroidxr/bugdroid/BugdroidController.kt index a23c04d..ddd8ce3 100644 --- a/app/src/main/java/com/example/helloandroidxr/bugdroid/BugdroidController.kt +++ b/app/src/main/java/com/example/helloandroidxr/bugdroid/BugdroidController.kt @@ -53,12 +53,16 @@ private object BugdroidGltfModelCache { suspend fun getOrLoadModel( xrCoreSession: Session?, context: Context ): GltfModel? { + xrCoreSession ?: run { + Log.w(TAG, "Cannot load model, session is null.") + return null + } return if (cachedModel == null) { try { val inputStream: InputStream = context.resources.openRawResource(R.raw.bugdroid_animated_wave) cachedModel = GltfModel.create( - xrCoreSession!!, inputStream.readBytes(), "BUGDROID" + xrCoreSession, inputStream.readBytes(), "BUGDROID" ) cachedModel } catch (e: Exception) { diff --git a/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidSliderControls.kt b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidSliderControls.kt index 4190578..27cd041 100644 --- a/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidSliderControls.kt +++ b/app/src/main/java/com/example/helloandroidxr/ui/components/BugdroidSliderControls.kt @@ -34,6 +34,22 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.example.helloandroidxr.R import com.example.helloandroidxr.ui.theme.HelloAndroidXRTheme +import com.example.helloandroidxr.viewmodel.MAX_MATERIAL_COLOR_VALUE +import com.example.helloandroidxr.viewmodel.MAX_MATERIAL_PROP_VALUE +import com.example.helloandroidxr.viewmodel.MAX_OFFSET_VALUE +import com.example.helloandroidxr.viewmodel.MAX_SCALE_VALUE +import com.example.helloandroidxr.viewmodel.MAX_W_ROTATION_VALUE +import com.example.helloandroidxr.viewmodel.MAX_X_ROTATION_VALUE +import com.example.helloandroidxr.viewmodel.MAX_Y_ROTATION_VALUE +import com.example.helloandroidxr.viewmodel.MAX_Z_ROTATION_VALUE +import com.example.helloandroidxr.viewmodel.MIN_MATERIAL_COLOR_VALUE +import com.example.helloandroidxr.viewmodel.MIN_MATERIAL_PROP_VALUE +import com.example.helloandroidxr.viewmodel.MIN_OFFSET_VALUE +import com.example.helloandroidxr.viewmodel.MIN_SCALE_VALUE +import com.example.helloandroidxr.viewmodel.MIN_W_ROTATION_VALUE +import com.example.helloandroidxr.viewmodel.MIN_X_ROTATION_VALUE +import com.example.helloandroidxr.viewmodel.MIN_Y_ROTATION_VALUE +import com.example.helloandroidxr.viewmodel.MIN_Z_ROTATION_VALUE import com.example.helloandroidxr.viewmodel.ModelMaterialColor import com.example.helloandroidxr.viewmodel.ModelMaterialProperties import com.example.helloandroidxr.viewmodel.ModelOffset @@ -120,7 +136,7 @@ fun ScaleSlider( Slider( value = scale, onValueChange = onScaleChange, - valueRange = .1f..5f, + valueRange = MIN_SCALE_VALUE..MAX_SCALE_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(scale)) @@ -147,7 +163,7 @@ fun RotationSliders( Slider( value = rotation.x, onValueChange = { newX -> onRotationChange(rotation.copy(x = newX)) }, - valueRange = -15f..15f, + valueRange = MIN_X_ROTATION_VALUE..MAX_X_ROTATION_VALUE, modifier = Modifier.weight(.7f) ) Text( @@ -162,7 +178,7 @@ fun RotationSliders( Slider( value = rotation.y, onValueChange = { newY -> onRotationChange(rotation.copy(y = newY)) }, - valueRange = -15f..15f, + valueRange = MIN_Y_ROTATION_VALUE..MAX_Y_ROTATION_VALUE, modifier = Modifier.weight(.7f) ) Text(text = "%.2f".format(rotation.y)) @@ -176,7 +192,7 @@ fun RotationSliders( Slider( value = rotation.z, onValueChange = { newZ -> onRotationChange(rotation.copy(z = newZ)) }, - valueRange = -50f..50f, + valueRange = MIN_Z_ROTATION_VALUE..MAX_Z_ROTATION_VALUE, modifier = Modifier.weight(.7f) ) Spacer(modifier = Modifier.padding(10.dp)) @@ -191,7 +207,7 @@ fun RotationSliders( Slider( value = rotation.w, onValueChange = { newW -> onRotationChange(rotation.copy(w = newW)) }, - valueRange = -5f..5f, + valueRange = MIN_W_ROTATION_VALUE..MAX_W_ROTATION_VALUE, modifier = Modifier.weight(.7f) ) Spacer(modifier = Modifier.padding(10.dp)) @@ -217,7 +233,7 @@ fun OffsetSliders( Slider( value = offset.x, onValueChange = { newX -> onOffsetChange(offset.copy(x = newX)) }, - valueRange = -1500f..1500f, + valueRange = MIN_OFFSET_VALUE..MAX_OFFSET_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(offset.x)) @@ -226,7 +242,7 @@ fun OffsetSliders( Slider( value = offset.y, onValueChange = { newY -> onOffsetChange(offset.copy(y = newY)) }, - valueRange = -1500f..1500f, + valueRange = MIN_OFFSET_VALUE..MAX_OFFSET_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(offset.y)) @@ -235,7 +251,7 @@ fun OffsetSliders( Slider( value = offset.z, onValueChange = { newZ -> onOffsetChange(offset.copy(z = newZ)) }, - valueRange = -1500f..1500f, + valueRange = MIN_OFFSET_VALUE..MAX_OFFSET_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(offset.z)) @@ -259,7 +275,7 @@ fun MaterialColorSliders( Slider( value = materialColor.x, onValueChange = { newX -> onMaterialColorChange(materialColor.copy(x = newX)) }, - valueRange = 0f..1f, + valueRange = MIN_MATERIAL_COLOR_VALUE..MAX_MATERIAL_COLOR_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(materialColor.x)) @@ -268,7 +284,7 @@ fun MaterialColorSliders( Slider( value = materialColor.y, onValueChange = { newY -> onMaterialColorChange(materialColor.copy(y = newY)) }, - valueRange = 0f..1f, + valueRange = MIN_MATERIAL_COLOR_VALUE..MAX_MATERIAL_COLOR_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(materialColor.y)) @@ -277,7 +293,7 @@ fun MaterialColorSliders( Slider( value = materialColor.z, onValueChange = { newZ -> onMaterialColorChange(materialColor.copy(z = newZ)) }, - valueRange = 0f..1f, + valueRange = MIN_MATERIAL_COLOR_VALUE..MAX_MATERIAL_COLOR_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(materialColor.z)) @@ -307,7 +323,7 @@ fun MaterialPropertySliders( ) ) }, - valueRange = 0f..1f, + valueRange = MIN_MATERIAL_PROP_VALUE..MAX_MATERIAL_PROP_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(materialProperties.ambientOcclusion)) @@ -322,7 +338,7 @@ fun MaterialPropertySliders( ) ) }, - valueRange = 0f..1f, + valueRange = MIN_MATERIAL_PROP_VALUE..MAX_MATERIAL_PROP_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(materialProperties.metallic)) @@ -337,7 +353,7 @@ fun MaterialPropertySliders( ) ) }, - valueRange = 0f..1f, + valueRange = MIN_MATERIAL_PROP_VALUE..MAX_MATERIAL_PROP_VALUE, modifier = Modifier.padding() ) Text(text = "%.2f".format(materialProperties.roughness)) diff --git a/app/src/main/java/com/example/helloandroidxr/viewmodel/BugdroidViewModel.kt b/app/src/main/java/com/example/helloandroidxr/viewmodel/BugdroidViewModel.kt index b6f7853..cfffa1e 100644 --- a/app/src/main/java/com/example/helloandroidxr/viewmodel/BugdroidViewModel.kt +++ b/app/src/main/java/com/example/helloandroidxr/viewmodel/BugdroidViewModel.kt @@ -37,8 +37,22 @@ private const val DEFAULT_W_MATERIAL_COLOR = 0.0f private const val DEFAULT_AMBIENT_OCCLUSION = 0.5f private const val DEFAULT_METALLIC = 0.0f private const val DEFAULT_ROUGHNESS = 0.0f -private const val MIN_COERCE_VALUE = -1500.0f -private const val MAX_COERCE_VALUE = 1500.0f +const val MIN_SCALE_VALUE = 0.1f +const val MAX_SCALE_VALUE = 5.0f +const val MIN_X_ROTATION_VALUE = -15.0f +const val MAX_X_ROTATION_VALUE = 15.0f +const val MIN_Y_ROTATION_VALUE = -15.0f +const val MAX_Y_ROTATION_VALUE = 15.0f +const val MIN_Z_ROTATION_VALUE = -50.0f +const val MAX_Z_ROTATION_VALUE = 50.0f +const val MIN_W_ROTATION_VALUE = -5.0f +const val MAX_W_ROTATION_VALUE = 5.0f +const val MIN_OFFSET_VALUE = -1500.0f +const val MAX_OFFSET_VALUE = 1500.0f +const val MIN_MATERIAL_COLOR_VALUE = 0.0f +const val MAX_MATERIAL_COLOR_VALUE = 1.0f +const val MIN_MATERIAL_PROP_VALUE = 0.0f +const val MAX_MATERIAL_PROP_VALUE = 1.0f // Represents the rotation values for the 3D model data class ModelRotation( @@ -122,7 +136,7 @@ class BugdroidViewModel : ViewModel() { _uiState.update { currentState -> currentState.copy( modelTransform = currentState.modelTransform.copy( - scale = newScale.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE) + scale = newScale.coerceIn(MIN_SCALE_VALUE, MAX_SCALE_VALUE) ) ) } @@ -133,10 +147,10 @@ class BugdroidViewModel : ViewModel() { currentState.copy( modelTransform = currentState.modelTransform.copy( rotation = newRotation.copy( - x = newRotation.x.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), - y = newRotation.y.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), - z = newRotation.z.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), - w = newRotation.w.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE) + x = newRotation.x.coerceIn(MIN_X_ROTATION_VALUE, MAX_X_ROTATION_VALUE), + y = newRotation.y.coerceIn(MIN_Y_ROTATION_VALUE, MAX_Y_ROTATION_VALUE), + z = newRotation.z.coerceIn(MIN_Z_ROTATION_VALUE, MAX_Z_ROTATION_VALUE), + w = newRotation.w.coerceIn(MIN_X_ROTATION_VALUE, MAX_W_ROTATION_VALUE) ) ) ) @@ -148,9 +162,9 @@ class BugdroidViewModel : ViewModel() { currentState.copy( modelTransform = currentState.modelTransform.copy( offset = currentState.modelTransform.offset.copy( - x = newOffset.x.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), - y = newOffset.y.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), - z = newOffset.z.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE) + x = newOffset.x.coerceIn(MIN_OFFSET_VALUE, MAX_OFFSET_VALUE), + y = newOffset.y.coerceIn(MIN_OFFSET_VALUE, MAX_OFFSET_VALUE), + z = newOffset.z.coerceIn(MIN_OFFSET_VALUE, MAX_OFFSET_VALUE), ) ) ) @@ -162,10 +176,10 @@ class BugdroidViewModel : ViewModel() { currentState.copy( modelTransform = currentState.modelTransform.copy( materialColor = currentState.modelTransform.materialColor.copy( - x = newMaterialColor.x.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), - y = newMaterialColor.y.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), - z = newMaterialColor.z.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE), - w = newMaterialColor.w.coerceIn(MIN_COERCE_VALUE, MAX_COERCE_VALUE) + x = newMaterialColor.x.coerceIn(MIN_MATERIAL_COLOR_VALUE, MAX_MATERIAL_COLOR_VALUE), + y = newMaterialColor.y.coerceIn(MIN_MATERIAL_COLOR_VALUE, MAX_MATERIAL_COLOR_VALUE), + z = newMaterialColor.z.coerceIn(MIN_MATERIAL_COLOR_VALUE, MAX_MATERIAL_COLOR_VALUE), + w = newMaterialColor.w.coerceIn(MIN_MATERIAL_COLOR_VALUE, MAX_MATERIAL_COLOR_VALUE), ) ) ) @@ -178,16 +192,16 @@ class BugdroidViewModel : ViewModel() { modelTransform = currentState.modelTransform.copy( materialProperties = currentState.modelTransform.materialProperties.copy( ambientOcclusion = newMaterialProperties.ambientOcclusion.coerceIn( - MIN_COERCE_VALUE, - MAX_COERCE_VALUE + MIN_MATERIAL_PROP_VALUE, + MAX_MATERIAL_PROP_VALUE ), metallic = newMaterialProperties.metallic.coerceIn( - MIN_COERCE_VALUE, - MAX_COERCE_VALUE + MIN_MATERIAL_PROP_VALUE, + MAX_MATERIAL_PROP_VALUE ), roughness = newMaterialProperties.roughness.coerceIn( - MIN_COERCE_VALUE, - MAX_COERCE_VALUE + MIN_MATERIAL_PROP_VALUE, + MAX_MATERIAL_PROP_VALUE ), ) )