Terrain rendering
Illustrate the use of hillshading on maps with terrain.
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.
import { MapControls } from "three/examples/jsm/controls/MapControls.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 WmtsSource from "@giro3d/giro3d/sources/WmtsSource.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, min, max, step) => {
if (min != null && max != null) {
element.min = min.toString();
element.max = max.toString();
if (step != null) {
element.step = step;
}
}
element.valueAsNumber = v;
onChange(element.valueAsNumber);
};
const initialValue = element.valueAsNumber;
return [setValue, initialValue, element];
}
import { MapLightingMode } from "@giro3d/giro3d/entities/MapLightingOptions.js";
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",
);
Instance.registerCRS(
"IGNF:WGS84G",
'GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]]',
);
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
lighting: {
enabled: true,
mode: MapLightingMode.Hillshade,
},
backgroundColor: "white",
});
instance.add(map);
const url =
"https://data.geopf.fr/wmts?SERVICE=WMTS&VERSION=1.0.0&REQUEST=GetCapabilities";
const noDataValue = -1000;
let colorLayer;
// Let's build the elevation layer from the WMTS capabilities
WmtsSource.fromCapabilities(url, {
layer: "ELEVATION.ELEVATIONGRIDCOVERAGE.HIGHRES",
format: new BilFormat(),
noDataValue,
})
.then((elevationWmts) => {
map.addLayer(
new ElevationLayer({
name: "elevation",
extent: map.extent,
minmax: { min: 100, max: 300 },
source: elevationWmts,
}),
);
})
.catch(console.error);
// Let's build the color layer from the WMTS capabilities
WmtsSource.fromCapabilities(url, {
layer: "HR.ORTHOIMAGERY.ORTHOPHOTOS",
})
.then((orthophotoWmts) => {
colorLayer = new ColorLayer({
name: "color",
extent: map.extent.split(2, 1)[0],
source: orthophotoWmts,
});
map.addLayer(colorLayer);
})
.catch(console.error);
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.lighting.elevationLayersOnly = !state;
instance.notifyChange(map);
});
const [, , azimuthSlider] = bindSlider("azimuth", (azimuth) => {
map.lighting.hillshadeAzimuth = azimuth;
instance.notifyChange(map);
});
const [, , zenithSlider] = bindSlider("zenith", (zenith) => {
map.lighting.hillshadeZenith = zenith;
instance.notifyChange(map);
});
bindToggle("enabled", (state) => {
map.lighting.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.lighting.hillshadeIntensity = intensity;
instance.notifyChange(map);
});
bindSlider("zFactor", (zFactor) => {
map.lighting.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;
});
<!doctype html>
<html lang="en">
<head>
<title>Hillshading & 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/examples/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>
{
"name": "hillshade",
"dependencies": {
"@giro3d/giro3d": "git+https://gitlab.com/giro3d/giro3d.git"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}