Illustrates the use of three.js physically accurate lights and shadows on Maps.

Giro3D version
THREE.js version
OpenLayers version
CRS
Memory usage (CPU)
Memory usage (GPU)
Frames
Clear color
Clear alpha
Status
Local clipping enabled
Capabilities
WebGL 2
Max texture units
Max texture size
Precision
Max fragment shader uniforms
Logarithmic depth buffer
Max shader attributes
Check shader errors
EXT_clip_control
EXT_color_buffer_float
EXT_color_buffer_half_float
EXT_conservative_depth
EXT_depth_clamp
EXT_float_blend
EXT_polygon_offset_clamp
EXT_texture_compression_bptc
EXT_texture_compression_rgtc
EXT_texture_filter_anisotropic
EXT_texture_mirror_clamp_to_edge
EXT_texture_norm16
NV_shader_noperspective_interpolation
OES_draw_buffers_indexed
OES_sample_variables
OES_shader_multisample_interpolation
OES_texture_float_linear
OVR_multiview2
WEBGL_clip_cull_distance
WEBGL_compressed_texture_astc
WEBGL_compressed_texture_etc
WEBGL_compressed_texture_etc1
WEBGL_compressed_texture_s3tc
WEBGL_compressed_texture_s3tc_srgb
WEBGL_debug_renderer_info
WEBGL_debug_shaders
WEBGL_lose_context
WEBGL_multi_draw
WEBGL_polygon_mode
WEBGL_stencil_texturing
MSAA
EDL
EDL Radius
EDL Strength
Inpainting
Inpainting steps
Inpainting depth contrib.
Point cloud occlusion
Type
FOV
Automatic plane computation
Far plane
Near plane
Max far plane
Min near plane
Width (pixels)
Height (pixels)
x
y
z
x
y
z
color
Enable cache
Default TTL (seconds)
Capacity (MB)
Capacity (entries)
Entries
Memory usage (approx)
Pending requests
Memory tracker
Identifier
Memory usage (CPU)
Memory usage (GPU)
Status
Render order
Enable
Plane normal X
Plane normal Y
Plane normal Z
Distance
Helper size
Negate plane
Visible
Freeze updates
Opacity
Show volumes
Volume color
Discard no-data values
Sidedness
Front
Depth test
Visible tiles
Reachable tiles
Loaded tiles
Cast shadow
Receive shadow
Tile width (pixels)
Tile height (pixels)
Show grid
Background
Background opacity
Show tiles outlines
Tile outline color
Show tile info
Show extent
Extent color
Subdivision threshold
Deformation
Wireframe
Tile subdivisions
Show collider meshes
CPU terrain
Stitching
Geometry pool
Enabled
Mode
LightBased
Hillshade intensity
Z factor
Hillshade zenith
Hillshade azimuth
Elevation layers only
Enable
Color
Opacity
X step
Y step
X Offset
Y Offset
Thickness
Enable
Color
Thickness
Opacity
Primary interval (m)
Secondary interval (m)
Brightness
Contrast
Saturation
Layer count
Render state
Normal
Layers
Identifier
Memory usage (CPU)
Memory usage (GPU)
Source CRS
Status
Resolution factor
Visible
Frozen
Interpretation
Loaded images
Blending mode
Normal
Brightness
Contrast
Saturation
Opacity
Show extent
Extent color
Enabled
Mode
Elevation
Lower bound
Upper bound
Type
Color space
Data type
Flip Y
Synchronous
CRS
Memory usage (CPU)
Memory usage (GPU)
Loaded/Requested
CRS
Zoom levels
Main URL
Inner source
Identifier
Memory usage (CPU)
Memory usage (GPU)
Source CRS
Status
Resolution factor
Visible
Frozen
Interpretation
Loaded images
Minimum elevation
Maximum elevation
Show extent
Extent color
Enabled
Mode
Elevation
Lower bound
Upper bound
Type
Color space
Data type
Flip Y
Synchronous
CRS
Memory usage (CPU)
Memory usage (GPU)
Loaded/Requested
CRS
Zoom levels
Main URL
Inner source
Show helpers
Show hidden objects
Name filter
Hierarchy
Properties
isObject3D
uuid
name
type
matrixAutoUpdate
matrixWorldAutoUpdate
matrixWorldNeedsUpdate
visible
castShadow
receiveShadow
frustumCulled
renderOrder
x
y
z
x
y
z

0% © Mapbox

three.js lights and shadows can be used on Maps.

index.js
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);
index.html
<!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 &lt;code&gt;Map&lt;/code&gt;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/latest/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>
package.json
{
    "name": "map_shadows",
    "dependencies": {
        "@giro3d/giro3d": "0.42.4"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}