Illustrate the use of hillshading on maps with terrain.

Terrain rendering
100% © IGN

Hillshading is a realistic shading technique that uses elevation data to simulate the sunlight behaviour. You can change the sun rays' orientation (azimuth) and slope (zenith). Terrain deformation can be toggled on and off. If toggled off, the map is displayed as a flat surface, while still retaining shading capabilities. Terrain stitching is a rendering technique that reduces cracks and visible seams at the boundary between neighbouring terrain tiles. Disabling stitching can improve performance at the cost of increased visual artifacts.

index.js
import { MapControls } from 'three/examples/jsm/controls/MapControls.js';

import TileWMS from 'ol/source/TileWMS.js';

import Extent from '@giro3d/giro3d/core/geographic/Extent.js';
import Instance from '@giro3d/giro3d/core/Instance.js';
import ColorLayer from '@giro3d/giro3d/core/layer/ColorLayer.js';
import ElevationLayer from '@giro3d/giro3d/core/layer/ElevationLayer.js';
import BilFormat from '@giro3d/giro3d/formats/BilFormat.js';
import Map from '@giro3d/giro3d/entities/Map.js';
import Inspector from '@giro3d/giro3d/gui/Inspector.js';
import TiledImageSource from '@giro3d/giro3d/sources/TiledImageSource.js';

// # Planar (EPSG:3946) viewer

// Defines projection that we will use (taken from https://epsg.io/3946, Proj4js section)
Instance.registerCRS(
    'EPSG:3946',
    '+proj=lcc +lat_1=45.25 +lat_2=46.75 +lat_0=46 +lon_0=3 +x_0=1700000 +y_0=5200000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs',
);

// Defines geographic extent: CRS, min/max X, min/max Y
const extent = new Extent('EPSG:3946', 1837816.94334, 1847692.32501, 5170036.4587, 5178412.82698);

// `viewerDiv` will contain Giro3D' rendering area (the canvas element)
const viewerDiv = document.getElementById('viewerDiv');
// Creates the Giro3D instance
const instance = new Instance(viewerDiv, { crs: extent.crs() });

// Adds the map that will contain the layers.
const map = new Map('planar', {
    extent,
    // Enables hillshading on this map
    hillshading: {
        enabled: true,
        elevationLayersOnly: false,
    },
    backgroundColor: 'white',
});
instance.add(map);

// Adds a WMS imagery layer
const colorSource = new TiledImageSource({
    source: new TileWMS({
        url: 'https://data.geopf.fr/wms-r',
        projection: 'EPSG:3946',
        params: {
            LAYERS: ['HR.ORTHOIMAGERY.ORTHOPHOTOS'],
            FORMAT: 'image/jpeg',
        },
    }),
});

const colorLayer = new ColorLayer({
    name: 'wms_imagery',
    extent: extent.split(2, 1)[0],
    source: colorSource,
});
map.addLayer(colorLayer);

// Adds a WMS elevation layer
const elevationSource = new TiledImageSource({
    source: new TileWMS({
        url: 'https://data.geopf.fr/wms-r',
        projection: 'EPSG:3946',
        crossOrigin: 'anonymous',
        params: {
            LAYERS: ['ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES'],
            FORMAT: 'image/x-bil;bits=32',
        },
    }),
    format: new BilFormat(),
    noDataValue: -1000,
});

const min = 149;
const max = 621;

const elevationLayer = new ElevationLayer({
    name: 'wms_elevation',
    extent,
    minmax: { min, max },
    source: elevationSource,
});

map.addLayer(elevationLayer);

const mapCenter = extent.centerAsVector3();

// Sets the camera position
instance.camera.camera3D.position.set(mapCenter.x, mapCenter.y - 1, 10000);

// Creates controls
const controls = new MapControls(instance.camera.camera3D, instance.domElement);

// Then looks at extent's center
controls.target = mapCenter;
controls.saveState();

controls.enableDamping = true;
controls.dampingFactor = 0.2;
controls.maxPolarAngle = Math.PI / 2.3;

instance.useTHREEControls(controls);

const hillshadingCheckbox = document.getElementById('hillshadingCheckbox');
const shadeColorLayersCheckbox = document.getElementById('colorLayers');
const terrainDeformationCheckbox = document.getElementById('terrainDeformation');
const terrainStitchingCheckbox = document.getElementById('terrainStitching');
const azimuthSlider = document.getElementById('azimuthSlider');
const zenithSlider = document.getElementById('zenithSlider');
const opacitySlider = document.getElementById('opacitySlider');
const intensitySlider = document.getElementById('intensitySlider');
const zFactorSlider = document.getElementById('zFactorSlider');

hillshadingCheckbox.oninput = function oninput() {
    const state = hillshadingCheckbox.checked;
    map.materialOptions.hillshading.enabled = state;
    instance.notifyChange(map);

    shadeColorLayersCheckbox.disabled = !state;
    azimuthSlider.disabled = !state;
    zenithSlider.disabled = !state;
};

shadeColorLayersCheckbox.oninput = function oninput() {
    const state = shadeColorLayersCheckbox.checked;
    map.materialOptions.hillshading.elevationLayersOnly = !state;
    instance.notifyChange(map);
};

opacitySlider.oninput = function oninput() {
    const percentage = opacitySlider.value;
    const opacity = percentage / 100.0;
    colorLayer.opacity = opacity;
    instance.notifyChange(map);
    opacitySlider.innerHTML = `${percentage}%`;
};

azimuthSlider.oninput = function oninput() {
    map.materialOptions.hillshading.azimuth = azimuthSlider.value;
    instance.notifyChange(map);
};

zenithSlider.oninput = function oninput() {
    map.materialOptions.hillshading.zenith = zenithSlider.value;
    instance.notifyChange(map);
};

intensitySlider.oninput = function oninput() {
    map.materialOptions.hillshading.intensity = intensitySlider.value;
    instance.notifyChange(map);
};

intensitySlider.oninput = function oninput() {
    map.materialOptions.hillshading.intensity = intensitySlider.value;
    instance.notifyChange(map);
};

zFactorSlider.oninput = function oninput() {
    map.materialOptions.hillshading.zFactor = zFactorSlider.value;
    instance.notifyChange(map);
};

terrainDeformationCheckbox.oninput = function oninput() {
    const state = terrainDeformationCheckbox.checked;
    map.materialOptions.terrain.enabled = state;
    instance.notifyChange(map);
    terrainStitchingCheckbox.disabled = !state;
};

terrainStitchingCheckbox.oninput = function oninput() {
    const state = terrainStitchingCheckbox.checked;
    map.materialOptions.terrain.stitching = state;
    instance.notifyChange(map);
};

Inspector.attach(document.getElementById('panelDiv'), instance);
index.html
<!doctype html>
<html lang="en">
    <head>
        <title>Hillshading &amp; terrain</title>
        <meta charset="UTF-8" />
        <meta name="name" content="hillshade" />
        <meta name="description" content="Illustrate the use of hillshading on maps with terrain." />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />

        <link rel="icon" href="https://giro3d.org/images/favicon.svg" />
        <link href="https://giro3d.org/assets/bootstrap-custom.css" rel="stylesheet" />
        <script src="https://giro3d.org/assets/bootstrap.bundle.min.js"></script>
        <link
            rel="stylesheet"
            type="text/css"
            href="https://giro3d.org/latest/css/example.css"
        />

        
    </head>

    <body>
        <div id="viewerDiv" class="m-0 p-0 w-100 h-100"></div>
        <div id="panelDiv" class="position-absolute top-0 start-0 mh-100 overflow-auto"></div>

        <div class="side-pane-with-status-bar">
            <div class="card">
                <div class="card-header">Terrain rendering</div>

                <div class="card-body">
                    <div class="form-check form-switch">
                        <input
                            class="form-check-input"
                            type="checkbox"
                            checked="true"
                            role="switch"
                            id="terrainDeformation"
                            autocomplete="off"
                        />
                        <label class="form-check-label" for="terrainDeformation">Terrain deformation</label>
                    </div>

                    <div class="form-check form-switch">
                        <input
                            class="form-check-input"
                            type="checkbox"
                            checked="true"
                            role="switch"
                            id="terrainStitching"
                            autocomplete="off"
                        />
                        <label class="form-check-label" for="terrainStitching">Terrain stitching</label>
                    </div>

                    <div class="form-check form-switch">
                        <input
                            class="form-check-input"
                            type="checkbox"
                            checked="true"
                            role="switch"
                            id="hillshadingCheckbox"
                            autocomplete="off"
                        />
                        <label class="form-check-label" for="hillshadingCheckbox">Hillshading</label>
                    </div>

                    <div class="form-check form-switch">
                        <input
                            class="form-check-input"
                            type="checkbox"
                            checked="true"
                            role="switch"
                            id="colorLayers"
                            autocomplete="off"
                        />
                        <label class="form-check-label" for="colorLayers">Shade color layers</label>
                    </div>

                    <label for="azimuthSlider" class="form-label">Azimuth (0 - 360)</label>
                    <div class="input-group">
                        <input
                            type="range"
                            min="0"
                            max="360"
                            value="315"
                            class="form-range"
                            id="azimuthSlider"
                            autocomplete="off"
                        />
                    </div>

                    <div class="my-2"></div>

                    <label for="zenithSlider" class="form-label">Zenith (0 - 90)</label>
                    <div class="input-group">
                        <input
                            type="range"
                            min="0"
                            max="90"
                            value="45"
                            class="form-range"
                            id="zenithSlider"
                            autocomplete="off"
                        />
                    </div>

                    <label for="intensitySlider" class="form-label">Intensity</label>
                    <div class="input-group">
                        <input
                            type="range"
                            min="0"
                            max="1"
                            value="1"
                            step="0.1"
                            class="form-range"
                            id="intensitySlider"
                            autocomplete="off"
                        />
                    </div>

                    <label for="zFactorSlider" class="form-label">Z-factor</label>
                    <div class="input-group">
                        <input
                            type="range"
                            min="0"
                            max="10"
                            value="1"
                            step="0.1"
                            class="form-range"
                            id="zFactorSlider"
                            autocomplete="off"
                        />
                    </div>

                    <label for="opacitySlider" class="form-label">Color layer opacity</label>
                    <div class="input-group">
                        <input
                            type="range"
                            min="0"
                            max="100"
                            value="100"
                            class="form-range"
                            id="opacitySlider"
                            autocomplete="off"
                        />
                    </div>
                </div>
            </div>
        </div>

        <script type="module" src="index.js"></script>
        <script>
            /* activate popovers */
            const popoverTriggerList = [].slice.call(
                document.querySelectorAll('[data-bs-toggle="popover"]'),
            );
            popoverTriggerList.map(
                // bootstrap is used as script in the template, disable warning about undef
                // eslint-disable-next-line no-undef
                popoverTriggerEl =>
                    new bootstrap.Popover(popoverTriggerEl, {
                        trigger: 'hover',
                        placement: 'left',
                        content: document.getElementById(
                            popoverTriggerEl.getAttribute('data-bs-content'),
                        ).innerHTML,
                        html: true,
                    }),
            );
        </script>
    </body>
</html>
package.json
{
    "name": "hillshade",
    "dependencies": {
        "@giro3d/giro3d": "0.37.3"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}