Parameters
Create a realistic atmosphere and sky dome.
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.
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);
<!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>
{
"name": "atmosphere",
"dependencies": {
"@giro3d/giro3d": "0.43.1"
},
"devDependencies": {
"vite": "^3.2.3"
},
"scripts": {
"start": "vite",
"build": "vite build"
}
}