<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<title>Global Illumination | Sample | ArcGIS Maps SDK for JavaScript</title>
<!-- Load the ArcGIS Maps SDK for JavaScript from CDN -->
<script type="module" src="https://js.arcgis.com/5.2/"></script>
:root.calcite-mode-dark {
max-width: calc(100vw - 32px);
background: var(--calcite-color-foreground-1);
color: var(--calcite-color-text-1);
box-shadow: 0 1px 4px #0003;
grid-template-columns: repeat(2, minmax(0, 1fr));
border: 1px solid var(--calcite-color-border-2);
box-shadow: 0 0 0 2px var(--calcite-color-brand);
transform: translateX(0);
background-color 180ms ease;
#time-toggle[aria-checked="true"]::before {
transform: translateX(100%);
#time-toggle:focus-visible {
outline: 2px solid var(--calcite-color-brand);
transition: color 180ms ease;
#time-toggle[aria-checked="false"] .time-day {
#time-toggle[aria-checked="true"] .time-night {
@media (prefers-reduced-motion: reduce) {
grid-template-columns: repeat(2, minmax(0, 1fr));
#lighting-controls calcite-label {
--calcite-label-margin-bottom: 12px;
border-top: 1px solid var(--calcite-color-border-2);
#gi-control calcite-label {
--calcite-label-margin-bottom: 0;
<arcgis-scene item-id="31f338935f724900b0dd00eeeccaf144">
<arcgis-zoom slot="top-left"></arcgis-zoom>
<arcgis-navigation-toggle slot="top-left"></arcgis-navigation-toggle>
<section id="lighting-controls" slot="bottom-right" aria-label="Global illumination">
<button id="time-toggle" type="button" role="switch" aria-checked="false" aria-label="Night mode">
<span class="time-option time-day" aria-hidden="true"
><calcite-icon icon="brightness" scale="s"></calcite-icon>Day</span
<span class="time-option time-night" aria-hidden="true"
><calcite-icon icon="moon" scale="s"></calcite-icon>Night</span
<div id="view-buttons" role="group" aria-label="View presets">
<calcite-button data-view="overview" data-night="false" width="full" aria-pressed="true"
><span>Overview<br />day</span></calcite-button
<calcite-button data-view="overview" data-night="true" width="full" appearance="outline" aria-pressed="false"
><span>Overview<br />night</span></calcite-button
<calcite-button data-view="closeup" data-night="false" width="full" appearance="outline" aria-pressed="false"
><span>Close-up<br />day</span></calcite-button
<calcite-button data-view="closeup" data-night="true" width="full" appearance="outline" aria-pressed="false"
><span>Close-up<br />night</span></calcite-button
<calcite-label layout="inline-space-between">
<calcite-switch id="emissive-toggle"></calcite-switch>
<calcite-label layout="inline-space-between">
<calcite-switch id="glow-toggle" checked></calcite-switch>
<calcite-label layout="inline-space-between">
<calcite-switch id="gi-toggle" checked></calcite-switch>
<arcgis-expand mode="floating" slot="top-right">
<arcgis-daylight></arcgis-daylight>
<arcgis-expand mode="floating" slot="top-right">
<arcgis-weather></arcgis-weather>
const [Glow, Basemap, ObjectSymbol3DLayer] = await $arcgis.import([
"@arcgis/core/webscene/Glow.js",
"@arcgis/core/Basemap.js",
"@arcgis/core/symbols/ObjectSymbol3DLayer.js",
// UI controls hold the current selection and drive the scene handlers below.
const viewElement = document.querySelector("arcgis-scene");
const timeToggle = document.querySelector("#time-toggle");
const viewButtons = document.querySelectorAll("#view-buttons calcite-button");
const giSwitch = document.querySelector("#gi-toggle");
const glowSwitch = document.querySelector("#glow-toggle");
const emissiveSwitch = document.querySelector("#emissive-toggle");
// Wait for the scene before accessing its saved slides and layers.
await viewElement.viewOnReady();
const view = viewElement.view;
// Swap only the 2D base layers, preserving the 3D buildings and trees in referenceLayers.
const topoBaseLayers = scene.basemap.baseLayers.toArray();
const darkGrayBasemap = Basemap.fromId("dark-gray-vector");
await darkGrayBasemap.load();
const darkBaseLayers = darkGrayBasemap.baseLayers.toArray();
// Reuse two saved viewpoints and lighting dates for the four day/night presets.
const slides = scene.presentation.slides;
const findSlide = (title) => slides.find((slide) => slide.title.text.trim().toUpperCase() === title);
const overviewNightSlide = findSlide("NIGHT");
const zoomDaySlide = findSlide("ZOOM");
// Target the transit layers whose renderer materials are adjusted by this sample.
const findLayer = (title) => view.map.layers.find((layer) => layer.title === title);
function changeTime(night) {
const lighting = (night ? overviewNightSlide : zoomDaySlide).environment.lighting;
view.environment.lighting.date = new Date(lighting.date.getTime());
updateTimeControls(night);
// Share basemap, material, and control updates with the view presets.
function updateTimeControls(night, viewpoint) {
timeToggle.setAttribute("aria-checked", String(night));
scene.basemap.baseLayers.removeAll();
scene.basemap.baseLayers.addMany(night ? darkBaseLayers : topoBaseLayers);
emissiveSwitch.checked = night;
toggleEmissiveMaterials();
// Keep the preset selection aligned with the current viewpoint and the requested time.
viewpoint ??= [...viewButtons].find((button) => button.getAttribute("aria-pressed") === "true")?.dataset.view;
for (const button of viewButtons) {
const selected = button.dataset.view === viewpoint && button.dataset.night === String(night);
button.setAttribute("aria-pressed", String(selected));
button.appearance = selected ? "solid" : "outline";
// Customize a clone so the original slides remain unchanged.
async function changeView(viewpoint, night, animate = true) {
const slide = (viewpoint === "overview" ? overviewNightSlide : zoomDaySlide).clone();
const lighting = slide.environment.lighting;
const timeLighting = (night ? overviewNightSlide : zoomDaySlide).environment.lighting;
lighting.date = new Date(timeLighting.date.getTime());
lighting.glow = glowSwitch.checked ? new Glow({ intensity: 0.5 }) : null;
// Keep the slide from replacing our independently selected base layers.
const activeView = [...viewButtons].find((button) => button.getAttribute("aria-pressed") === "true")?.dataset
updateTimeControls(night, viewpoint);
// The SDK interpolates sun position along camera movement, so nudge same-view transitions.
// applyTo restores the exact saved camera at the end, preventing accumulated drift.
if (animate && activeView === viewpoint) {
const camera = view.camera.clone();
camera.position.z += viewpoint === "overview" ? 2 : 0.5;
await slide.applyTo(view, { animate, speedFactor: 0.3, duration: activeView === viewpoint ? 2000 : undefined });
toggleGlobalIllumination();
function toggleGlobalIllumination() {
view.environment.lighting.globalIllumination = giSwitch.checked;
// Glow adds a halo around bright emitters and can be toggled independently of GI.
view.environment.lighting.glow = glowSwitch.checked ? { intensity: 0.5 } : null;
// Switch transit symbols between ordinary shading and emissive materials.
function toggleEmissiveMaterials() {
// This sample uses the first 3D symbol layer; some renderer classes have no symbol.
function updateSymbol(symbol) {
const symbolLayer = symbol?.symbolLayers?.at(0);
symbolLayer.material ?? (symbolLayer.material = {});
// Models use their emissive textures; primitive symbols emit their material color.
symbolLayer.material.emissive = emissiveSwitch.checked
source: symbolLayer instanceof ObjectSymbol3DLayer && symbolLayer.resource?.href ? "emissive" : "color",
// Clone and reassign each renderer so the scene redraws with the new materials.
for (const layer of [findLayer("NYC Subway Lines"), findLayer("NYCT Bus Routes Points")]) {
const renderer = layer.renderer?.clone();
if (renderer.type === "simple") {
updateSymbol(renderer.symbol);
// Each unique-value class has its own symbol and needs the same update.
for (const group of renderer.uniqueValueGroups) {
for (const symbolClass of group.classes) {
updateSymbol(symbolClass.symbol);
layer.renderer = renderer;
// Change time independently of the camera; only presets apply saved viewpoints.
timeToggle.addEventListener("click", () => {
changeTime(timeToggle.getAttribute("aria-checked") !== "true");
for (const button of viewButtons) {
button.addEventListener("click", () => void changeView(button.dataset.view, button.dataset.night === "true"));
// GI, glow, and emission can also be compared independently of the view presets.
giSwitch.addEventListener("calciteSwitchChange", toggleGlobalIllumination);
glowSwitch.addEventListener("calciteSwitchChange", toggleGlow);
emissiveSwitch.addEventListener("calciteSwitchChange", toggleEmissiveMaterials);
// Initialize the daytime overview without an entrance animation.
toggleGlobalIllumination();
await changeView("overview", false, false);