Display a globe-shaped Map.

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)
Horizon distance
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
🪟 Opacity
🔳 Show volumes
Volume color
Discard no-data values
Sidedness
Front
Depth test
Visible tiles
Reachable tiles
Loaded tiles
Horizon culling
Complete paints
Cast shadow
Receive shadow
Show grid
Background
Background opacity
Show tiles outlines
Tile outline color
Show extent
Extent color
Show bounding spheres
Subdivision threshold
Show tile info
Image size
Elevation range
Layer info
Deformation
Wireframe
Tile subdivisions
Show collider meshes
Stitching
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)
Name
Source CRS
Status
Resolution factor
Visible
Frozen
Interpretation
Loaded images
Blending mode
Add
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)
Identifier
Memory usage (CPU)
Memory usage (GPU)
Name
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)
Name
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)
Name
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)
Feature count
Identifier
Memory usage (CPU)
Memory usage (GPU)
Name
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
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
🪟 Opacity
🔳 Show volumes
Volume color
Discard no-data values
Sidedness
Front
Depth test
Visible tiles
Reachable tiles
Loaded tiles
Horizon culling
Complete paints
Cast shadow
Receive shadow
Show grid
Background
Background opacity
Show tiles outlines
Tile outline color
Show extent
Extent color
Show bounding spheres
Subdivision threshold
Show tile info
Image size
Elevation range
Layer info
Deformation
Wireframe
Tile subdivisions
Show collider meshes
Stitching
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)
URL
Channel mapping
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
🪟 Opacity
🔳 Show volumes
Volume color
Discard no-data values
Sidedness
Front
Depth test
Visible tiles
Reachable tiles
Loaded tiles
Horizon culling
Complete paints
Cast shadow
Receive shadow
Show grid
Background
Background opacity
Show tiles outlines
Tile outline color
Show extent
Extent color
Show bounding spheres
Subdivision threshold
Show tile info
Image size
Elevation range
Layer info
Deformation
Wireframe
Tile subdivisions
Show collider meshes
Stitching
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)
URL
Channel mapping
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
🪟 Opacity
🔳 Show volumes
Volume color
Discard no-data values
Sidedness
Front
Depth test
Visible tiles
Reachable tiles
Loaded tiles
Horizon culling
Complete paints
Cast shadow
Receive shadow
Show grid
Background
Background opacity
Show tiles outlines
Tile outline color
Show extent
Extent color
Show bounding spheres
Subdivision threshold
Show tile info
Image size
Elevation range
Layer info
Deformation
Wireframe
Tile subdivisions
Show collider meshes
Stitching
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)
URL
Channel mapping
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
🪟 Opacity
🔳 Show volumes
Volume color
Discard no-data values
Sidedness
Front
Depth test
Visible tiles
Reachable tiles
Loaded tiles
Complete paints
Cast shadow
Receive shadow
Show grid
Background
Background opacity
Show tiles outlines
Tile outline color
Show extent
Extent color
Show bounding spheres
Subdivision threshold
Show tile info
Image size
Elevation range
Layer info
Deformation
Wireframe
Tile subdivisions
Show collider meshes
Stitching
Enabled
Mode
Hillshade
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)
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
🪟 Opacity
Inner
Outer
Red wavelength
Green wavelength
Blue wavelength
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
🪟 Opacity
Inner
Outer
Red wavelength
Green wavelength
Blue wavelength
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
🪟 Opacity
Color
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
Enabled
Zoom speed
Damping
Parameters
  • Layers

00:00
12:00
24:00
index.js
import { AmbientLight, DirectionalLight, MathUtils, Vector3 } from "three";

import { TopoJSON } from "ol/format.js";
import OSM from "ol/source/OSM.js";
import XYZ from "ol/source/XYZ.js";
import { Fill, Style } from "ol/style.js";

import GlobeControls from "@giro3d/giro3d/controls/GlobeControls.js";
import ColorMap from "@giro3d/giro3d/core/ColorMap.js";
import Ellipsoid from "@giro3d/giro3d/core/geographic/Ellipsoid.js";
import Extent from "@giro3d/giro3d/core/geographic/Extent.js";
import Sun from "@giro3d/giro3d/core/geographic/Sun.js";
import Instance from "@giro3d/giro3d/core/Instance.js";
import BlendingMode from "@giro3d/giro3d/core/layer/BlendingMode.js";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer.js";
import ElevationLayer from "@giro3d/giro3d/core/layer/ElevationLayer.js";
import Atmosphere from "@giro3d/giro3d/entities/Atmosphere.js";
import Globe from "@giro3d/giro3d/entities/Globe.js";
import Glow from "@giro3d/giro3d/entities/Glow.js";
import SphericalPanorama from "@giro3d/giro3d/entities/SphericalPanorama.js";
import MapboxTerrainFormat from "@giro3d/giro3d/formats/MapboxTerrainFormat.js";
import Inspector from "@giro3d/giro3d/gui/Inspector.js";
import GeoTIFFSource from "@giro3d/giro3d/sources/GeoTIFFSource.js";
import StaticImageSource from "@giro3d/giro3d/sources/StaticImageSource.js";
import TiledImageSource from "@giro3d/giro3d/sources/TiledImageSource.js";
import VectorSource from "@giro3d/giro3d/sources/VectorSource.js";
import GlobeControlsInspector from "@giro3d/giro3d/gui/GlobeControlsInspector.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 bindDatePicker(id, onChange) {
  const element = document.getElementById(id);
  if (!(element instanceof HTMLInputElement)) {
    throw new Error(
      "invalid binding element: expected HTMLInputElement, got: " +
        element.constructor.name,
    );
  }

  element.onchange = () => {
    onChange(new Date(element.value));
  };

  const callback = (v) => {
    const clone = new Date(v.getTime());
    v.setMinutes(v.getMinutes() - v.getTimezoneOffset());
    element.value = clone.toISOString().slice(0, 10);
    onChange(new Date(element.value));
  };

  return [callback, new Date(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 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;
}

const instance = new Instance({
  target: "view",
  crs: "EPSG:4978",
  backgroundColor: "black",
});

/////////////////////////////// Globe creations ///////////////////////////////////////////////////

const earth = new Globe({
  lighting: {
    enabled: true,
  },
  graticule: {
    enabled: true,
    color: "black",
    xStep: 10, // In degrees
    yStep: 10, // In degrees
    xOffset: 0,
    yOffset: 0,
    opacity: 0.5,
    thickness: 0.5, // In degrees
  },
  backgroundColor: "#001B35",
});

earth.name = "Earth";

instance.add(earth);

const moon = new Globe({
  lighting: {
    enabled: true,
  },
  graticule: {
    enabled: true,
    color: "black",
    xStep: 10, // In degrees
    yStep: 10, // In degrees
    xOffset: 0,
    yOffset: 0,
    opacity: 0.5,
    thickness: 0.5, // In degrees
  },
  backgroundColor: "grey",
  // For the moon we use a custom ellipsoid
  ellipsoid: new Ellipsoid({
    semiMajorAxis: 1_738_100,
    semiMinorAxis: 1_736_000,
  }),
});

moon.name = "Moon";

instance.add(moon);

const moonLayer = new ColorLayer({
  source: new GeoTIFFSource({
    url: "https://3d.oslandia.com/giro3d/rasters/moon.tif",
    crs: "EPSG:4326",
  }),
});

moon.addLayer(moonLayer);

const mars = new Globe({
  lighting: {
    enabled: true,
  },
  graticule: {
    enabled: true,
    color: "black",
    xStep: 10, // In degrees
    yStep: 10, // In degrees
    xOffset: 0,
    yOffset: 0,
    opacity: 0.5,
    thickness: 0.5, // In degrees
  },
  backgroundColor: "#C64600",
  // For Mars we use a custom ellipsoid
  // See https://tharsis.gsfc.nasa.gov/geodesy.html
  ellipsoid: new Ellipsoid({
    semiMajorAxis: 3_396_200,
    semiMinorAxis: 3_376_189,
  }),
});

mars.name = "Mars";

instance.add(mars);

const marsLayer = new ColorLayer({
  source: new GeoTIFFSource({
    // From https://www.solarsystemscope.com/textures/
    url: "https://3d.oslandia.com/giro3d/rasters/8k_mars.tif",
    crs: "EPSG:4326",
  }),
});

mars.addLayer(marsLayer);

// The sun is so huge that it would be impractical
// to display it in its actual scale.
const SUN_SIZE_FACTOR = 0.1;

const sun = new Globe({
  lighting: {
    enabled: false,
  },
  graticule: {
    enabled: true,
    color: "black",
    xStep: 10, // In degrees
    yStep: 10, // In degrees
    xOffset: 0,
    yOffset: 0,
    opacity: 0.5,
    thickness: 0.5, // In degrees
  },
  backgroundColor: "grey",
  // For the sun we use a spherical ellipsoid
  ellipsoid: new Ellipsoid({
    semiMajorAxis: 696_340_000 * SUN_SIZE_FACTOR,
    semiMinorAxis: 696_340_000 * SUN_SIZE_FACTOR,
  }),
});

sun.name = "Sun";

instance.add(sun);

const sunLayer = new ColorLayer({
  source: new GeoTIFFSource({
    // From https://www.solarsystemscope.com/textures/
    url: "https://3d.oslandia.com/giro3d/rasters/8k_sun.tif",
    crs: "EPSG:4326",
  }),
});

sun.addLayer(sunLayer);

const allGlobes = [earth, moon, mars, sun];

/////////////////////////////// Star background /////////////////////////////////////////////////

const background = new SphericalPanorama({
  radius: 10_000_000,
  subdivisionThreshold: 0.4,
  depthTest: false,
});
background.name = "background";
background.renderOrder = -9999;
instance.add(background);

const starLayer = new ColorLayer({
  source: new StaticImageSource({
    source: "https://3d.oslandia.com/giro3d/images/4k_stars_milky_way.jpg",
    extent: Extent.fullEquirectangularProjection,
  }),
});

background.addLayer(starLayer);

/////////////////////////////// Earth layers ////////////////////////////////////////////////////

const mapboxApiKey =
  "pk.eyJ1IjoidG11Z3VldCIsImEiOiJjbGJ4dTNkOW0wYWx4M25ybWZ5YnpicHV6In0.KhDJ7W5N3d1z3ArrsDjX_A";

// Adds a XYZ elevation layer with MapBox terrain RGB tileset
const elevationLayer = new ElevationLayer({
  name: "elevation",
  preloadImages: true,
  colorMap: new ColorMap({
    colors: makeColorRamp("greens"),
    min: -1500,
    max: 6000,
  }),
  minmax: { min: -500, max: 8000 },
  // We dont want the full resolution because the terrain
  // mesh has a much lower resolution than the raster image
  resolutionFactor: 1 / 8,
  source: new TiledImageSource({
    retries: 0,
    format: new MapboxTerrainFormat(),
    source: new XYZ({
      url: `https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=${mapboxApiKey}`,
      projection: "EPSG:3857",
    }),
  }),
});
earth.addLayer(elevationLayer).catch(console.error);

const watermask = new ColorLayer({
  name: "watermask",
  source: new VectorSource({
    dataProjection: "EPSG:4326",
    data: {
      url: "https://3d.oslandia.com/giro3d/vectors/water_mask.topojson",
      format: new TopoJSON(),
    },
    style: new Style({
      fill: new Fill({
        color: "#22274a",
      }),
    }),
  }),
});

earth.addLayer(watermask);

// Adds a XYZ color layer with MapBox satellite tileset
const satellite = new ColorLayer({
  name: "satellite",
  preloadImages: true,
  source: new TiledImageSource({
    source: new XYZ({
      url: `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.webp?access_token=${mapboxApiKey}`,
      projection: "EPSG:3857",
      crossOrigin: "anonymous",
    }),
  }),
});
earth.addLayer(satellite).catch((e) => console.error(e));

// Create the OpenStreetMap color layer using an OpenLayers source.
// See https://openlayers.org/en/latest/apidoc/module-ol_source_OSM-OSM.html
// for more informations.
const osm = new ColorLayer({
  name: "OSM",
  source: new TiledImageSource({ source: new OSM() }),
});
earth.addLayer(osm).catch((e) => console.error(e));

const clouds = new ColorLayer({
  name: "clouds",
  blendingMode: BlendingMode.Add,
  source: new StaticImageSource({
    source: "https://3d.oslandia.com/giro3d/images/cloud_cover.webp",
    extent: Extent.WGS84,
  }),
});
earth.addLayer(clouds).catch(console.error);

/////////////////////////////// Lighting //////////////////////////////////////////////////////

// Let's add a sun in our scene
const sunlight = new DirectionalLight("white", 4);
sunlight.name = "sun";

instance.add(sunlight);
instance.add(sunlight.target);

sunlight.updateMatrixWorld(true);

const ambientLight = new AmbientLight("white", 0.3);
instance.add(ambientLight);

/////////////////////////////// Atmospheres //////////////////////////////////////////////////

const earthAtmosphere = new Atmosphere({ ellipsoid: earth.ellipsoid });
earthAtmosphere.name = "Earth atmosphere";
instance.add(earthAtmosphere);

const marsAtmosphere = new Atmosphere({
  ellipsoid: mars.ellipsoid,
  wavelengths: [0.414, 0.443, 0.475], // To give the atmosphere the rusty color of Mars
});
marsAtmosphere.name = "Mars atmosphere";
instance.add(marsAtmosphere);

// For the sun we don't use an atmosphere, but a glow
const sunGlow = new Glow({
  color: "#ff7800",
  ellipsoid: sun.ellipsoid,
});

sunGlow.name = "sun glow";

instance.add(sunGlow);

/////////////////////////////// Camera & controls ///////////////////////////////////////////

const defaultCameraPosition = new Vector3(
  35_785_000 + Ellipsoid.WGS84.semiMajorAxis,
  0,
  0,
);

// Geostationary orbit at 36,000 km
instance.view.camera.position.copy(defaultCameraPosition);
instance.view.camera.lookAt(new Vector3(0, 0, 0));

let controls;

const updateControls = () => {
  if (controls) {
    controls.update();
    instance.notifyChange();
  }

  requestAnimationFrame(updateControls);
};

updateControls();

/////////////////////////////// Example GUI bindings ///////////////////////////////////////////

const [setGraticule] = bindToggle("graticule", (enabled) => {
  allGlobes.forEach((g) => (g.graticule.enabled = enabled));
  instance.notifyChange(allGlobes);
});

setGraticule(earth.graticule.enabled);

const [setAtmosphere] = bindToggle("atmosphere", (enabled) => {
  earthAtmosphere.visible = enabled && earth.visible;
  marsAtmosphere.visible = enabled && mars.visible;
  sunGlow.visible = enabled && sun.visible;

  instance.notifyChange([earthAtmosphere, marsAtmosphere, sunGlow]);
});

const getActiveGlobe = () => {
  return allGlobes.find((g) => g.visible);
};

function update() {
  const globe = getActiveGlobe();

  if (globe == null) {
    return;
  }

  const { x, y, z } = instance.view.camera.position;
  let altitude = globe.ellipsoid.toGeodetic(x, y, z).altitude;
  altitude = MathUtils.clamp(altitude, 2, +Infinity);

  // Let's adjust the graticule step and thickness so that
  // it more or less always look the same when altitude changes.
  if (earth.graticule.enabled) {
    let step = 0;
    if (altitude > 10_000_000) {
      step = 10;
    } else if (altitude > 3_000_000) {
      step = 5;
    } else if (altitude > 1_000_000) {
      step = 2;
    } else if (altitude > 500_000) {
      step = 1;
    } else {
      step = 0.5;
    }

    const thickness = MathUtils.mapLinear(
      altitude,
      200,
      39_000_000,
      0.002,
      0.9,
    );

    earth.graticule.xStep = step;
    earth.graticule.yStep = step;
    earth.graticule.thickness = thickness;
  }

  // Let's make the clouds transparent when we zoom in.
  const opacity = MathUtils.mapLinear(altitude, 12_000_000, 30_000_000, 0, 1);
  clouds.opacity = MathUtils.clamp(opacity, 0, 1);
  earthAtmosphere.opacity = clouds.opacity;

  // Let's increase the shading on the terrain when we zoom out
  const zFactor = MathUtils.mapLinear(altitude, 12_000_000, 30_000_000, 1, 10);
  earth.lighting.zFactor = MathUtils.clamp(zFactor, 1, 10);

  background.object3d.position.set(x, y, z);
  background.object3d.updateMatrixWorld(true);
}

update();

const updateColorMap = () => {
  const minmax = earth.getElevationMinMaxForVisibleTiles();

  if (minmax != null && isFinite(minmax.min) && isFinite(minmax.max)) {
    const colorMap = elevationLayer.colorMap;
    colorMap.min = MathUtils.lerp(minmax.min, colorMap.min, 0.8);
    colorMap.max = MathUtils.lerp(minmax.max, colorMap.max, 0.8);

    instance.notifyChange(elevationLayer);
  }
};

setInterval(updateColorMap, 50);

instance.addEventListener("after-camera-update", update);

const sunParams = {
  latitude: 9,
  longitude: -41,
};

const updateSunDirection = (latitude, longitude) => {
  const position = Ellipsoid.WGS84.toCartesian(
    sunParams.latitude,
    sunParams.longitude,
    50_000_000,
  );

  sunlight.position.copy(position);
  sunlight.target.position.set(0, 0, 0);
  sunlight.target.updateMatrixWorld(true);
  sunlight.updateMatrixWorld(true);

  const normal = Ellipsoid.WGS84.getNormal(
    sunParams.latitude,
    sunParams.longitude,
  );
  earthAtmosphere.setSunPosition(position);
  marsAtmosphere.setSunPosition(position);
};

const [setSunLatitude] = bindSlider("sunLatitude", (lat) => {
  sunParams.latitude = lat;
  updateSunDirection(sunParams.latitude, sunParams.longitude);
  updateLabel(
    "sunLatitudeLabel",
    `Lat: ${Math.round(Math.abs(lat))}° ${lat >= 0 ? "N" : "S"}`,
  );
});

const [setSunLongitude] = bindSlider("sunLongitude", (lon) => {
  sunParams.longitude = lon;
  updateSunDirection(sunParams.latitude, sunParams.longitude);
  updateLabel(
    "sunLongitudeLabel",
    `Lon: ${Math.round(Math.abs(lon))} ${lon >= 0 ? "E" : "W"}°`,
  );
});

const [setLighting] = bindToggle("lighting", (enabled) => {
  earth.lighting.enabled = enabled;
  document.getElementById("lightingParams").style.display = enabled
    ? "block"
    : "none";
  instance.notifyChange(earth);
});

function setSunPosition(date) {
  const sunPosition = Sun.getGeographicPosition(date);

  setSunLongitude(sunPosition.longitude);
  setSunLatitude(sunPosition.latitude);
}

let date = new Date();

const [setDate] = bindDatePicker("date", (date) => {
  setSunPosition(date);
});

const [setTime] = bindSlider("time", (seconds) => {
  const h = seconds / 3600;
  const wholeH = Math.floor(h);

  const m = (h - wholeH) * 60;
  const wholeM = Math.floor(m);

  date.setUTCHours(wholeH, wholeM);

  setSunPosition(date);

  document.getElementById("timeLabel").innerText =
    `${wholeH.toString().padStart(2, "0")}:${wholeM.toString().padStart(2, "0")} UTC`;
});

const setCurrentDate = (date) => {
  setSunPosition(date);
  setDate(date);
  setTime(
    date.getUTCHours() * 3600 +
      date.getUTCMinutes() * 60 +
      date.getUTCSeconds(),
  );
};

bindButton("now", () => {
  date = new Date();
  setCurrentDate(date);
});

const [setSunPositionMode] = bindDropDown("sun-position-mode", (newMode) => {
  const datePicker = document.getElementById("date-picker");
  const locationPicker = document.getElementById("sun-location");
  const timeSlider = document.getElementById("timeContainer");

  datePicker.style.display = "none";
  locationPicker.style.display = "none";
  timeSlider.style.display = "none";

  switch (newMode) {
    case "custom-date":
      datePicker.style.display = "block";
      timeSlider.style.display = "block";
      break;
    case "custom-location":
      locationPicker.style.display = "block";
      break;
  }
});

const [setGraticuleColor] = bindColorPicker("graticule-color", (color) => {
  allGlobes.forEach((g) => (g.graticule.color = color));
  instance.notifyChange(allGlobes);
});

function setLayers(...name) {
  for (const layer of earth.getLayers()) {
    layer.visible = name.includes(layer.name);
  }
}

const [setAmbientIntensity] = bindSlider("ambientIntensity", (intensity) => {
  ambientLight.intensity = intensity;
  instance.notifyChange();
});

const [setSunIntensity] = bindSlider("sunIntensity", (intensity) => {
  sunlight.intensity = intensity;
  instance.notifyChange();
});

const [setGlobe] = bindDropDown("globe-selector", (globe) => {
  allGlobes.forEach((g) => (g.visible = false));

  let entity;

  switch (globe) {
    case "moon":
      moon.visible = true;
      entity = moon;
      break;
    case "sun":
      sun.visible = true;
      entity = sun;
      break;
    case "earth":
      earth.visible = true;
      entity = earth;
      break;
    case "mars":
      mars.visible = true;
      entity = mars;
      break;
  }

  controls?.dispose();

  instance.view.goTo(entity);

  document.getElementById("earth-params").style.display = earth.visible
    ? "block"
    : "none";
  document.getElementById("lightingGroup").style.display = sun.visible
    ? "none"
    : "block";
  document.getElementById("sunParams").style.display = sun.visible
    ? "none"
    : "block";

  earthAtmosphere.visible = earth.visible;
  marsAtmosphere.visible = mars.visible;
  sunGlow.visible = sun.visible;

  instance.notifyChange(entity);

  controls = new GlobeControls({
    scene: entity.object3d,
    ellipsoid: entity.ellipsoid,
    camera: instance.view.camera,
    domElement: instance.domElement,
  });
});

const reset = () => {
  setGlobe("earth"); // TODO
  setLayers("satellite", "clouds");
  setAtmosphere(true);
  setGraticule(false);
  setGraticuleColor(0x000000);
  setSunLatitude(9);
  setSunLongitude(-41);
  setAmbientIntensity(0.4);
  setSunIntensity(4);
  setLighting(true);
  setSunPositionMode("custom-location");

  instance.view.camera.position.copy(defaultCameraPosition);
  instance.view.camera.lookAt(new Vector3(0, 0, 0));

  populateLayerList();
};

bindButton("reset", reset);

function populateLayerList() {
  const list = document.getElementById("layer-list");
  list.innerHTML = "";

  const entries = [
    `<li class="list-group-item list-group-item-secondary">Layers</li>`,
  ];

  const createEntry = (name, visible) => {
    const entry = `
            <li class="list-group-item">
                <input id="layer-${name}" class="form-check-input me-1" ${visible ? "checked" : ""} type="checkbox" />
                <label class="form-check-label" for="layer-${name}">${name}</label>
            </li>
        `;

    entries.push(entry);
  };

  for (const layer of earth.getColorLayers().reverse()) {
    createEntry(layer.name, layer.visible);
  }

  for (const layer of earth.getElevationLayers()) {
    createEntry(layer.name, layer.visible);
  }

  list.innerHTML = entries.join("\n");

  for (const layer of earth.getLayers()) {
    bindToggle(`layer-${layer.name}`, (visible) => {
      layer.visible = visible;
      instance.notifyChange(earth);
    });
  }
}

reset();

const inspector = Inspector.attach("inspector", instance);

inspector.addPanel(
  new GlobeControlsInspector(inspector.gui, instance, controls),
);
index.html
<!doctype html>
<html lang="en">
  <head>
    <title>Globe</title>
    <meta charset="UTF-8" />
    <meta name="name" content="globe" />
    <meta name="description" content="Display a globe-shaped Map." />
    <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"
    />
  </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">
        <div class="card-header">
          Parameters
          <button
            type="button"
            id="reset"
            class="btn btn-sm btn-primary rounded float-end"
          >
            reset
          </button>
        </div>

        <div class="card-body" id="top-options">
          <!-- Earth/Moon selector -->
          <div class="input-group mb-3">
            <label class="input-group-text" for="globe-selector">Globe</label>
            <select class="form-select" id="globe-selector" autocomplete="off">
              <option value="earth" selected>Earth</option>
              <option value="moon">Moon</option>
              <option value="mars">Mars</option>
              <option value="sun">Sun</option>
            </select>
          </div>

          <div id="earth-params">
            <ul class="list-group mb-3" id="layer-list">
              <!-- Content of this list is generated by the example code -->
              <li class="list-group-item">
                <input class="form-check-input me-1" type="checkbox" />
                <label class="form-check-label" for="firstCheckbox"
                  >Layer 1</label
                >
              </li>
            </ul>
          </div>

          <!-- Atmosphere -->
          <div class="form-check form-switch mb-1">
            <input
              class="form-check-input"
              type="checkbox"
              role="switch"
              id="atmosphere"
              autocomplete="off"
            />
            <label class="form-check-label" for="atmosphere">Atmosphere</label>
          </div>

          <!-- Toggle graticule -->
          <div class="form-check form-switch mb-1">
            <input
              class="form-check-input"
              type="checkbox"
              role="switch"
              id="graticule"
              autocomplete="off"
            />
            <label class="form-check-label w-100" for="graticule">
              <label class="form-check-label w-100" for="graticule">
                <div class="row">
                  <div class="col">Graticule</div>
                  <div class="col-auto">
                    <input
                      type="color"
                      style="height: 1.5rem"
                      class="form-control form-control-color float-end"
                      id="graticule-color"
                      value="#000000"
                      title="Graticule color"
                    />
                  </div>
                </div> </label
            ></label>
          </div>

          <div id="lightingGroup">
            <!-- Toggle lighting -->
            <div class="form-check form-switch mb-1">
              <input
                class="form-check-input"
                type="checkbox"
                role="switch"
                id="lighting"
                autocomplete="off"
              />
              <label class="form-check-label" for="lighting">Lighting</label>
            </div>

            <div id="lightingParams">
              <!-- Sun intensity -->
              <div class="row">
                <div class="col">
                  <label
                    id="sunIntensityLabel"
                    for="sunIntensity"
                    class="form-label"
                    >Sun intensity</label
                  >
                </div>
                <div class="col">
                  <input
                    type="range"
                    min="0"
                    max="10"
                    step="0.1"
                    value="4"
                    class="form-range"
                    id="sunIntensity"
                    autocomplete="off"
                  />
                </div>
              </div>

              <!-- Ambient intensity -->
              <div class="row">
                <div class="col">
                  <label
                    id="ambientIntensityLabel"
                    for="ambientIntensity"
                    class="form-label"
                    >Ambient light intensity</label
                  >
                </div>
                <div class="col">
                  <input
                    type="range"
                    min="0"
                    max="3"
                    step="0.1"
                    value="0.3"
                    class="form-range"
                    id="ambientIntensity"
                    autocomplete="off"
                  />
                </div>
              </div>
            </div>
          </div>

          <div id="sunParams">
            <hr />
            <!-- Sun position mode -->
            <div class="input-group">
              <label class="input-group-text" for="sun-position-mode"
                >Sun position</label
              >
              <select
                class="form-select"
                id="sun-position-mode"
                autocomplete="off"
              >
                <option value="custom-location" selected>By location</option>
                <option value="custom-date">By date</option>
              </select>
            </div>

            <!-- Date -->
            <div id="date-picker">
              <div class="input-group mt-3">
                <label class="input-group-text" for="date">Date</label>
                <input
                  class="form-control"
                  type="date"
                  id="date"
                  autocomplete="off"
                />
                <div class="input-group-text">
                  <button class="btn btn-sm btn-primary" id="now">Now</button>
                </div>
              </div>
            </div>

            <div id="sun-location" class="mt-3">
              <!-- Sun latitude slider -->
              <div class="row 1">
                <div class="col">
                  <label
                    id="sunLatitudeLabel"
                    for="sunLatitude"
                    class="form-label"
                    >Lat: 35° N</label
                  >
                </div>
                <div class="col">
                  <input
                    type="range"
                    min="-90"
                    max="90"
                    step="1"
                    value="35"
                    class="form-range"
                    id="sunLatitude"
                    autocomplete="off"
                  />
                </div>
              </div>

              <!-- Sun longitude -->
              <div class="row">
                <div class="col">
                  <label
                    id="sunLongitudeLabel"
                    for="sunLongitude"
                    class="form-label"
                    >Lat: 35° N</label
                  >
                </div>
                <div class="col">
                  <input
                    type="range"
                    min="-180"
                    max="180"
                    step="1"
                    value="9"
                    class="form-range"
                    id="sunLongitude"
                    autocomplete="off"
                  />
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>

    <div
      class="bg-body border"
      id="timeContainer"
      style="
        display: none;
        position: absolute;
        left: 0;
        bottom: 1.3rem;
        width: 100%;
        height: 7rem;
        padding: 1rem;
      "
    >
      <!-- Background opacity slider -->
      <label for="time" class="form-label"
        ><span id="timeLabel" class="badge rounded-pill text-bg-primary"
          >12:00 UTC</span
        >
      </label>
      <div class="input-group">
        <input
          type="range"
          min="0"
          max="86400"
          step="60"
          value="43200"
          class="form-range"
          id="time"
          autocomplete="off"
        />
      </div>

      <div class="row">
        <div class="col text-start">
          <span id="timeLabel" class="badge rounded-pill text-bg-secondary"
            >00:00</span
          >
        </div>
        <div class="col text-center">
          <span id="timeLabel" class="badge rounded-pill text-bg-secondary"
            >12:00</span
          >
        </div>
        <div class="col text-end">
          <span id="timeLabel" class="badge rounded-pill text-bg-secondary"
            >24:00</span
          >
        </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": "globe",
    "dependencies": {
        "colormap": "^2.3.2",
        "@giro3d/giro3d": "0.43.2"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}