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";

function bindToggle(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLButtonElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    onChange(element.checked);
  };

  const callback = (v) => {
    element.checked = v;
    onChange(element.checked);
  };

  return [callback, element.checked, element];
}

function bindSlider(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLInputElement, got: " +
        element.constructor.name,
    );
  }

  element.oninput = function oninput() {
    onChange(element.valueAsNumber);
  };

  const setValue = (v) => {
    element.valueAsNumber = v;
    onChange(element.valueAsNumber);
  };

  const initialValue = element.valueAsNumber;

  return [setValue, initialValue, element];
}

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",
);

const extent = new Extent(
  "EPSG:3946",
  1837816.94334,
  1847692.32501,
  5170036.4587,
  5178412.82698,
);

const instance = new Instance({
  target: "view",
  crs: extent.crs,
});

const map = new Map({
  extent,
  // Enables hillshading on this map
  hillshading: {
    enabled: true,
    elevationLayersOnly: false,
  },
  backgroundColor: "white",
});
instance.add(map);

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: "orthophoto",
  extent: extent.split(2, 1)[0],
  source: colorSource,
});
map.addLayer(colorLayer);

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: "elevation",
  extent,
  minmax: { min, max },
  source: elevationSource,
});

map.addLayer(elevationLayer);

const mapCenter = extent.centerAsVector3();

instance.view.camera.position.set(mapCenter.x, mapCenter.y - 1, 10000);

const controls = new MapControls(instance.view.camera, instance.domElement);
controls.target = mapCenter;
controls.saveState();
controls.enableDamping = true;
controls.dampingFactor = 0.2;
controls.maxPolarAngle = Math.PI / 2.3;
instance.view.setControls(controls);

Inspector.attach("inspector", instance);

// Example GUI

const [, , colorLayersToggle] = bindToggle("colorLayers", (state) => {
  map.hillshading.elevationLayersOnly = !state;
  instance.notifyChange(map);
});

const [, , azimuthSlider] = bindSlider("azimuth", (azimuth) => {
  map.hillshading.azimuth = azimuth;
  instance.notifyChange(map);
});

const [, , zenithSlider] = bindSlider("zenith", (zenith) => {
  map.hillshading.zenith = zenith;
  instance.notifyChange(map);
});

bindToggle("enabled", (state) => {
  map.hillshading.enabled = state;
  instance.notifyChange(map);

  colorLayersToggle.disabled = !state;
  azimuthSlider.disabled = !state;
  zenithSlider.disabled = !state;
});

const [, , opacitySlider] = bindSlider("opacity", (percentage) => {
  const opacity = percentage / 100.0;
  colorLayer.opacity = opacity;
  instance.notifyChange(map);
  opacitySlider.innerHTML = `${percentage}%`;
});

bindSlider("intensity", (intensity) => {
  map.hillshading.intensity = intensity;
  instance.notifyChange(map);
});

bindSlider("zFactor", (zFactor) => {
  map.hillshading.zFactor = zFactor;
  instance.notifyChange(map);
});

const [, , stitchingToggle] = bindToggle("stitching", (enabled) => {
  map.terrain.stitching = enabled;
  instance.notifyChange(map);
});

bindToggle("terrainDeformation", (enabled) => {
  map.terrain.enabled = enabled;
  instance.notifyChange(map);
  stitchingToggle.disabled = !enabled;
});
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/next/css/example.css"
    />
  </head>

  <body>
    <div id="view" class="m-0 p-0 w-100 h-100"></div>
    <div
      id="inspector"
      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="stitching"
              autocomplete="off"
            />
            <label class="form-check-label" for="stitching"
              >Terrain stitching</label
            >
          </div>

          <div class="form-check form-switch">
            <input
              class="form-check-input"
              type="checkbox"
              checked="true"
              role="switch"
              id="enabled"
              autocomplete="off"
            />
            <label class="form-check-label" for="enabled">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="azimuth" class="form-label">Azimuth (0 - 360)</label>
          <div class="input-group">
            <input
              type="range"
              min="0"
              max="360"
              value="315"
              class="form-range"
              id="azimuth"
              autocomplete="off"
            />
          </div>

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

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

          <label for="intensity" 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="intensity"
              autocomplete="off"
            />
          </div>

          <label for="zFactor" 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="zFactor"
              autocomplete="off"
            />
          </div>

          <label for="opacity" class="form-label">Color layer opacity</label>
          <div class="input-group">
            <input
              type="range"
              min="0"
              max="100"
              value="100"
              class="form-range"
              id="opacity"
              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": "git+https://gitlab.com/giro3d/giro3d.git"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}