Show realistic lighting and shadows for a given time of day.

Use case
You can use realistic lighting to evaluate the shadow impact of buildings and utility infrastructure on the surrounding community. This could be useful for civil engineers and urban planners, or for events management assessing the impact of building shadows during an outdoor event.
How to use the sample
Select one of the three lighting options to show that lighting effect on the SceneView. Select a time of day from the slider (based on a 24hr clock), and a date from the date picker, to show the lighting for that time of day in the SceneView.
How it works
- Create a
Sceneand display it in a composableSceneView. - Create a
ZonedDateTimeto define the day and time of day. - Set the sun time to the scene view using zoned date time and a time offset in milliseconds.
- Set the sun lighting of the scene view to a
LightingModeofNoLight,Light, orLightAndShadows.
Relevant API
- Scene
- SceneView
- SceneView.SunLighting
Additional information
This sample uses the GeoView-Compose Toolkit module to be able to implement a composable SceneView.
Tags
3D, lighting, realism, realistic, rendering, sceneview-compose, shadows, sun, time, toolkit
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.showrealisticlightandshadows
import android.os.Bundleimport androidx.activity.ComponentActivityimport androidx.activity.compose.setContentimport androidx.activity.enableEdgeToEdgeimport 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.showrealisticlightandshadows.screens.ShowRealisticLightAndShadowsScreen
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)
enableEdgeToEdge() setContent { SampleAppTheme { ShowRealisticLightAndShadowsApp() } } }
@Composable private fun ShowRealisticLightAndShadowsApp() { Surface(color = MaterialTheme.colorScheme.background) { ShowRealisticLightAndShadowsScreen( sampleName = getString(R.string.show_realistic_light_and_shadows_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.showrealisticlightandshadows.components
import android.app.Applicationimport androidx.compose.runtime.MutableStateimport androidx.compose.runtime.mutableStateOfimport androidx.compose.ui.graphics.Colorimport androidx.lifecycle.AndroidViewModelimport 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.layers.ArcGISSceneLayerimport com.arcgismaps.mapping.view.AtmosphereEffectimport com.arcgismaps.mapping.view.Cameraimport com.arcgismaps.mapping.view.LightingModeimport com.arcgismaps.mapping.view.SpaceEffectimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
class ShowRealisticLightAndShadowsViewModel(application: Application) : AndroidViewModel(application) {
val arcGISScene = ArcGISScene(BasemapStyle.ArcGISTopographic).apply { // Add a base surface with elevation source baseSurface = Surface().apply { // Create an elevation source from Terrain3D REST service elevationSources.add( ArcGISTiledElevationSource( "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer" ) ) } // Create a scene layer from buildings REST service operationalLayers.add( ArcGISSceneLayer( "https://tiles.arcgis.com/tiles/P3ePLMYs2RVChkJx/arcgis/rest/services/DevA_BuildingShells/SceneServer" ) ) // Create a point to center on val point = Point( x = -122.69033, y = 45.54605, z = 500.0, spatialReference = SpatialReference.wgs84() ) initialViewpoint = Viewpoint( center = point, scale = 17000.0, camera = Camera( locationPoint = point, heading = 162.58544, pitch = 72.0, roll = 0.0 ) ) }
// Create a LightingOptionsState with default values that will be used by the scene view val lightingOptionsState = LightingOptionsState( mutableStateOf(LightingMode.LightAndShadows), mutableStateOf(Color(red = 220, green = 220, blue = 220, alpha = 255)), mutableStateOf(AtmosphereEffect.HorizonOnly), mutableStateOf(SpaceEffect.Stars) )
// Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel()}
/** * Represents various lighting options used by the composable scene view. */data class LightingOptionsState( val sunLighting: MutableState<LightingMode>, val ambientLightColor: MutableState<Color>, val atmosphereEffect: MutableState<AtmosphereEffect>, val spaceEffect: MutableState<SpaceEffect>)/* 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.showrealisticlightandshadows.screens
import androidx.compose.foundation.BorderStrokeimport androidx.compose.foundation.layout.Arrangementimport androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.Rowimport androidx.compose.foundation.layout.fillMaxWidthimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.DatePickerimport androidx.compose.material3.DatePickerDialogimport androidx.compose.material3.DatePickerStateimport androidx.compose.material3.ExperimentalMaterial3Apiimport androidx.compose.material3.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.SegmentedButtonimport androidx.compose.material3.SegmentedButtonDefaultsimport androidx.compose.material3.SingleChoiceSegmentedButtonRowimport androidx.compose.material3.Sliderimport androidx.compose.material3.Textimport androidx.compose.material3.TextButtonimport androidx.compose.material3.rememberDatePickerStateimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableFloatStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.setValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.text.font.FontWeightimport androidx.compose.ui.unit.dpimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.mapping.view.LightingModeimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.showrealisticlightandshadows.components.ShowRealisticLightAndShadowsViewModelimport java.time.Instantimport java.time.ZoneIdimport java.time.ZonedDateTimeimport java.time.format.DateTimeFormatter
/** * Main screen layout for the sample app. */@OptIn(ExperimentalMaterial3Api::class)@Composablefun ShowRealisticLightAndShadowsScreen(sampleName: String) { val mapViewModel: ShowRealisticLightAndShadowsViewModel = viewModel()
var showDatePicker by remember { mutableStateOf(false) } val datePickerState = rememberDatePickerState() var dateTime by remember { mutableStateOf(ZonedDateTime.now(ZoneId.of("US/Pacific"))) } var timeOfDay by remember { mutableFloatStateOf(dateTime.toLocalTime().toSecondOfDay().toFloat()) }
val lightingOptionsState = mapViewModel.lightingOptionsState val lightingModes = listOf( LightingMode.LightAndShadows, LightingMode.Light, LightingMode.NoLight )
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { Column( modifier = Modifier .fillMaxWidth() .padding(it) ) { SceneView( mapViewModel.arcGISScene, modifier = Modifier.weight(1f), // Set the sun time to the selected date, plus the seconds chosen for timeOfDay sunTime = dateTime.withHour(0).withMinute(0).withSecond(0) .plusSeconds(timeOfDay.toLong()).toInstant(), sunLighting = lightingOptionsState.sunLighting.value, ambientLightColor = lightingOptionsState.ambientLightColor.value, atmosphereEffect = lightingOptionsState.atmosphereEffect.value, spaceEffect = lightingOptionsState.spaceEffect.value ) LightingOptionsPanel( lightingModes = lightingModes, selectedLightingMode = lightingOptionsState.sunLighting.value, onLightingModeSelected = { lightingOptionsState.sunLighting.value = it }, timeOfDay = timeOfDay, onTimeOfDayChanged = { timeOfDay = it }, showDatePicker = showDatePicker, onShowDatePickerChanged = { showDatePicker = it }, dateTime = dateTime, onDateTimeChanged = { dateTime = it }, datePickerState = datePickerState ) }
mapViewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}
@OptIn(ExperimentalMaterial3Api::class)@Composablefun LightingOptionsPanel( lightingModes: List<LightingMode>, selectedLightingMode: LightingMode, onLightingModeSelected: (LightingMode) -> Unit, timeOfDay: Float, onTimeOfDayChanged: (Float) -> Unit, showDatePicker: Boolean, onShowDatePickerChanged: (Boolean) -> Unit, dateTime: ZonedDateTime, onDateTimeChanged: (ZonedDateTime) -> Unit, datePickerState: DatePickerState) { Column( modifier = Modifier.fillMaxWidth() ) { // Lighting mode segmented button row SingleChoiceSegmentedButtonRow( modifier = Modifier .fillMaxWidth() .padding(8.dp) ) { lightingModes.forEachIndexed { index, mode -> val text = when (mode) { LightingMode.Light -> "Light" LightingMode.LightAndShadows -> "Light & shadows" LightingMode.NoLight -> "No light" } SegmentedButton( shape = SegmentedButtonDefaults.itemShape(index, lightingModes.size), selected = (mode == selectedLightingMode), onClick = { onLightingModeSelected(mode) }, ) { Text( text = text, fontWeight = if (mode == selectedLightingMode) FontWeight.Bold else FontWeight.Normal ) } } } // Create a slider with AM and PM labels to control the sun time Row(verticalAlignment = Alignment.CenterVertically) { Text( modifier = Modifier.padding(start = 12.dp, end = 8.dp), text = "AM", style = MaterialTheme.typography.titleMedium ) Slider( modifier = Modifier.weight(1f), value = timeOfDay, onValueChange = { onTimeOfDayChanged(it) }, // The range is 0 (start of the day) to 86399 (end of the day in seconds) valueRange = 0f..86399f ) Text( modifier = Modifier.padding(start = 8.dp, end = 12.dp), text = "PM", style = MaterialTheme.typography.titleMedium ) } // Date picker and time display Row( modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text( text = "Date:", style = MaterialTheme.typography.titleMedium ) TextButton( modifier = Modifier.padding(horizontal = 4.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), onClick = { onShowDatePickerChanged(true) }) { Text( text = dateTime.format(DateTimeFormatter.ofPattern("MMMM dd, yyyy")), style = MaterialTheme.typography.titleMedium ) } Text( text = "Time: " + dateTime.withHour(0).withMinute(0).withSecond(0) .plusSeconds(timeOfDay.toLong()) .format(DateTimeFormatter.ofPattern("hh:mm a")), style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(start = 12.dp) ) } if (showDatePicker) { DatePickerDialog( onDismissRequest = { onShowDatePickerChanged(false) }, confirmButton = { TextButton(onClick = { onShowDatePickerChanged(false) datePickerState.selectedDateMillis?.let { millis -> // Convert the selected date in milliseconds to a LocalDate val localDate = Instant.ofEpochMilli(millis).atZone(ZoneId.of("UTC")).toLocalDate() // Set the dateTime to the start of the day in Pacific Time onDateTimeChanged(localDate.atStartOfDay(ZoneId.of("US/Pacific"))) } }) { Text("Confirm") } }, dismissButton = { TextButton(onClick = { onShowDatePickerChanged(false) }) { Text("Cancel") } } ) { DatePicker(state = datePickerState) } } }}