Create a realistic atmosphere and sky dome.

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.1"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}