Shadows are only available in light-based shading mode
Illustrates the use of three.js physically accurate lights and shadows on Map
s.
three.js lights and shadows can be used on Maps.
import colormap from "colormap";
import {
AmbientLight,
ArrowHelper,
BasicShadowMap,
BoxGeometry,
CameraHelper,
Clock,
Color,
DirectionalLight,
DirectionalLightHelper,
Group,
MathUtils,
Mesh,
MeshStandardMaterial,
PointLight,
PointLightHelper,
Vector3,
VSMShadowMap,
} from "three";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";
import XYZ from "ol/source/XYZ.js";
import ColorMap from "@giro3d/giro3d/core/ColorMap.js";
import { MapLightingMode } from "@giro3d/giro3d/entities/MapLightingOptions.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import Coordinates from "@giro3d/giro3d/core/geographic/Coordinates.js";
import Extent from "@giro3d/giro3d/core/geographic/Extent.js";
import Sun from "@giro3d/giro3d/core/geographic/Sun.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import ElevationLayer from "@giro3d/giro3d/core/layer/ElevationLayer.js";
import Map from "@giro3d/giro3d/entities/Map.js";
import MapboxTerrainFormat from "@giro3d/giro3d/formats/MapboxTerrainFormat.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import TiledImageSource from "@giro3d/giro3d/sources/TiledImageSource.js";
function bindButton(id, onClick) {
const element = document.getElementById(id);
if (!(element instanceof HTMLButtonElement)) {
throw new Error(
"invalid binding element: expected HTMLButtonElement, got: " +
element.constructor.name,
);
}
element.onclick = () => {
onClick(element);
};
return element;
}
function bindColorPicker(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() {
// Let's change the classification color with the color picker value
const hexColor = element.value;
onChange(new Color(hexColor));
};
const externalFunction = (v) => {
element.value = `#${new Color(v).getHexString()}`;
onChange(element.value);
};
return [externalFunction, new Color(element.value), element];
}
function bindDropDown(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLSelectElement)) {
throw new Error(
"invalid binding element: expected HTMLSelectElement, got: " +
element.constructor.name,
);
}
element.onchange = () => {
onChange(element.value);
};
const callback = (v) => {
element.value = v;
onChange(element.value);
};
const setOptions = (options) => {
const items = options.map(
(opt) =>
`<option value=${opt.id} ${opt.selected ? "selected" : ""}>${opt.name}</option>`,
);
element.innerHTML = items.join("\n");
};
return [callback, element.value, element, setOptions];
}
function bindNumericalDropDown(id, onChange) {
const element = document.getElementById(id);
if (!(element instanceof HTMLSelectElement)) {
throw new Error(
"invalid binding element: expected HTMLSelectElement, got: " +
element.constructor.name,
);
}
element.onchange = () => {
onChange(parseInt(element.value));
};
const callback = (v) => {
element.value = v.toString();
onChange(parseInt(element.value));
};
return [callback, parseInt(element.value), 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];
}
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 makeColorRamp(
preset,
discrete = false,
invert = false,
mirror = false,
) {
let nshades = discrete ? 10 : 256;
const values = colormap({ colormap: preset, nshades });
const colors = values.map((v) => new Color(v));
if (invert) {
colors.reverse();
}
if (mirror) {
const mirrored = [...colors, ...colors.reverse()];
return mirrored;
}
return colors;
}
function updateLabel(id, text) {
const element = document.getElementById(id);
if (!(element instanceof HTMLLabelElement)) {
throw new Error(
"invalid binding element: expected HTMLLabelElement, got: " +
element.constructor.name,
);
}
element.innerText = text;
}
Instance.registerCRS(
"EPSG:3482",
"+proj=tmerc +lat_0=31 +lon_0=-113.75 +k=0.999933333 +x_0=213360 +y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs",
);
const EXTENT_SIZE = 20_000;
const min = 1500;
const max = 2000;
// Monument Valley coordinates
const center = new Coordinates("EPSG:4326", -110.08252, 36.98715)
.as("EPSG:3857")
.toVector3();
const extent = Extent.fromCenterAndSize(
"EPSG:3857",
center,
EXTENT_SIZE,
EXTENT_SIZE,
);
const instance = new Instance({
target: "view",
crs: "EPSG:3857",
backgroundColor: null,
});
const map = new Map({
extent,
// Enables light-based shading on this map
lighting: {
enabled: true,
mode: MapLightingMode.LightBased,
},
discardNoData: true,
segments: 64,
subdivisionThreshold: 1,
backgroundColor: "#c0bfbc",
});
instance.add(map);
const northArrow = new ArrowHelper(
new Vector3(0, 1, 0),
new Vector3(center.x, extent.north + 500, min),
EXTENT_SIZE * 0.5,
"yellow",
EXTENT_SIZE * 0.1,
EXTENT_SIZE * 0.02,
);
instance.add(northArrow);
northArrow.updateMatrixWorld(true);
const token =
"pk.eyJ1IjoidG11Z3VldCIsImEiOiJjbGJ4dTNkOW0wYWx4M25ybWZ5YnpicHV6In0.KhDJ7W5N3d1z3ArrsDjX_A";
const elevationLayer = new ElevationLayer({
extent,
preloadImages: true,
minmax: { min, max },
colorMap: new ColorMap({ colors: makeColorRamp("turbidity"), min, max }),
source: new TiledImageSource({
extent,
format: new MapboxTerrainFormat(),
source: new XYZ({
projection: "EPSG:3857",
url: `https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=${token}`,
}),
}),
});
map.addLayer(elevationLayer).catch(console.error);
const colorLayer = new ColorLayer({
extent,
preloadImages: true,
source: new TiledImageSource({
extent,
source: new XYZ({
projection: "EPSG:3857",
url: `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.webp?access_token=${token}`,
}),
}),
});
map.addLayer(colorLayer).catch(console.error);
instance.view.camera.position.set(-12254256, 4417664, 9400);
const controls = new MapControls(instance.view.camera, instance.domElement);
controls.target.set(center.x, center.y, 1600);
controls.saveState();
controls.enableDamping = true;
controls.dampingFactor = 0.2;
instance.view.setControls(controls);
// Light & shadow management
const lightParams = {
zenith: 45,
azimuth: 315,
shadowBias: -0.0001,
normalBias: 0,
shadowMapType: VSMShadowMap,
enableShadows: true,
lightType: "directional",
shadowIntensity: 1,
distance: EXTENT_SIZE * 4,
directionalLightIntensity: 3,
pointLightIntensity: 20000000,
ambientIntensity: 0.5,
shadowVolumeSize: EXTENT_SIZE,
shadowVolumeNear: 50000,
shadowVolumeFar: 150000,
shadowMapResolution: 512,
color: new Color("white"),
ambientColor: new Color("white"),
showHelpers: false,
};
instance.renderer.shadowMap.enabled = true;
instance.renderer.shadowMap.type = BasicShadowMap;
let directionalLight;
let pointLight;
let pointLightHelper;
let directionalLightHelper;
let shadowCameraHelper;
const ambientLight = new AmbientLight("#dbf1ff", 0.5);
instance.add(ambientLight);
const createLights = () => {
if (directionalLight) {
directionalLight.target.removeFromParent();
directionalLight.dispose();
directionalLight.removeFromParent();
}
if (directionalLightHelper) {
directionalLightHelper.dispose();
directionalLightHelper.removeFromParent();
}
if (shadowCameraHelper) {
shadowCameraHelper.dispose();
shadowCameraHelper.removeFromParent();
}
if (pointLight) {
pointLight.dispose();
pointLight.removeFromParent();
}
if (pointLightHelper) {
pointLightHelper.dispose();
pointLightHelper.removeFromParent();
}
directionalLight = new DirectionalLight(
lightParams.color,
lightParams.directionalLightIntensity,
);
instance.add(directionalLight);
instance.add(directionalLight.target);
directionalLight.name = "sunlight";
directionalLight.target.name = "sunlight target";
directionalLight.castShadow = true;
directionalLight.position.set(center.x, center.y, lightParams.distance);
directionalLight.target.position.set(center.x, center.y, 2200);
const size = lightParams.shadowMapResolution;
directionalLight.shadow.mapSize.set(size, size);
directionalLight.shadow.bias = lightParams.shadowBias;
directionalLight.shadow.normalBias = lightParams.normalBias;
directionalLight.shadow.intensity = lightParams.shadowIntensity;
instance.renderer.shadowMap.type = lightParams.shadowMapType;
directionalLight.shadow.camera.top = lightParams.shadowVolumeSize;
directionalLight.shadow.camera.bottom = -lightParams.shadowVolumeSize;
directionalLight.shadow.camera.left = -lightParams.shadowVolumeSize;
directionalLight.shadow.camera.right = lightParams.shadowVolumeSize;
directionalLight.shadow.camera.near = lightParams.shadowVolumeNear;
directionalLight.shadow.camera.far = lightParams.shadowVolumeFar;
directionalLight.updateMatrixWorld(true);
directionalLight.target.updateMatrixWorld(true);
directionalLight.shadow.updateMatrices(directionalLight);
directionalLightHelper = new DirectionalLightHelper(
directionalLight,
200,
lightParams.color,
);
instance.add(directionalLightHelper);
shadowCameraHelper = new CameraHelper(directionalLight.shadow.camera);
instance.add(shadowCameraHelper);
pointLight = new PointLight(lightParams.color, 20_000_000, 4000);
pointLight.castShadow = true;
pointLight.shadow.bias = lightParams.shadowBias;
pointLight.shadow.normalBias = lightParams.normalBias;
pointLight.shadow.intensity = lightParams.shadowIntensity;
pointLight.shadow.camera.near = 1;
pointLight.shadow.camera.far = 10000;
pointLight.shadow.mapSize.set(size, size);
pointLight.position.set(center.x, center.y, min + 400);
pointLight.updateMatrixWorld(true);
pointLightHelper = new PointLightHelper(pointLight, 200, "black");
instance.add(pointLightHelper);
pointLightHelper.updateMatrixWorld(true);
instance.add(pointLight);
updateLightsAndHelpers();
};
createLights();
// Example GUI
function updatePointLight() {
pointLight.visible = lightParams.lightType === "point";
pointLight.intensity = lightParams.pointLightIntensity;
pointLight.shadow.intensity = lightParams.shadowIntensity;
pointLightHelper.visible = pointLight.visible && lightParams.showHelpers;
instance.notifyChange();
}
function updateDirectionalLight() {
const pos = Sun.getLocalPosition({
point: center,
zenith: lightParams.zenith,
azimuth: lightParams.azimuth,
distance: lightParams.distance,
});
directionalLight.position.copy(pos);
directionalLight.updateMatrixWorld(true);
directionalLight.target.updateMatrixWorld(true);
directionalLight.shadow.bias = lightParams.shadowBias;
directionalLight.shadow.normalBias = lightParams.normalBias;
directionalLight.shadow.intensity = lightParams.shadowIntensity;
directionalLight.shadow.camera.top = lightParams.shadowVolumeSize;
directionalLight.shadow.camera.bottom = -lightParams.shadowVolumeSize;
directionalLight.shadow.camera.left = -lightParams.shadowVolumeSize;
directionalLight.shadow.camera.right = lightParams.shadowVolumeSize;
directionalLight.shadow.camera.near = lightParams.shadowVolumeNear;
directionalLight.shadow.camera.far = lightParams.shadowVolumeFar;
directionalLight.shadow.camera.updateProjectionMatrix();
directionalLight.shadow.camera.updateMatrix();
directionalLightHelper.update();
directionalLightHelper.updateMatrixWorld(true);
shadowCameraHelper.update();
shadowCameraHelper.updateMatrixWorld(true);
directionalLight.intensity = lightParams.directionalLightIntensity;
directionalLight.visible = lightParams.lightType === "directional";
shadowCameraHelper.visible =
directionalLight.visible && lightParams.showHelpers;
directionalLightHelper.visible =
directionalLight.visible && lightParams.showHelpers;
instance.notifyChange();
}
function updateLightsAndHelpers() {
updateDirectionalLight();
updatePointLight();
northArrow.visible = lightParams.showHelpers;
instance.notifyChange();
}
const [setColorLayerToggle] = bindToggle("colorLayers", (state) => {
map.lighting.elevationLayersOnly = !state;
instance.notifyChange(map);
});
const [setAzimuth] = bindSlider("azimuth", (azimuth) => {
map.lighting.hillshadeAzimuth = azimuth;
lightParams.azimuth = azimuth;
updateLightsAndHelpers();
updateLabel("azimuth-label", `Azimuth: ${Math.round(azimuth)}°`);
instance.notifyChange(map);
});
const [setZenith] = bindSlider("zenith", (zenith) => {
map.lighting.hillshadeZenith = zenith;
lightParams.zenith = zenith;
updateLightsAndHelpers();
updateLabel("zenith-label", `Zenith: ${Math.round(zenith)}°`);
instance.notifyChange(map);
});
const [setLightColor] = bindColorPicker("color", (v) => {
lightParams.color = new Color(v);
directionalLight.color = lightParams.color;
pointLight.color = lightParams.color;
instance.notifyChange();
});
const [setAmbientColor] = bindColorPicker("ambient-color", (v) => {
lightParams.ambientColor = new Color(v);
ambientLight.color = lightParams.ambientColor;
instance.notifyChange();
});
const [setShadowMapResolution] = bindSlider("shadow-map-resolution", (size) => {
lightParams.shadowMapResolution = size;
createLights();
instance.notifyChange();
});
const [setShadowMapBias] = bindSlider("shadow-map-bias", (bias) => {
lightParams.shadowBias = bias;
updateLightsAndHelpers();
instance.notifyChange();
});
const [setShadowMapNormalBias] = bindSlider(
"shadow-map-normal-bias",
(bias) => {
lightParams.normalBias = bias;
updateLightsAndHelpers();
instance.notifyChange();
},
);
const [setShadowVolumeSize] = bindSlider("shadow-volume-size", (size) => {
lightParams.shadowVolumeSize = size;
updateLightsAndHelpers();
instance.notifyChange();
});
const [setLightType] = bindDropDown("light-type", (type) => {
lightParams.lightType = type;
document.getElementById("point-light-params").style.display =
lightParams.lightType === "point" ? "block" : "none";
document.getElementById("directional-light-params").style.display =
lightParams.lightType === "directional" ? "block" : "none";
updateLightsAndHelpers();
});
const [setEnableShadows] = bindToggle("enable-shadows", (v) => {
lightParams.enableShadows = v;
directionalLight.castShadow = v;
pointLight.castShadow = v;
instance.notifyChange();
});
const [setShadowMapType] = bindNumericalDropDown("shadow-map-type", (type) => {
lightParams.shadowMapType = type;
instance.renderer.shadowMap.type = type;
});
const [setMode] = bindNumericalDropDown("mode", (newMode) => {
const simpleGroup = document.getElementById("simpleGroup");
const realisticGroup = document.getElementById("realisticGroup");
const shadingGroup = document.getElementById("shadingParams");
const shadowGroup = document.getElementById("group-shadows");
const noShadowGroup = document.getElementById("group-noshadows");
switch (newMode) {
case -1:
map.lighting.enabled = false;
shadingGroup.style.display = "none";
shadowGroup.style.display = "none";
noShadowGroup.style.display = "block";
break;
case MapLightingMode.Hillshade:
shadingGroup.style.display = "block";
simpleGroup.style.display = "block";
realisticGroup.style.display = "none";
map.lighting.enabled = true;
map.lighting.mode = MapLightingMode.Hillshade;
shadowGroup.style.display = "none";
noShadowGroup.style.display = "block";
break;
case MapLightingMode.LightBased:
shadingGroup.style.display = "block";
simpleGroup.style.display = "none";
realisticGroup.style.display = "block";
map.lighting.enabled = true;
map.lighting.mode = MapLightingMode.LightBased;
shadowGroup.style.display = "block";
noShadowGroup.style.display = "none";
break;
}
instance.notifyChange(map);
});
const [setOpacity, , opacitySlider] = bindSlider("opacity", (percentage) => {
const opacity = percentage / 100.0;
colorLayer.opacity = opacity;
instance.notifyChange(map);
opacitySlider.innerHTML = `${percentage}%`;
});
const [setIntensity] = bindSlider("intensity", (intensity) => {
map.lighting.hillshadeIntensity = intensity;
instance.notifyChange(map);
});
const [setDirectionalLightIntensity] = bindSlider(
"directional-light-intensity",
(v) => {
lightParams.directionalLightIntensity = v;
directionalLight.intensity = v;
instance.notifyChange();
},
);
const [setPointLightIntensity] = bindSlider("point-light-intensity", (v) => {
lightParams.pointLightIntensity = v;
pointLight.intensity = v;
instance.notifyChange();
});
const [setShadowVolumeNear] = bindSlider("shadow-volume-near", (v) => {
lightParams.shadowVolumeNear = v;
directionalLight.shadow.camera.near = v;
updateLightsAndHelpers();
instance.notifyChange();
});
const [setShadowVolumeFar] = bindSlider("shadow-volume-far", (v) => {
lightParams.shadowVolumeFar = v;
directionalLight.shadow.camera.far = v;
updateLightsAndHelpers();
instance.notifyChange();
});
const [setAmbientIntensity] = bindSlider("ambient-intensity", (v) => {
lightParams.ambientIntensity = v;
ambientLight.intensity = v;
instance.notifyChange();
});
const [setShadowIntensity] = bindSlider("shadow-map-intensity", (v) => {
lightParams.shadowIntensity = v;
updateLightsAndHelpers();
instance.notifyChange();
});
const [setZFactor] = bindSlider("zFactor", (zFactor) => {
map.lighting.zFactor = zFactor;
instance.notifyChange(map);
});
const [setShowHelpers] = bindToggle("show-helpers", (enabled) => {
lightParams.showHelpers = enabled;
updateLightsAndHelpers();
instance.notifyChange();
});
const cubes = [];
const reset = () => {
cubes.forEach((c) => {
c.geometry.dispose();
c.material.dispose();
c.removeFromParent();
});
setColorLayerToggle(true);
setLightColor("white");
setAmbientColor("white");
setLightType("directional");
setIntensity(1);
setEnableShadows(true);
setShadowMapType(VSMShadowMap);
setShadowVolumeSize(EXTENT_SIZE);
setShadowMapResolution(4096);
setShadowMapBias(-0.0001);
setShadowMapNormalBias(0);
setShadowVolumeNear(50000);
setShadowVolumeFar(150000);
setDirectionalLightIntensity(5);
setPointLightIntensity(20000000);
setAmbientIntensity(1);
setShadowIntensity(1);
setZFactor(1);
setOpacity(100);
setMode(MapLightingMode.LightBased);
setShowHelpers(false);
setAzimuth(252);
setZenith(71);
updateLightsAndHelpers();
};
bindButton("reset", () => {
reset();
});
bindButton("create-cube", (btn) => {
btn.disabled = true;
const size = Math.random() * 500 + 100;
const cube = new Mesh(
new BoxGeometry(size, size, size),
new MeshStandardMaterial({
color: new Color().setHSL(Math.random(), 0.5, 0.5),
}),
);
cube.castShadow = true;
cube.receiveShadow = true;
cube.material.opacity = 0.5;
cube.material.transparent = true;
instance.add(cube);
cubes.push(cube);
const onMouseMove = (e) => {
const picked = instance.pickObjectsAt(e, {
sortByDistance: true,
filter: (p) => p.object !== cube,
})[0];
if (picked) {
const { x, y, z } = picked.point;
cube.position.set(x, y, z + size / 2);
cube.updateMatrixWorld(true);
instance.notifyChange();
}
};
instance.domElement.addEventListener("mousemove", onMouseMove);
instance.domElement.addEventListener("mousedown", (e) => {
cube.material.opacity = 1;
cube.material.transparent = false;
btn.disabled = false;
instance.domElement.removeEventListener("mousemove", onMouseMove);
instance.notifyChange();
});
});
reset();
instance.domElement.addEventListener("mousemove", (e) => {
const picked = instance.pickObjectsAt(e, { sortByDistance: true })[0];
if (picked) {
const { x, y, z } = picked.point;
pointLight.position.set(x, y, z + 200);
pointLight.updateMatrixWorld(true);
pointLightHelper.update();
pointLightHelper.updateMatrixWorld(true);
instance.notifyChange();
}
});
Inspector.attach("inspector", instance);
<!doctype html>
<html lang="en">
<head>
<title>Physical lights and shadow maps</title>
<meta charset="UTF-8" />
<meta name="name" content="map_shadows" />
<meta
name="description"
content="Illustrates the use of three.js physically accurate lights and shadows on <code>Map</code>s."
/>
<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"
/>
<style>
#view canvas {
background: rgb(132, 170, 182);
background: radial-gradient(
circle,
rgba(132, 170, 182, 1) 0%,
rgba(37, 44, 48, 1) 100%
);
}
</style>
</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" style="width: 20rem">
<!--Parameters -->
<div class="card-body">
<!-- Accordion -->
<div class="accordion" id="accordion">
<!-- Section: map -->
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#section-map"
aria-expanded="true"
aria-controls="section-map"
>
Options
</button>
</h2>
<div
id="section-map"
class="accordion-collapse collapse show p-3"
data-bs-parent="#accordion"
>
<button type="button" id="reset" class="btn btn-warning w-100">
<i class="bi bi-trash"></i>
Reset scene
</button>
<button
type="button"
id="create-cube"
class="btn btn-primary w-100 mt-2"
>
<i class="bi bi-box"></i>
Create cube
</button>
<!-- Shade color layers toggle -->
<div class="form-check form-switch mt-2">
<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>
<!-- Show helpers -->
<div class="form-check form-switch mt-2">
<input
class="form-check-input"
type="checkbox"
role="switch"
id="show-helpers"
autocomplete="off"
/>
<label class="form-check-label" for="show-helpers"
>Show helpers</label
>
</div>
<label for="opacity" class="form-label mt-2"
>Color layer opacity</label
>
<input
type="range"
min="0"
max="100"
value="100"
class="form-range"
id="opacity"
autocomplete="off"
/>
</div>
</div>
<!-- Section: lights -->
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#section-lights"
aria-expanded="true"
aria-controls="section-lights"
>
Lights
</button>
</h2>
<div
id="section-lights"
class="accordion-collapse collapse show p-3"
data-bs-parent="#accordion"
>
<!-- Shading mode -->
<div class="input-group">
<label class="input-group-text" for="mode">Shading model</label>
<select class="form-select" id="mode" autocomplete="off">
<option value="-1">Disabled</option>
<option value="0">Hillshading</option>
<option value="1" selected>Light-based</option>
</select>
</div>
<div id="shadingParams">
<!-- Azimuth slider -->
<label id="azimuth-label" for="azimuth" class="form-label mt-2"
>Azimuth: 315°</label
>
<input
type="range"
min="0"
max="360"
step="1"
value="315"
class="form-range"
id="azimuth"
autocomplete="off"
/>
<!-- Zenith slider -->
<label id="zenith-label" for="zenith" class="form-label mt-2"
>Zenith: 45°</label
>
<input
type="range"
min="0.1"
step="1"
max="90"
value="45"
class="form-range"
id="zenith"
autocomplete="off"
/>
<!-- Z-factor -->
<label for="zFactor" class="form-label mt-2">Z-factor</label>
<input
type="range"
min="0"
max="10"
value="1"
step="0.1"
class="form-range"
id="zFactor"
autocomplete="off"
/>
<div id="simpleGroup">
<!-- Simple shading intensity -->
<label for="intensity" class="form-label"
>Shade opacity</label
>
<input
type="range"
min="0"
max="1"
value="1"
step="0.1"
class="form-range"
id="intensity"
autocomplete="off"
/>
</div>
<!-- Group for light-based settings -->
<div id="realisticGroup">
<!-- Light type (only if realistic mode) -->
<div class="input-group mt-2">
<label class="input-group-text" for="light-type"
>Light type</label
>
<select
class="form-select"
id="light-type"
autocomplete="off"
>
<option value="directional" selected>Directional</option>
<option value="point">Point</option>
</select>
</div>
<!-- Directional light intensity -->
<div class="mt-2" id="directional-light-params">
<label for="directional-light-intensity" class="form-label"
>Light intensity</label
>
<input
type="number"
min="0"
max="99"
step="0.01"
value="3"
class="form-control"
id="directional-light-intensity"
autocomplete="off"
/>
</div>
<!-- Point light intensity -->
<div class="mt-2" id="point-light-params">
<label for="point-light-intensity" class="form-label"
>Light intensity</label
>
<input
type="number"
min="0"
max="20000000"
step="1000"
value="20000000"
class="form-control"
id="point-light-intensity"
autocomplete="off"
/>
</div>
<!-- Light color -->
<label for="color" class="form-label mt-2">Light color</label>
<input
type="color"
class="form-control form-control-color w-100"
id="color"
value="#ffffff"
title="color"
/>
<!-- Ambient light intensity -->
<label for="ambient-intensity" class="form-label mt-2"
>Ambient intensity</label
>
<input
type="number"
min="0"
max="2"
step="0.5"
value="0.5"
class="form-control"
id="ambient-intensity"
autocomplete="off"
/>
<!-- Ambient light color -->
<label for="ambient-color" class="form-label mt-2"
>Ambient color</label
>
<input
type="color"
class="form-control form-control-color w-100"
id="ambient-color"
value="#ffffff"
title="color"
/>
</div>
</div>
</div>
</div>
<!-- Section: shadows -->
<div class="accordion-item">
<h2 class="accordion-header">
<button
class="accordion-button"
type="button"
data-bs-toggle="collapse"
data-bs-target="#section-shadows"
aria-expanded="false"
aria-controls="section-shadows"
>
Shadows
</button>
</h2>
<div
id="section-shadows"
class="accordion-collapse collapse p-3"
data-bs-parent="#accordion"
>
<div id="group-shadows">
<!-- Toggle shadows -->
<div class="form-check form-switch">
<input
class="form-check-input"
type="checkbox"
checked="true"
role="switch"
id="enable-shadows"
autocomplete="off"
/>
<label class="form-check-label" for="enable-shadows"
>Enable shadows</label
>
</div>
<!-- Shadow type -->
<div class="input-group mt-2">
<label class="input-group-text" for="shadow-map-type"
>Shadow type</label
>
<select
class="form-select"
id="shadow-map-type"
autocomplete="off"
>
<option value="0">BasicShadowMap</option>
<option value="1">PCFShadowMap</option>
<option value="2">PCFSoftShadowMap</option>
<option value="3" selected>VSMShadowMap</option>
</select>
</div>
<!-- Shadow map texture size -->
<label for="shadow-map-resolution" class="form-label mt-2"
>Texture size</label
>
<input
type="number"
min="64"
max="4096"
value="4096"
class="form-control"
id="shadow-map-resolution"
autocomplete="off"
/>
<!-- Shadow map camera volume size (meters) -->
<label for="shadow-volume-size" class="form-label mt-2"
>Volume size</label
>
<input
type="number"
min="100"
max="100000"
value="100000"
class="form-control"
id="shadow-volume-size"
autocomplete="off"
/>
<!-- Shadow volume near plane -->
<label for="shadow-volume-near" class="form-label mt-2"
>Camera near plane</label
>
<input
type="number"
min="100"
max="100000"
value="5000"
class="form-control"
id="shadow-volume-near"
autocomplete="off"
/>
<!-- Shadow volume near plane -->
<label for="shadow-volume-far" class="form-label mt-2"
>Camera far plane</label
>
<input
type="number"
min="100"
max="100000"
value="50000"
class="form-control"
id="shadow-volume-far"
autocomplete="off"
/>
<!-- Shadow intensity -->
<label for="shadow-map-intensity" class="form-label mt-2"
>Intensity</label
>
<input
type="range"
min="0"
max="1"
value="1"
step="0.01"
class="form-range"
id="shadow-map-intensity"
autocomplete="off"
/>
<!-- Shadow map bias -->
<label for="shadow-map-bias" class="form-label mt-2"
>Bias</label
>
<input
type="number"
min="-0.01"
max="0.01"
step="0.0001"
value="-0.0001"
class="form-control"
id="shadow-map-bias"
autocomplete="off"
/>
<!-- Shadow map normal bias -->
<label for="shadow-map-normal-bias" class="form-label mt-2"
>Normal bias</label
>
<input
type="number"
min="-10"
max="10"
step="0.1"
value="0"
class="form-control"
id="shadow-map-normal-bias"
autocomplete="off"
/>
</div>
<div id="group-noshadows" style="display: none">
Shadows are only available in light-based shading mode
</div>
</div>
</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": "map_shadows",
"dependencies": {
"@giro3d/giro3d": "git+https://gitlab.com/giro3d/giro3d.git"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}