Enable hillshading and set sunlight azimuth and angle, as well as terrain deformation.

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,
    },
    segments: 64,
    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);
};

azimuthSlider.oninput = function oninput() {
    map.materialOptions.hillshading.azimuth = azimuthSlider.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>
  <meta charset="UTF-8">
  <title>Giro3D - Hillshading & terrain</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
  <style>
    body {
      padding: 0;
      margin: 0;
      width: 100vw;
      height: 100vh;
    }

    #viewerDiv {
      width: 100%;
      height: 100%;
    }

    #panelDiv {
      position: absolute;
      top: 0;
      left: 0;
    }
    
  </style>
</head>

<body>
  <div id="viewerDiv"></div>
  <div id="panelDiv"></div>
  <div class="m-2 position-absolute top-0 end-0">
    <div class="card m-1">
        <div class="card-header">Terrain rendering</div>

        <fieldset class="container m-1" id="terrainOptions">
            <div class="form-check form-switch">
                <input
                    class="form-check-input"
                    type="checkbox"
                    checked="true"
                    role="switch"
                    id="terrainDeformation"
                />
                <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"
                />
                <label class="form-check-label" for="terrainStitching">Terrain stitching</label>
            </div>
        </fieldset>

        <hr />

        <fieldset class="container m-1" id="hillshadingOptions">
            <div class="form-check form-switch">
                <input
                    class="form-check-input"
                    type="checkbox"
                    checked="true"
                    role="switch"
                    id="hillshadingCheckbox"
                />
                <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"
                />
                <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="slider"
                    id="azimuthSlider"
                />
            </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="slider" id="zenithSlider" />
            </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="slider"
                    id="intensitySlider"
                />
            </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="slider"
                    id="zFactorSlider"
                />
            </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="slider"
                    id="opacitySlider"
                />
            </div>
        </fieldset>
    </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.35.0"
  },
  "devDependencies": {
    "vite": "^3.2.3"
  },
  "scripts": {
    "start": "vite",
    "build": "vite build"
  }
}