Vertically exaggerate terrain in a scene.

Use case
Vertical exaggeration is useful when the horizontal extent of a landscape is much larger than the vertical relief. Exaggerating elevation makes small terrain variations more visible, which is helpful for visualizations, presentations, and exploratory analysis.
How to use the sample
Open the sample to display a SceneView centered on a location with elevation data. Use the ”+” and ”-” buttons in the bottom sheet to increase or decrease the terrain vertical exaggeration. The UI shows the current exaggeration factor (1x to 10x).
How it works
- Create an
ArcGISTiledElevationSourcethat points to a terrain ImageServer.- An elevation source defines the terrain based on a digital elevation model (DEM) or digital terrain model (DTM).
- Add the elevation source to a
Surfaceand assign that surface to a Scene’sbaseSurface. - Configure the surface’s
elevationExaggerationusing a multiplier factor.
Relevant API
- baseSurface
- elevationExaggeration
- Scene
- Surface
Tags
3D, DEM, elevation, exaggeration, scene, surface, terrain
Sample Code
/* Copyright 2025 Esri * * 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 * * http://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.esri.arcgismaps.sample.applyterrainexaggeration
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Surfaceimport androidx.compose.runtime.Composableimport com.arcgismaps.ApiKeyimport com.arcgismaps.ArcGISEnvironmentimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppThemeimport com.esri.arcgismaps.sample.applyterrainexaggeration.screens.ApplyTerrainExaggerationScreen
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // authentication with an API key or named user is // required to access basemaps and other location services ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)
setContent { SampleAppTheme { ApplyTerrainExaggerationApp() } } }
@Composable private fun ApplyTerrainExaggerationApp() { Surface(color = MaterialTheme.colorScheme.background) { ApplyTerrainExaggerationScreen( sampleName = getString(R.string.apply_terrain_exaggeration_app_name) ) } }}/* Copyright 2025 Esri * * 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 * * http://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.esri.arcgismaps.sample.applyterrainexaggeration.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.geometry.Pointimport com.arcgismaps.geometry.SpatialReferenceimport com.arcgismaps.mapping.ArcGISSceneimport com.arcgismaps.mapping.ArcGISTiledElevationSourceimport com.arcgismaps.mapping.BasemapStyleimport com.arcgismaps.mapping.Surfaceimport com.arcgismaps.mapping.Viewpointimport com.arcgismaps.mapping.view.Cameraimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.launch
/** * ViewModel that prepares a Scene with an elevation source and exposes * an elevation exaggeration value that the UI can update. */class ApplyTerrainExaggerationViewModel(app: Application) : AndroidViewModel(app) {
// Elevation source used by the scene's base surface. private val elevationSource: ArcGISTiledElevationSource = ArcGISTiledElevationSource( uri = "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer" )
// Expose the elevation exaggeration as a mutable state. var currentElevationExaggeration by mutableFloatStateOf(1f)
// Set the camera location to center on Levering, WA. private val camera = Camera( lookAtPoint = Point( x = -119.9489, y = 46.75792, spatialReference = SpatialReference.wgs84() ), distance = 15000.0, heading = 40.0, pitch = 60.0, roll = 0.0 )
// Create a scene with a topographic basemap and a surface that holds the elevation source. val arcGISScene: ArcGISScene = ArcGISScene(BasemapStyle.ArcGISTopographic).apply { baseSurface = Surface().apply { elevationSources.add(elevationSource) // Initial exaggeration value. elevationExaggeration = currentElevationExaggeration } // Set the initial viewpoint using the camera. initialViewpoint = Viewpoint(camera = camera, boundingGeometry = camera.location) }
// Dialog helper for showing errors. val messageDialogVM = MessageDialogViewModel()
init { // Load the scene. viewModelScope.launch { arcGISScene.load().onFailure { messageDialogVM.showMessageDialog(it) } } }
/** * Update exaggeration using an increment or decrement amount. * Value is expected between 1 and 10, clamped for the sample. */ fun updateElevationExaggeration(amount: Float) { // Update current elevation state with the exaggeration amount currentElevationExaggeration = (currentElevationExaggeration + amount).coerceIn(1f, 10f) // Update the base surface to honor the current elevation state arcGISScene.baseSurface.elevationExaggeration = currentElevationExaggeration }}/* Copyright 2025 Esri * * 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 * * http://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.esri.arcgismaps.sample.applyterrainexaggeration.screens
import android.content.res.Configurationimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material.icons.Iconsimport androidx.compose.material.icons.filled.Settingsimport androidx.compose.material3.Buttonimport androidx.compose.material3.FloatingActionButtonimport androidx.compose.material3.Iconimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Textimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.tooling.preview.Previewimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.esri.arcgismaps.sample.applyterrainexaggeration.components.ApplyTerrainExaggerationViewModelimport com.esri.arcgismaps.sample.sampleslib.components.BottomSheetimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SamplePreviewSurfaceimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Main screen that displays a SceneView and a bottom sheet with controls * to adjust terrain vertical exaggeration. */@Composablefun ApplyTerrainExaggerationScreen(sampleName: String) { val sceneViewModel: ApplyTerrainExaggerationViewModel = viewModel() var isBottomSheetVisible by remember { mutableStateOf(false) }
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, floatingActionButton = { if (!isBottomSheetVisible) { FloatingActionButton( modifier = Modifier.padding(bottom = 36.dp, end = 12.dp), onClick = { isBottomSheetVisible = true } ) { Icon(Icons.Default.Settings, contentDescription = "Show options") } } }, content = { padding -> Column( modifier = Modifier .fillMaxSize() .padding(padding) ) { SceneView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISScene = sceneViewModel.arcGISScene, onViewpointChangedForBoundingGeometry = { isBottomSheetVisible = false } ) }
BottomSheet( isVisible = isBottomSheetVisible, sheetTitle = "Terrain options", onDismissRequest = { isBottomSheetVisible = false } ) { TerrainOptions( currentExaggeration = sceneViewModel.currentElevationExaggeration, onIncrement = { sceneViewModel.updateElevationExaggeration(1f) }, onDecrement = { sceneViewModel.updateElevationExaggeration(-1f) } ) } // Display error dialogs from the ViewModel sceneViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}
@Composablefun TerrainOptions( currentExaggeration: Float, onIncrement: () -> Unit, onDecrement: () -> Unit) { Column( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier .fillMaxWidth() .padding(12.dp) ) { Text(text = "Elevation exaggeration: ${currentExaggeration.toInt()}x")
Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { Button(onClick = { onDecrement() }, enabled = currentExaggeration > 1f) { Text(text = "-") }
Text(text = "${currentExaggeration.toInt()}x")
Button(onClick = { onIncrement() }, enabled = currentExaggeration < 10f) { Text(text = "+") } } }}
@Preview(showBackground = true)@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)@Composablefun PreviewTerrainOptions() { SamplePreviewSurface { TerrainOptions( currentExaggeration = 2f, onIncrement = {}, onDecrement = {} ) }}