Change the appearance of a 3D object scene layer with different renderers.

Use case
A scene layer of 3D buildings hosted on ArcGIS Online comes with a preset renderer that defines how the buildings are displayed in the application. However, the fill color may sometimes blend into the basemap, making the buildings difficult to distinguish. To enhance visualization, you can apply a custom renderer with a more contrasting fill color, helping the 3D buildings stand out more clearly. Additionally, you can use a unique value renderer to represent different building uses, or a class breaks renderer to visualize building ages - valuable insights for urban planning and analysis.
How to use the sample
Wait for the scene layer to load. The original scene layer displays 3D textured buildings. Tap on the “Select Renderer” dropdown menu and choose a different renderer to change how the buildings are visualized. Each renderer applies different symbology to the scene layer. Setting the renderer to null will remove any applied symbology, reverting the buildings to their original textured appearance.
How it works
- Create an
ArcGISSceneLayerfrom a service URL. - Add the scene layer to an
ArcGISSceneand display it in aSceneView. - Create different renderers:
- A
SimpleRendererwith aMultilayerMeshSymboland a fill color and edges. - A
UniqueValueRendererusing a string field and differentMultilayerMeshSymbolfor each unique value of the building usage. - A
ClassBreaksRendererusing a numeric field and differentMultilayerMeshSymbolfor each value range of the year the building was completed.
- A
- Set the scene layer’s
rendererproperty to the selected renderer. - Set the scene layer’s
rendererproperty tonull, resulting in displaying the original texture of the buildings.
Relevant API
- ArcGISSceneLayer
- ClassBreaksRenderer
- MaterialFillSymbolLayer
- MultilayerMeshSymbol
- SceneView
- SimpleRenderer
- SymbolLayerEdges3D
- UniqueValueRenderer
About the data
This sample displays a Helsinki 3D buildings scene hosted on ArcGIS Online, showing 3D textured buildings in Helsinki, Finland.
Tags
3D, buildings, renderer, scene layer, symbology, visualization
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.applyrendererstoscenelayer
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.applyrendererstoscenelayer.screens.ApplyRenderersToSceneLayerScreenimport com.esri.arcgismaps.sample.sampleslib.theme.SampleAppTheme
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 { ApplyRenderersToSceneLayerApp() } } }
@Composable private fun ApplyRenderersToSceneLayerApp() { Surface(color = MaterialTheme.colorScheme.background) { ApplyRenderersToSceneLayerScreen( sampleName = getString(R.string.apply_renderers_to_scene_layer_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.applyrendererstoscenelayer.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.Colorimport 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.symbology.ClassBreakimport com.arcgismaps.mapping.symbology.ClassBreaksRendererimport com.arcgismaps.mapping.symbology.ColorMixModeimport com.arcgismaps.mapping.symbology.MaterialFillSymbolLayerimport com.arcgismaps.mapping.symbology.MultilayerMeshSymbolimport com.arcgismaps.mapping.symbology.SimpleRendererimport com.arcgismaps.mapping.symbology.SymbolLayerEdges3Dimport com.arcgismaps.mapping.symbology.UniqueValueimport com.arcgismaps.mapping.symbology.UniqueValueRendererimport com.arcgismaps.mapping.view.Cameraimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.flow.MutableStateFlowimport kotlinx.coroutines.flow.asStateFlowimport kotlinx.coroutines.launch
/** * ViewModel for the ApplyRenderersToSceneLayer sample. * Displays a 3D buildings scene layer and allows switching between different renderers. */class ApplyRenderersToSceneLayerViewModel(app: Application) : AndroidViewModel(app) { // URLs for the Helsinki buildings scene and world elevation private val helsinkiBuildingsSceneLayerUrl = "https://services.arcgis.com/V6ZHFr6zdgNZuVG0/arcgis/rest/services/Helsinki_buildings/SceneServer" private val worldElevationServiceUrl = "https://elevation3d.arcgis.com/arcgis/rest/services/WorldElevation3D/Terrain3D/ImageServer"
// ArcGISTiledElevationSource for world elevation private val elevationSource = ArcGISTiledElevationSource(worldElevationServiceUrl)
// ArcGISSceneLayer for Helsinki buildings val sceneLayer = ArcGISSceneLayer(helsinkiBuildingsSceneLayerUrl)
// Camera location for Helsinki private val cameraLocation = Point( x = 2778453.8008, y = 8436451.3882, z = 387.4524, spatialReference = SpatialReference.webMercator() )
// Camera to view the scene private val camera = Camera( locationPoint = cameraLocation, heading = 308.9, pitch = 50.7, roll = 0.0 )
// Create the ArcGISScene with light gray basemap val arcGISScene by mutableStateOf( ArcGISScene(BasemapStyle.ArcGISLightGray).apply { // Add the Helsinki buildings scene layer operationalLayers.add(sceneLayer) // Add the 3D elevation source to the base surface baseSurface = Surface().apply { elevationSources.add(elevationSource) } // Set the viewpoint camera at Helsinki initialViewpoint = Viewpoint( center = cameraLocation, camera = camera, scale = 1e4 ) } )
// Renderer selection state val rendererTypes = listOf( RendererType.SimpleRenderer, RendererType.UniqueValueRenderer, RendererType.ClassBreaksRenderer, RendererType.NullRenderer ) // Create a flow to keep track of the current selected renderer type. It is initialized as SimpleRenderer. private val _selectedRendererType = MutableStateFlow(RendererType.SimpleRenderer) // Keep track of the current selected renderer type val selectedRendererType = _selectedRendererType.asStateFlow()
// Simple renderer created from a multilayer mesh symbol with a material fill symbol layer // The colorMixMode of the material fill symbol layer is Replace which will replace the texture with the new color. // Edges are also set. private val simpleRenderer : SimpleRenderer by lazy { SimpleRenderer( symbol = MultilayerMeshSymbol( symbolLayer = MaterialFillSymbolLayer( color = Color.yellow ).apply { colorMixMode= ColorMixMode.Replace edges = SymbolLayerEdges3D( color = Color.black, width = 0.5 ) } ) ) }
// Unique value renderer using multilayer mesh symbols based on usage of the building // The material fill symbol layers use the default colorMixMode which is Multiply. // Multiply colorMixMode will multiply the initial texture color with the new color. private val uniqueValueRenderer : UniqueValueRenderer by lazy { UniqueValueRenderer( fieldNames = listOf("usage"), defaultSymbol = MultilayerMeshSymbol( symbolLayer = MaterialFillSymbolLayer( color = Color.fromRgba(230, 230, 230, 255) ) ), uniqueValues = listOf( UniqueValue( description = "commercial buildings", label = "commercial buildings", symbol = MultilayerMeshSymbol( symbolLayer = MaterialFillSymbolLayer( color = Color.fromRgba(245, 213, 169, 200) ) ), values = listOf("general or commercial") ), UniqueValue( description = "residential buildings", label = "residential buildings", symbol = MultilayerMeshSymbol( symbolLayer = MaterialFillSymbolLayer( color = Color.fromRgba(210, 254, 208, 255) ) ), values = listOf("residential") ), UniqueValue( description = "other", label = "other", symbol = MultilayerMeshSymbol( symbolLayer = MaterialFillSymbolLayer( color = Color.fromRgba(253, 198, 227, 150) ) ), values = listOf("other") ) ) ) }
// Class breaks renderer using multilayer mesh symbols based on year completed of the building // The colorMixMode used in the material fill symbol layer is Tint which will set the new color on the desaturated texture. private val classBreaksRenderer : ClassBreaksRenderer by lazy { ClassBreaksRenderer( fieldName = "yearCompleted", classBreaks = listOf( ClassBreak( description = "before 1900", label = "before 1900", minValue = 1725.0, maxValue = 1899.0, symbol = MultilayerMeshSymbol( symbolLayer = MaterialFillSymbolLayer( color = Color.fromRgba(230, 238, 207, 255) ).apply { colorMixMode = ColorMixMode.Tint } ) ), ClassBreak( description = "1900 - 1956", label = "1900 - 1956", minValue = 1900.0, maxValue = 1956.0, symbol = MultilayerMeshSymbol( symbolLayer = MaterialFillSymbolLayer( color = Color.fromRgba(155, 196, 193, 255) ).apply { colorMixMode = ColorMixMode.Tint } ) ), ClassBreak( description = "1957 - 2000", label = "1957 - 2000", minValue = 1957.0, maxValue = 2000.0, symbol = MultilayerMeshSymbol( symbolLayer = MaterialFillSymbolLayer( color = Color.fromRgba(105, 168, 183, 255) ).apply { colorMixMode = ColorMixMode.Tint } ) ), ClassBreak( description = "after 2000", label = "after 2000", minValue = 2001.0, maxValue = 3000.0, symbol = MultilayerMeshSymbol( symbolLayer = MaterialFillSymbolLayer( color = Color.fromRgba(75, 126, 152, 255) ).apply { colorMixMode = ColorMixMode.Tint } ) ) ) ) }
// Create a message dialog view model for handling error messages val messageDialogVM = MessageDialogViewModel()
init { // Load the scene viewModelScope.launch { // Apply simple renderer to the scene layer as the initial renderer sceneLayer.renderer = simpleRenderer arcGISScene.load().onFailure { messageDialogVM.showMessageDialog(it) } } }
/** * Switches the selected renderer according to the renderer type. */ fun updateSceneLayerRenderer(rendererType: RendererType) { _selectedRendererType.value = rendererType sceneLayer.renderer = when (rendererType) { RendererType.SimpleRenderer -> { simpleRenderer } RendererType.UniqueValueRenderer -> { uniqueValueRenderer } RendererType.ClassBreaksRenderer -> { classBreaksRenderer } RendererType.NullRenderer -> { null } } }
/** * Enum representing the different renderer types. */ enum class RendererType(val label: String) { SimpleRenderer("SimpleRenderer - Buildings without texture"), UniqueValueRenderer("UniqueValueRenderer - Buildings by usage"), ClassBreaksRenderer("ClassBreaksRenderer - Buildings by year completed"), NullRenderer("Null renderer - Buildings with original texture") }}/* 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.applyrendererstoscenelayer.screens
import androidx.compose.foundation.layout.Columnimport androidx.compose.foundation.layout.fillMaxSizeimport androidx.compose.foundation.layout.paddingimport androidx.compose.material3.Scaffoldimport androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.ui.Alignmentimport androidx.compose.ui.Modifierimport androidx.compose.ui.unit.dpimport androidx.lifecycle.compose.collectAsStateWithLifecycleimport androidx.lifecycle.viewmodel.compose.viewModelimport com.arcgismaps.toolkit.geoviewcompose.SceneViewimport com.esri.arcgismaps.sample.applyrendererstoscenelayer.components.ApplyRenderersToSceneLayerViewModelimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBarimport com.esri.arcgismaps.sample.sampleslib.components.DropDownMenuBox
/** * Main screen for the ApplyRenderersToSceneLayer sample. */@Composablefun ApplyRenderersToSceneLayerScreen(sampleName: String) { val viewModel: ApplyRenderersToSceneLayerViewModel = viewModel() val selectedRendererType by viewModel.selectedRendererType.collectAsStateWithLifecycle() val rendererTypes = viewModel.rendererTypes
Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, content = { padding -> Column( modifier = Modifier .fillMaxSize() .padding(padding), horizontalAlignment = Alignment.CenterHorizontally ) { SceneView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISScene = viewModel.arcGISScene ) DropDownMenuBox( modifier = Modifier .padding(12.dp), textFieldValue = selectedRendererType.label, textFieldLabel = "Select scene layer renderer", dropDownItemList = rendererTypes.map { it.label }, onIndexSelected = { index -> viewModel.updateSceneLayerRenderer(rendererTypes[index]) } ) } viewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } )}