Skip to content
View on GitHubSample viewer app

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.

Image of apply blend renderer to hillshade sample

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

  1. Create a Raster object from a raster file.
  2. Create a RasterLayer object from the raster.
  3. Create a Basemap object from the raster layer and set it to the map.
  4. Create another Raster object for elevation from a grayscale raster file.
  5. Create a BlendRenderer object, specifying the elevation raster, color ramp, and other properties.
  • If you specify a non-null color 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.
  1. 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

ApplyBlendRendererToHillshadeViewModel.ktApplyBlendRendererToHillshadeViewModel.ktMainActivity.ktDownloadActivity.ktApplyBlendRendererToHillshadeScreen.kt
Use dark colors for code blocksCopy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/* 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.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.arcgismaps.mapping.ArcGISMap
import com.arcgismaps.mapping.Basemap
import com.arcgismaps.mapping.layers.RasterLayer
import com.arcgismaps.mapping.symbology.raster.BlendRenderer
import com.arcgismaps.mapping.symbology.raster.ColorRamp
import com.arcgismaps.mapping.symbology.raster.PresetColorRampType
import com.arcgismaps.raster.Raster
import com.arcgismaps.raster.SlopeType
import com.arcgismaps.toolkit.geoviewcompose.MapViewProxy
import com.esri.arcgismaps.sample.sampleslib.components.MessageDialogViewModel
import kotlinx.coroutines.launch
import 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
        )
    }
}

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.