Create a realistic atmosphere and sky dome.

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
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
Luminance
Turbidity
Rayleigh coefficient
Mie directional g
Mie coefficient
Solar disc diameter
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
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
Parameters

Wavelengths ([0, 1])
100%

Use the Atmosphere entity to display a realistic atmosphere for a specific ellipsoid. The external side of the sphere displays the atmospheric halo, and the internal side displays a sky dome with realistic atmospheric scattering.

index.js
import {
  Clock,
  Color,
  DirectionalLight,
  Mesh,
  MeshBasicMaterial,
  MeshStandardMaterial,
  SphereGeometry,
  Vector3,
} from "three";

import { CSS2DObject } from "three/examples/jsm/renderers/CSS2DRenderer.js";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

import Coordinates from "@giro3d/giro3d/core/geographic/Coordinates";
import Ellipsoid from "@giro3d/giro3d/core/geographic/Ellipsoid";
import Instance from "@giro3d/giro3d/core/Instance.js";
import Atmosphere from "@giro3d/giro3d/entities/Atmosphere";
import Inspector from "@giro3d/giro3d/gui/Inspector";
import DrawTool from "@giro3d/giro3d/interactions/DrawTool";
import SkyDome from "@giro3d/giro3d/entities/SkyDome";
import EllipsoidHelper from "@giro3d/giro3d/helpers/EllipsoidHelper";

import { bindButton } from "./widgets/bindButton";
import { bindColorPicker } from "./widgets/bindColorPicker";
import { bindSlider } from "./widgets/bindSlider";
import { bindToggle } from "./widgets/bindToggle";

const Z_UP = new Vector3(0, 0, 1);

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

const ellipsoid = Ellipsoid.WGS84;

const DEFAULT_PARAMS = {
  automaticSunRotation: true,
  fov: 30,
  redWavelength: 0.65,
  greenWavelength: 0.57,
  blueWavelength: 0.475,
  thickness: 300_000,
  globeColor: "#1e4485",
  showSunObject: true,
  sunPosition: new Vector3(0.2, 1, 0),
  inner: true,
  lookAtSun: false,
  outer: true,
  showSunMarker: false,
  showEllipsoidHelper: false,
  observer: new Coordinates("EPSG:4326", 40, 25, 36_000_000),
  target: new Coordinates("EPSG:4326", 0, 0, 0),
};

let params = { ...DEFAULT_PARAMS };

// For this example, we create a simple sphere Mesh. For a full-featured globe with map/GIS support, create a Globe object (see the "globe" example)
const globe = new Mesh(
  new SphereGeometry(ellipsoid.semiMajorAxis, 256, 128),
  new MeshStandardMaterial({
    color: params.globeColor,
    emissive: params.globeColor,
    emissiveIntensity: 0.1,
  }),
);

globe.name = "Globe";

// Scale it to match the ellipsoid compression factor.
globe.scale.set(1, 1, ellipsoid.compressionFactor);
globe.updateMatrixWorld(true);

instance.threeObjects.add(globe);

let atmosphere;

// The skydome will be visible when the camera is near the ground.
const skyDome = new SkyDome();
instance.add(skyDome);

// Create simple orbit controls to move around globe.
// Those controls will be disabled when we are on the ground.
const controls = new OrbitControls(instance.view.camera, instance.domElement);
controls.target.set(0, 0, 0);
instance.view.setControls(controls);

function createAtmosphere() {
  if (atmosphere) {
    instance.remove(atmosphere);
  }

  atmosphere = new Atmosphere({ ellipsoid, thickness: params.thickness });

  instance.add(atmosphere);

  if (params.redWavelength) {
    atmosphere.redWavelength = params.redWavelength;
    atmosphere.greenWavelength = params.greenWavelength;
    atmosphere.blueWavelength = params.blueWavelength;
  }

  atmosphere.inner.visible = params.inner;
  atmosphere.outer.visible = params.outer;

  atmosphere.setSunPosition(params.sunPosition);
  skyDome.setSunPosition(params.sunPosition);

  instance.notifyChange(atmosphere);
}

createAtmosphere();

const camera = instance.view.camera;

function updateCamera() {
  const { observer, target } = params;

  const position = ellipsoid.toCartesian(
    observer.latitude,
    observer.longitude,
    observer.altitude,
  );
  camera.position.set(position.x, position.y, position.z);

  if (observer.altitude < 1_000_000) {
    const up = ellipsoid.getNormal(observer.latitude, observer.longitude);
    camera.up = up;
  } else {
    camera.up = Z_UP;
  }

  const lookAt = ellipsoid.toCartesian(
    target.latitude,
    target.longitude,
    target.altitude,
  );
  camera.lookAt(lookAt);

  camera.fov = params.fov;

  camera.updateMatrixWorld(true);
}

updateCamera();

const sun = new Mesh(
  new SphereGeometry(ellipsoid.semiMajorAxis * 0.02),
  new MeshBasicMaterial({ color: "yellow" }),
);

const elt = document.createElement("span");
elt.style.width = "15px";
elt.style.height = "15px";
elt.style.backgroundColor = "cyan";
elt.style.display = "inline-block";
elt.style.borderRadius = "50%";
elt.style.borderWidth = "2px";
elt.style.borderStyle = "solid";
elt.style.borderColor = "black";
const sunCSSMarker = new CSS2DObject(elt);
sun.add(sunCSSMarker);

sun.name = "Sun";

instance.add(sun);

// Let's create an ellipsoid helper to help us visualize the ellipsoid and its axes.
const helper = new EllipsoidHelper({
  ellipsoid: ellipsoid.scale(1.01),
  segments: 64,
});
instance.threeObjects.add(helper);
helper.visible = params.showEllipsoidHelper;

const sunlight = new DirectionalLight();
instance.add(sunlight);
instance.add(sunlight.target);

const apparentSunCourseRadius = ellipsoid.semiMajorAxis * 2;
const actualSunCourseRAdius = ellipsoid.semiMajorAxis * 200;

const clock = new Clock();
let time = 0;
const actualSunPosition = new Vector3(0, 0, 0);

const updateSunPosition = () => {
  requestAnimationFrame(updateSunPosition);

  if (!params.automaticSunRotation) {
    clock.stop();
    return;
  }

  if (!clock.running) {
    clock.start();
  }

  const speed = -1;

  time += clock.getDelta();
  const t = speed * time;

  const cosT = Math.cos(t);
  const sinT = Math.sin(t);

  const x = cosT * apparentSunCourseRadius;
  const y = sinT * apparentSunCourseRadius;

  actualSunPosition.setX(cosT * actualSunCourseRAdius);
  actualSunPosition.setY(sinT * actualSunCourseRAdius);

  sun.position.set(x, y, 0);

  sun.material.visible = params.showSunObject;

  sun.updateMatrixWorld(true);

  sunlight.position.copy(sun.position);
  sunlight.lookAt(globe.position);

  sunlight.updateMatrixWorld(true);

  if (atmosphere) {
    atmosphere.setSunPosition(sun.position);
  }

  skyDome.setSunPosition(sun.position);

  if (params.lookAtSun) {
    camera.lookAt(actualSunPosition);
  }

  instance.notifyChange();
};

updateSunPosition();

const [setRed] = bindSlider("red", (v) => {
  params.redWavelength = v;
  atmosphere.redWavelength = v;
  instance.notifyChange(atmosphere);
});
const [setGreen] = bindSlider("green", (v) => {
  params.greenWavelength = v;
  atmosphere.greenWavelength = v;
  instance.notifyChange(atmosphere);
});
const [setBlue] = bindSlider("blue", (v) => {
  params.blueWavelength = v;
  atmosphere.blueWavelength = v;
  instance.notifyChange(atmosphere);
});
const [setGlobeColor] = bindColorPicker("globe-color", (c) => {
  const color = new Color(c);
  params.globeColor = "#" + color.getHexString();
  globe.material.color = color;
  globe.material.emissive = color;

  instance.notifyChange();
});
const [setThickness] = bindSlider("thickness", (thickness) => {
  params.thickness = thickness;
  createAtmosphere();
});
const [showInner] = bindToggle("inner", (show) => {
  params.inner = show;
  atmosphere.inner.visible = show;
  instance.notifyChange();
});
const [showHelper] = bindToggle("show-ellipsoid", (show) => {
  params.showEllipsoidHelper = show;
  helper.showLabels = show;
  helper.visible = show;
  instance.notifyChange();
});
const [setLookAtSun] = bindToggle("look-at-sun", (enable) => {
  params.lookAtSun = enable;
  instance.notifyChange();
});
const [setAutomaticSunRotation] = bindToggle(
  "automatic-sun-rotation",
  (enabled) => {
    params.automaticSunRotation = enabled;
  },
);
const [showOuter] = bindToggle("outer", (show) => {
  params.outer = show;
  atmosphere.outer.visible = show;
  instance.notifyChange();
});
const [showMarker] = bindToggle("sun-marker", (show) => {
  params.showSunMarker = show;
  sunCSSMarker.visible = show;
  instance.notifyChange();
});

function goToGround(latitude, longitude) {
  params.fov = 120;

  helper.showLabels = false;

  controls.enabled = false;
  instance.view.setControls(null);

  const altitude = 100;

  params.observer = new Coordinates("EPSG:4326", longitude, latitude, altitude);

  params.showSunObject = false;
  params.lookAtSun = false;

  params.target = new Coordinates(
    params.observer.crs,
    longitude + 0.01, // Look toward the east (the sunrise)
    latitude,
    altitude + 200, // And slightly above the horizon
  );

  setLookAtSun(params.lookAtSun);
  showInner(false);
  showOuter(false);

  updateCamera();
}

bindButton("set-ground-position", () => {
  const drawTool = new DrawTool({ instance });

  function vertexLabelFormatter({ position }) {
    const geo = ellipsoid.toGeodetic(position.x, position.y, position.z);

    return `lat: ${geo.latitude.toFixed(3)}°, lon: ${geo.longitude.toFixed(3)}°`;
  }

  drawTool
    .createPoint({ showVertexLabels: true, vertexLabelFormatter })
    .then((shape) => {
      instance.remove(shape);

      const point = shape.points[0];

      const { latitude, longitude } = ellipsoid.toGeodetic(
        point.x,
        point.y,
        point.z,
      );

      goToGround(latitude, longitude);
    });
});

function reset() {
  params = {
    ...DEFAULT_PARAMS,
    observer: DEFAULT_PARAMS.observer.clone(),
    target: DEFAULT_PARAMS.target.clone(),
  };

  showHelper(params.showEllipsoidHelper);
  setRed(params.redWavelength);
  setGreen(params.greenWavelength);
  setBlue(params.blueWavelength);
  setGlobeColor(params.globeColor);
  setThickness(params.thickness, 3_000, 300_000, 1);
  showOuter(params.outer);
  showInner(params.inner);
  setLookAtSun(params.lookAtSun);
  showMarker(params.showSunMarker);
  setAutomaticSunRotation(params.automaticSunRotation);
  updateCamera();

  instance.view.setControls(controls);
  controls.enabled = true;
  controls.target.set(0, 0, 0);
}

bindButton("reset", reset);

reset();

Inspector.attach("inspector", instance);
index.html
<!doctype html>
<html lang="en">
  <head>
    <title>Atmosphere</title>
    <meta charset="UTF-8" />
    <meta name="name" content="atmosphere" />
    <meta
      name="description"
      content="Create a realistic atmosphere and sky dome."
    />
    <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: 15rem">
      <!--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">
          <button
            type="button"
            class="btn btn-primary w-100"
            id="set-ground-position"
            title="Sets the camera position on the ground"
          >
            <i class="bi bi-crosshair"></i>
            Set ground position
          </button>

          <hr />

          <!-- Toggle outer atmosphere -->
          <div class="form-check form-switch">
            <input
              class="form-check-input"
              checked
              type="checkbox"
              role="switch"
              id="outer"
              autocomplete="off"
            />
            <label class="form-check-label" for="outer">Outer atmosphere</label>
          </div>

          <!-- Toggle inner atmosphere -->
          <div class="form-check form-switch">
            <input
              class="form-check-input"
              checked
              type="checkbox"
              role="switch"
              id="inner"
              autocomplete="off"
            />
            <label class="form-check-label" for="inner">Inner atmosphere</label>
          </div>

          <!-- Toggle sun position marker  -->
          <div
            class="form-check form-switch"
            title="The sun position marker must match the apparent sun position in the sky dome. Any discrepancy indicates an error in the sky dome shader."
          >
            <input
              class="form-check-input"
              type="checkbox"
              role="switch"
              id="sun-marker"
              autocomplete="off"
            />
            <label class="form-check-label" for="sun-marker"
              >Sun position marker</label
            >
          </div>

          <!-- Toggle automatic sun rotation -->
          <div class="form-check form-switch">
            <input
              class="form-check-input"
              checked
              type="checkbox"
              role="switch"
              id="automatic-sun-rotation"
              autocomplete="off"
            />
            <label class="form-check-label" for="automatic-sun-rotation"
              >Automatic sun rotation</label
            >
          </div>

          <!-- Toggle ellipsoid helper -->
          <div class="form-check form-switch">
            <input
              class="form-check-input"
              checked
              type="checkbox"
              role="switch"
              id="show-ellipsoid"
              autocomplete="off"
            />
            <label class="form-check-label" for="show-ellipsoid"
              >Show ellipsoid</label
            >
          </div>

          <!-- Toggle look at sun -->
          <div class="form-check form-switch">
            <input
              class="form-check-input"
              checked
              type="checkbox"
              role="switch"
              id="look-at-sun"
              autocomplete="off"
            />
            <label class="form-check-label" for="look-at-sun"
              >Look at sun</label
            >
          </div>

          <!-- Globe color -->
          <label class="form-check-label w-100 mt-3" for="globe-color">
            <div class="row">
              <div class="col">Globe color</div>
              <div class="col">
                <input
                  type="color"
                  class="form-control form-control-color float-end h-100 w-100"
                  id="globe-color"
                  value="#2978b4"
                  title="Globe color"
                  autocomplete="off"
                />
              </div>
            </div>
          </label>

          <!-- Thickness -->
          <div class="row mt-2">
            <div class="col-5">
              <label for="thickness" class="form-label">Thickness</label>
            </div>
            <div class="col">
              <input
                type="range"
                min="0"
                max="1"
                step="0.0001"
                value="0"
                class="form-range"
                id="thickness"
                autocomplete="off"
              />
            </div>
          </div>

          <h6 class="mt-3">Wavelengths ([0, 1])</h6>

          <!-- Red -->
          <div class="row">
            <div class="col-3">
              <label for="red" class="form-label">Red</label>
            </div>
            <div class="col">
              <input
                type="range"
                min="0"
                max="1"
                step="0.0001"
                value="0"
                class="form-range"
                id="red"
                autocomplete="off"
              />
            </div>
          </div>

          <!-- Green -->
          <div class="row">
            <div class="col-3">
              <label for="green" class="form-label">Green</label>
            </div>
            <div class="col">
              <input
                type="range"
                min="0"
                max="1"
                step="0.0001"
                value="0"
                class="form-range"
                id="green"
                autocomplete="off"
              />
            </div>
          </div>

          <!-- Blue -->
          <div class="row">
            <div class="col-3">
              <label for="blue" class="form-label">Blue</label>
            </div>
            <div class="col">
              <input
                type="range"
                min="0"
                max="1"
                step="0.0001"
                value="0"
                class="form-range"
                id="blue"
                autocomplete="off"
              />
            </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": "atmosphere",
    "dependencies": {
        "@giro3d/giro3d": "0.43.2"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}