Blend a hillshade with a raster by specifying the elevation data. The resulting raster looks similar to the original raster, but with some terrain shading, giving it a textured look.

Use case
BlendRenderer can be used to apply a color ramp to a hillshade to emphasize areas of high or low elevation. A BlendRenderer can also be used to add a hillshade effect to aerial or satellite imagery, thereby making changes in elevation more visible.
How to use the sample
Choose and adjust the altitude, azimuth, slope type and color ramp type settings to update the image.
How it works
- Create a
Rasterobject from a raster file. - Create a
RasterLayerobject from the raster. - Create a
Basemapobject from the raster layer and set it to the map. - Create another
Rasterobject for elevation from a grayscale raster file. - Create a
BlendRendererobject, specifying the elevation raster, color ramp, and other properties.
- If you specify a non-
nullcolor ramp, use the elevation raster as the base raster in addition to the elevation raster parameter. That way, the color ramp is used instead of the satellite imagery.
- Set the blend renderer to the raster layer.
Relevant API
- BlendRenderer
- ColorRamp
- Raster
- RasterLayer
Offline data
This sample uses the Shasta Raster and Shasta Elevation Raster. Both are downloaded from ArcGIS Online automatically.
Tags
color ramp, elevation, hillshade, image, raster, raster layer, 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.applyblendrenderertohillshade
import android.content.Intentimport android.os.Bundleimport com.esri.arcgismaps.sample.sampleslib.DownloaderActivity
class DownloadActivity : DownloaderActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadAndStartSample( Intent(this, MainActivity::class.java), getString(R.string.apply_blend_renderer_to_hillshade_app_name), listOf( "https://www.arcgis.com/home/item.html?id=7c4c679ab06a4df19dc497f577f111bd", "https://www.arcgis.com/home/item.html?id=b051f5c3e01048f3bf11c59b41507896" ) ) }}/* 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.applyblendrenderertohillshade
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.applyblendrenderertohillshade.screens.ApplyBlendRendererToHillshadeScreen
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 { ApplyBlendRendererToHillshadeApp() } } }
@Composable private fun ApplyBlendRendererToHillshadeApp() { Surface(color = MaterialTheme.colorScheme.background) { ApplyBlendRendererToHillshadeScreen( sampleName = getString(R.string.apply_blend_renderer_to_hillshade_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.applyblendrenderertohillshade.components
import android.app.Applicationimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableDoubleStateOfimport androidx.compose.runtime.mutableIntStateOfimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.setValueimport androidx.lifecycle.AndroidViewModelimport androidx.lifecycle.viewModelScopeimport com.arcgismaps.mapping.ArcGISMapimport com.arcgismaps.mapping.Basemapimport com.arcgismaps.mapping.layers.RasterLayerimport com.arcgismaps.mapping.symbology.raster.BlendRendererimport com.arcgismaps.mapping.symbology.raster.ColorRampimport com.arcgismaps.mapping.symbology.raster.PresetColorRampTypeimport com.arcgismaps.raster.Rasterimport com.arcgismaps.raster.SlopeTypeimport com.arcgismaps.toolkit.geoviewcompose.MapViewProxyimport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModelimport kotlinx.coroutines.launchimport java.io.File
/** * ViewModel for the "Apply blend renderer to hillshade" sample. * * This ViewModel builds two raster basemaps (imagery and elevation) and applies * a BlendRenderer to give a hillshade blended appearance. The sample exposes * a few renderer parameters (altitude, azimuth, slope type and a color ramp * preset) and updates the renderer when these change. */class ApplyBlendRendererToHillshadeViewModel(private val app: Application) : AndroidViewModel(app) {
private val provisionPath: String by lazy { app.getExternalFilesDir(null)?.path.toString() + File.separator + app.getString(com.esri.arcgismaps.sample.applyblendrenderertohillshade.R.string.apply_blend_renderer_to_hillshade_app_name) }
// Raster containing imagery (Shasta). private val imageryRaster: Raster by lazy { Raster.createWithPath("$provisionPath${File.separator}raster-file${File.separator}Shasta.tif") }
// Raster layer for imagery. private val imageryRasterLayer: RasterLayer by lazy { RasterLayer(imageryRaster) }
// Raster containing elevation values (grayscale elevation) private val elevationRaster: Raster by lazy { Raster.createWithPath("$provisionPath${File.separator}Shasta_Elevation.tif") }
// Raster layer for elevation (used as a basemap when applying a color ramp). private val elevationRasterLayer: RasterLayer by lazy { RasterLayer(elevationRaster) }
// A Basemap for the imagery raster layer. private val imageryBasemap: Basemap by lazy { Basemap(baseLayer = imageryRasterLayer) }
// A Basemap for the elevation raster layer. private val elevationBasemap: Basemap by lazy { Basemap(baseLayer = elevationRasterLayer) }
// The ArcGISMap exposed to the UI. The basemap will be // set to the imagery basemap once the raster layers are loaded // and may be switched to the elevation basemap when a color ramp is applied through the UI. val arcGISMap = ArcGISMap()
// MapViewProxy provided to the MapView composable. val mapViewProxy = MapViewProxy()
// Message dialog view model to report errors to the UI val messageDialogVM = MessageDialogViewModel()
// Renderer parameters which can be modified from the UI var altitude by mutableDoubleStateOf(45.0) private set
var azimuth by mutableDoubleStateOf(0.0) private set
var slopeType by mutableStateOf<SlopeType>(SlopeType.None) private set
// Color ramp presets exposed as user-friendly strings. The underlying ColorRamp is created // on demand in updateRenderer() when a preset is selected. val colorRampPresets = listOf("None", "DEM Light", "Screen Display", "Elevation")
// Index of the selected color ramp preset; 0 means "None" (no color ramp). var selectedColorRampPresetIndex by mutableIntStateOf(0) private set
init { // Load the raster layers and load and set up the map. Perform long-running loads in a coroutine // and surface any errors through the MessageDialogViewModel. viewModelScope.launch { try { // Load both raster layers. If any fails, report the error. elevationBasemap.load().onFailure { throw it } imageryBasemap.load().onFailure { throw it } updateRenderer() arcGISMap.load().onFailure { throw it } } catch (ex: Throwable) { messageDialogVM.showMessageDialog(ex) } } }
/** * Constructs and applies a BlendRenderer * using the current UI parameters. Also switches the map basemap to the elevation * basemap when a color ramp is active so the color ramp is visible. */ private fun updateRenderer() { val colorRamp = createColorRampFromPresetIndex(selectedColorRampPresetIndex) val renderer = BlendRenderer( elevationRaster = elevationRaster, outputMinValues = listOf(9.0), outputMaxValues = listOf(255.0), sourceMinValues = emptyList(), sourceMaxValues = emptyList(), noDataValues = emptyList(), gammas = emptyList(), colorRamp = colorRamp, altitude = altitude, azimuth = azimuth, slopeType = slopeType )
if (colorRamp != null) { // When a color ramp is applied, set the basemap to the elevation raster elevationRasterLayer.renderer = renderer arcGISMap.setBasemap(elevationBasemap) } else { // Since no ColorRamp is selected, apply BlendRenderer to imagery raster layer. imageryRasterLayer.renderer = renderer arcGISMap.setBasemap(imageryBasemap) } }
/** * Update the altitude value and re-apply the renderer. */ fun updateAltitude(newAltitude: Double) { altitude = newAltitude updateRenderer() }
/** * Update the azimuth value and re-apply the renderer. */ fun updateAzimuth(newAzimuth: Double) { azimuth = newAzimuth updateRenderer() }
/** * Update the slope type for the hillshade renderer and re-apply. */ fun updateSlopeType(newSlopeType: SlopeType) { slopeType = newSlopeType updateRenderer() }
/** * Update the selected color ramp preset index and re-apply the renderer. */ fun updateColorRampPresetIndex(index: Int) { selectedColorRampPresetIndex = index.coerceIn(0, colorRampPresets.lastIndex) updateRenderer() }
/** * Helper that creates a Kotlin ColorRamp from the selected preset index. * If the preset is "None" this returns null. */ private fun createColorRampFromPresetIndex(index: Int): ColorRamp? { return when (index) { 1 -> ColorRamp.create(type = PresetColorRampType.DemLight, size = 256) 2 -> ColorRamp.create(type = PresetColorRampType.DemScreen, size = 256) 3 -> ColorRamp.create(type = PresetColorRampType.Elevation, size = 256) else -> null } }
companion object { // Small helper list for exposing slope types with a user-friendly label in the UI. val slopeTypeOptions: List<Pair<String, SlopeType>> = listOf( "None" to SlopeType.None, "Degree" to SlopeType.Degree, "Percent Rise" to SlopeType.PercentRise, "Scaled" to SlopeType.Scaled ) }}/* 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.applyblendrenderertohillshade.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.MaterialThemeimport androidx.compose.material3.Scaffoldimport androidx.compose.material3.Sliderimport 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.raster.SlopeTypeimport com.arcgismaps.toolkit.geoviewcompose.MapViewimport com.esri.arcgismaps.sample.applyblendrenderertohillshade.components.ApplyBlendRendererToHillshadeViewModelimport com.esri.arcgismaps.sample.sampleslib.components.DropDownMenuBoximport com.esri.arcgismaps.sample.sampleslib.components.MessageDialogimport com.esri.arcgismaps.sample.sampleslib.components.SampleDialogimport com.esri.arcgismaps.sample.sampleslib.components.SamplePreviewSurfaceimport com.esri.arcgismaps.sample.sampleslib.components.SampleTopAppBar
/** * Main screen layout for the sample app. The UI exposes a floating settings button * which opens a dialog where the user can adjust the altitude, azimuth, slope type * and color ramp preset. Changes are applied live to the ViewModel which updates * the renderer on the map. */@Composablefun ApplyBlendRendererToHillshadeScreen(sampleName: String) { val viewModel: ApplyBlendRendererToHillshadeViewModel = viewModel()
// Dialog visibility state var isDialogOptionsVisible by remember { mutableStateOf(false) } Scaffold( topBar = { SampleTopAppBar(title = sampleName) }, floatingActionButton = { if (!isDialogOptionsVisible) { FloatingActionButton( modifier = Modifier.padding(bottom = 36.dp, end = 12.dp), onClick = { isDialogOptionsVisible = true } ) { Icon(Icons.Filled.Settings, contentDescription = "Show options") } } } ) { innerPadding -> Column(modifier = Modifier .fillMaxSize() .padding(innerPadding)) {
// MapView shows the map from the ViewModel MapView( modifier = Modifier .fillMaxSize() .weight(1f), arcGISMap = viewModel.arcGISMap )
// Settings dialog if (isDialogOptionsVisible) { DialogOptions( altitude = viewModel.altitude, onAltitudeChange = viewModel::updateAltitude, azimuth = viewModel.azimuth, onAzimuthChange = viewModel::updateAzimuth, slopeTypeOptions = ApplyBlendRendererToHillshadeViewModel.slopeTypeOptions, selectedSlopeType = viewModel.slopeType, onSlopeTypeSelected = viewModel::updateSlopeType, colorRampPresets = viewModel.colorRampPresets, selectedColorRampIndex = viewModel.selectedColorRampPresetIndex, onColorRampSelected = viewModel::updateColorRampPresetIndex, onDismissRequest = { isDialogOptionsVisible = false } ) }
// Message dialog for errors surfaced by the ViewModel viewModel.messageDialogVM.apply { if (dialogStatus) { MessageDialog( title = messageTitle, description = messageDescription, onDismissRequest = ::dismissDialog ) } } } }}
@Composableprivate fun DialogOptions( altitude: Double, onAltitudeChange: (Double) -> Unit, azimuth: Double, onAzimuthChange: (Double) -> Unit, slopeTypeOptions: List<Pair<String, SlopeType>>, selectedSlopeType: SlopeType, onSlopeTypeSelected: (SlopeType) -> Unit, colorRampPresets: List<String>, selectedColorRampIndex: Int, onColorRampSelected: (Int) -> Unit, onDismissRequest: () -> Unit) { SampleDialog(onDismissRequest = onDismissRequest) { Text("Renderer Settings", style = MaterialTheme.typography.titleMedium)
// Slope type dropdown val slopeLabels = slopeTypeOptions.map { it.first } val selectedSlopeIndex = slopeTypeOptions.indexOfFirst { it.second == selectedSlopeType }.coerceAtLeast(0) DropDownMenuBox( textFieldValue = slopeLabels.getOrNull(selectedSlopeIndex) ?: slopeLabels[0], textFieldLabel = "Slope Type", dropDownItemList = slopeLabels, onIndexSelected = { index -> onSlopeTypeSelected(slopeTypeOptions[index].second) } )
// Color ramp dropdown DropDownMenuBox( textFieldValue = colorRampPresets.getOrNull(selectedColorRampIndex) ?: colorRampPresets[0], textFieldLabel = "Color Ramp Preset", dropDownItemList = colorRampPresets, onIndexSelected = onColorRampSelected )
// Altitude slider Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(6.dp)) { Text("Altitude: ${altitude.toInt()}°") Slider( value = altitude.toFloat(), onValueChange = { onAltitudeChange(it.toDouble()) }, valueRange = 0f..360f ) }
// Azimuth slider Column(modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(6.dp)) { Text("Azimuth: ${azimuth.toInt()}°") Slider( value = azimuth.toFloat(), onValueChange = { onAzimuthChange(it.toDouble()) }, valueRange = 0f..360f ) }
// Dismiss button row Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically ) { Button(onClick = onDismissRequest, modifier = Modifier.padding(start = 8.dp)) { Text("Done") } } }}
@Preview(showBackground = true)@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)@Composablefun PreviewDialogOptions() { SamplePreviewSurface { DialogOptions( altitude = 45.0, onAltitudeChange = {}, azimuth = 0.0, onAzimuthChange = {}, slopeTypeOptions = ApplyBlendRendererToHillshadeViewModel.slopeTypeOptions, selectedSlopeType = SlopeType.Degree, onSlopeTypeSelected = {}, colorRampPresets = listOf("None", "DEM Light", "Screen Display", "Elevation"), selectedColorRampIndex = 0, onColorRampSelected = {}, onDismissRequest = {} ) }}