Use the SunExposure tool to compute solar exposition for a time interval.

Parameters
100%
index.js
/*
 * Copyright (c) 2015-2018, IGN France.
 * Copyright (c) 2018-2026, Giro3D team.
 * SPDX-License-Identifier: MIT
 */

import XYZ from "ol/source/XYZ";
import * as THREE from "three";
import {
  AmbientLight,
  BoxGeometry,
  DirectionalLight,
  Mesh,
  MeshStandardMaterial,
  PlaneGeometry,
  SphereGeometry,
  Vector3,
} from "three";
import { GLTFLoader } from "three/examples/jsm/Addons.js";
import { MapControls } from "three/examples/jsm/controls/MapControls.js";

import ColorMap from "@giro3d/giro3d/core/ColorMap";
import Coordinates from "@giro3d/giro3d/core/geographic/Coordinates";
import CoordinateSystem from "@giro3d/giro3d/core/geographic/CoordinateSystem";
import Extent from "@giro3d/giro3d/core/geographic/Extent";
import Instance from "@giro3d/giro3d/core/Instance";
import ColorLayer from "@giro3d/giro3d/core/layer/ColorLayer";
import ElevationLayer from "@giro3d/giro3d/core/layer/ElevationLayer";
import Entity3D from "@giro3d/giro3d/entities/Entity3D";
import Map from "@giro3d/giro3d/entities/Map";
import { MapLightingMode } from "@giro3d/giro3d/entities/MapLightingOptions";
import MapboxTerrainFormat from "@giro3d/giro3d/formats/MapboxTerrainFormat";
import Inspector from "@giro3d/giro3d/gui/Inspector";
import DrawTool from "@giro3d/giro3d/interactions/DrawTool";
import SunExposure from "@giro3d/giro3d/interactions/SunExposure";
import TiledImageSource from "@giro3d/giro3d/sources/TiledImageSource";
import Fetcher from "@giro3d/giro3d/utils/Fetcher";

import { bindButton } from "./widgets/bindButton";
import { bindDropDown } from "./widgets/bindDropDown";
import { bindNumberInput } from "./widgets/bindNumberInput";
import { bindTextInput } from "./widgets/bindTextInput";
import { bindToggle } from "./widgets/bindToggle";
import { makeColorRamp } from "./widgets/makeColorRamp";
import updateColorMapPreview from "./widgets/updateColorMapPreview";

const lambert93 = CoordinateSystem.register(
  "EPSG:2154",
  "+proj=lcc +lat_0=46.5 +lon_0=3 +lat_1=49 +lat_2=44 +x_0=700000 +y_0=6600000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs",
);

const utm32 = CoordinateSystem.register(
  "EPSG:25832",
  "+proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs",
);

const params = {
  abortController: new AbortController(),
  aoiShape: null,
  helpers: false,
  showInputs: true,
  scenario: "simple",
  activeAttribute: "irradiation",
  spatialResolution: 10,
  date: new Date(Date.UTC(2025, 6, 21)),
  startTime: 8,
  endTime: 16,
  originalVolume: new THREE.Box3().makeEmpty(),
  volume: null,
  boxHelper: null,
  temporalResolutionMinutes: 60,
  currentObjects: [],
  inputs: [],
};

function createLights(instance, center) {
  // Note: those lights are for illustrative purposes,
  // they are not part of the sun exposure computation.

  const main = new DirectionalLight();
  const secondary = new DirectionalLight("white", 0.3);
  const ambientLight = new AmbientLight("white", 0.2);

  instance.add(ambientLight);

  instance.add(main);
  instance.add(main.target);
  instance.add(secondary);
  instance.add(secondary.target);

  main.target.position.copy(center);
  main.position.set(center.x + 1000, center.y + 300, 1000);
  main.updateMatrixWorld(true);
  main.target.updateMatrixWorld(true);

  params.currentObjects.push(main);
  params.currentObjects.push(main.target);

  secondary.target.position.copy(center);
  secondary.position.set(center.x - 1000, center.y + 300, 1000);
  secondary.updateMatrixWorld(true);
  secondary.target.updateMatrixWorld(true);

  params.currentObjects.push(secondary);
  params.currentObjects.push(secondary.target);

  params.currentObjects.push(ambientLight);
}

async function planeOnlyScenario(instance) {
  const center = Coordinates.WGS84(23.43, 0).as(lambert93);
  const width = 200;
  const areaOfInterest = Extent.fromCenterAndSize(
    lambert93,
    center,
    width,
    width,
  );
  const plane = new Mesh(
    new PlaneGeometry(width, width),
    new MeshStandardMaterial({ color: "white" }),
  );

  const centerVec3 = center.toVector3();
  plane.position.copy(centerVec3);
  plane.updateMatrixWorld();

  await instance.add(plane);

  const inputs = [plane];

  return {
    areaOfInterest,
    inputs,
    allowedSpatialResolutionRange: [0.5, 20, 1],
    center: centerVec3,
    volume: areaOfInterest.toBox3(-1, 1),
  };
}

async function planeBoxScenario(instance) {
  const center = Coordinates.WGS84(23.4384024785, 0).as(lambert93);
  const width = 300;
  const areaOfInterest = Extent.fromCenterAndSize(
    lambert93,
    center,
    width,
    width,
  );
  const plane = new Mesh(
    new PlaneGeometry(width, width),
    new MeshStandardMaterial({ color: "white" }),
  );
  const box = new Mesh(
    new BoxGeometry(30, 30, 60),
    new MeshStandardMaterial({ color: "#89dce5" }),
  );

  const centerVec3 = center.toVector3();
  plane.position.copy(centerVec3);
  box.position.set(centerVec3.x, centerVec3.y, 30);
  plane.updateMatrixWorld();
  box.updateMatrixWorld();

  await instance.add(plane);
  await instance.add(box);

  const volume = new THREE.Box3();
  volume.expandByObject(plane);
  volume.expandByObject(box);

  const inputs = [plane, box];

  return {
    areaOfInterest,
    allowedSpatialResolutionRange: [1, 50, 1],
    inputs,
    center: centerVec3,
    volume,
  };
}

async function sphereScenario(instance) {
  const center = Coordinates.WGS84(48.85304790669139, 2.3497154907829603).as(
    lambert93,
  );
  const width = 300;
  const areaOfInterest = Extent.fromCenterAndSize(
    lambert93,
    center,
    width,
    width,
  );
  const plane = new Mesh(
    new PlaneGeometry(width, width),
    new MeshStandardMaterial({ color: "white" }),
  );
  const sphere = new Mesh(
    new SphereGeometry(30),
    new MeshStandardMaterial({ color: "#89dce5" }),
  );

  const centerVec3 = center.toVector3();
  plane.position.copy(centerVec3);
  sphere.position.set(centerVec3.x, centerVec3.y, 30);
  plane.updateMatrixWorld();
  sphere.updateMatrixWorld();

  await instance.add(plane);
  await instance.add(sphere);

  const inputs = [plane, sphere];

  const volume = new THREE.Box3();
  volume.expandByObject(plane);
  volume.expandByObject(sphere);

  return {
    areaOfInterest,
    inputs,
    allowedSpatialResolutionRange: [1, 50, 1],
    center: centerVec3,
    volume,
  };
}

async function cityBlockScenario(instance) {
  const center = Coordinates.WGS84(45.93506, 6.63125).as(lambert93);

  const loader = new GLTFLoader();
  const model = await loader.loadAsync(
    "https://3d.oslandia.com/giro3d/gltf/jena/scene.gltf",
  );

  model.scene.position.copy(center);
  model.scene.rotateX(Math.PI / 2);
  model.scene.updateMatrixWorld(true);

  const box = new THREE.Box3().setFromObject(model.scene);
  const actualCenter = box.getCenter(new Vector3());
  actualCenter.setZ(box.min.z);
  const size = box.getSize(new Vector3());
  const areaOfInterest = Extent.fromCenterAndSize(
    lambert93,
    actualCenter,
    size.x,
    size.y,
  );

  await instance.add(model.scene);

  return {
    areaOfInterest,
    inputs: [model.scene],
    allowedSpatialResolutionRange: [0.1, 50, 0.5],
    center: actualCenter,
    volume: box,
  };
}

async function createMap(instance, extent) {
  const map = new Map({
    backgroundColor: "white",
    lighting: {
      enabled: true,
      mode: MapLightingMode.LightBased,
      elevationLayersOnly: true,
    },
    extent,
    terrain: {
      enabled: true,
      skirts: {
        enabled: true,
        depth: 0,
      },
    },
  });

  await instance.add(map);

  const key =
    "pk.eyJ1IjoiZ2lybzNkIiwiYSI6ImNtZ3Q0NDNlNTAwY2oybHI3Ym1kcW03YmoifQ.Zl7_KZiAhqWSPjlkKDKYnQ";

  // Adds a XYZ elevation layer with MapBox terrain RGB tileset
  const elevationLayer = new ElevationLayer({
    name: "xyz_elevation",
    extent,
    // We dont want the full resolution because the terrain
    // mesh has a much lower resolution than the raster image
    resolutionFactor: 0.5,
    minmax: { min: 0, max: 5000 },
    source: new TiledImageSource({
      format: new MapboxTerrainFormat(),
      source: new XYZ({
        url: `https://api.mapbox.com/v4/mapbox.terrain-rgb/{z}/{x}/{y}.pngraw?access_token=${key}`,
        projection: "EPSG:3857",
        crossOrigin: "anonymous",
      }),
    }),
  });
  await map.addLayer(elevationLayer);

  // Adds a XYZ color layer with MapBox satellite tileset
  const satelliteLayer = new ColorLayer({
    name: "xyz_color",
    extent,
    source: new TiledImageSource({
      source: new XYZ({
        url: `https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}.webp?access_token=${key}`,
        projection: "EPSG:3857",
        crossOrigin: "anonymous",
      }),
    }),
  });
  await map.addLayer(satelliteLayer);

  return map;
}

async function terrainScenario(instance) {
  const center = Coordinates.WGS84(45.9231, 6.8697).as(lambert93);
  const size = 30_000;
  const extent = Extent.fromCenterAndSize(lambert93, center, size, size);

  const map = await createMap(instance, extent);

  return {
    areaOfInterest: extent,
    inputs: [map],
    allowedSpatialResolutionRange: [50, 1000, 100],
    center: center.toVector3(),
    volume: extent.toBox3(0, 6000),
  };
}

const scenarios = {
  box: planeBoxScenario,
  plane: planeOnlyScenario,
  terrain: terrainScenario,
  sphere: sphereScenario,
  "city-block": cityBlockScenario,
};

const colorMaps = {
  meanIrradiance: new ColorMap({
    colors: makeColorRamp("magma"),
    min: 0,
    max: 0,
  }),
  irradiation: new ColorMap({ colors: makeColorRamp("jet"), min: 0, max: 0 }),
  hoursOfSunlight: new ColorMap({
    colors: makeColorRamp("RdBu"),
    min: 0,
    max: 0,
  }),
};

const instance = new Instance({
  target: "view",
  crs: lambert93,
  backgroundColor: "white",
});

const controls = new MapControls(instance.view.camera, instance.domElement);

controls.enableDamping = true;
controls.dampingFactor = 0.2;
instance.view.setControls(controls);

const compassMaterial = new THREE.MeshBasicMaterial({
  transparent: true,
  opacity: 0.5,
  visible: false,
  color: "white",
});

Fetcher.texture("https://3d.oslandia.com/giro3d/images/compass.webp", {
  flipY: true,
}).then((t) => {
  compassMaterial.map = t;
  compassMaterial.visible = true;
  instance.notifyChange();
});

const compass = new Mesh(new PlaneGeometry(1, 1), compassMaterial);
compass.name = "compass";

instance.add(compass);

const progressBar = document.getElementById("progress");

let currentPointCloud = null;

let currentSunExposure = null;

async function run(scenarioName) {
  if (params.currentObjects.length > 0) {
    params.currentObjects.forEach((o) => instance.remove(o));
    params.currentObjects.length = 0;
  }

  if (currentPointCloud) {
    instance.remove(currentPointCloud);
    currentPointCloud = null;
  }

  params.volume = null;
  if (params.boxHelper) {
    instance.remove(params.boxHelper);
    params.boxHelper = null;
  }

  if (currentSunExposure) {
    currentSunExposure.dispose();
  }

  const scenario = scenarios[scenarioName];
  const {
    areaOfInterest,
    inputs,
    center,
    volume,
    allowedSpatialResolutionRange,
  } = await scenario(instance);

  const spatialResInput = document.getElementById("spatial-resolution");
  const [minRes, maxRes, defaultRes] = allowedSpatialResolutionRange;
  spatialResInput.min = minRes;
  spatialResInput.max = maxRes;
  spatialResInput.value = defaultRes;
  params.spatialResolution = defaultRes;

  params.inputs = inputs;
  params.originalVolume = volume;
  params.currentObjects.push(...inputs);

  const dims = areaOfInterest.dimensions();

  const compassSize = dims.width * 2;
  compass.scale.set(compassSize, compassSize, 1);
  compass.position.set(center.x, center.y, center.z - 1);
  compass.updateMatrixWorld(true);
  instance.add(compass);

  const pov = instance.view.goTo(inputs[0]);

  createLights(instance, center);

  controls.target.set(pov.target.x, pov.target.y, 0);
  controls.update();
  controls.saveState();

  instance.notifyChange();

  const setActiveAttribute = (att) => {
    updateColorMapPreview("gradient", colorMaps[att].colors);
    currentPointCloud?.setActiveAttribute(att);

    // Note that irradiation is technically in Watt-hour/m², but for readability,
    // we convert to Kilowatt-hour/m² to be displayed in the UI. Remember that
    // the actual values are in Watt-hour/m² though.
    const factor = att === "irradiation" ? 0.001 : 1;

    const min = Math.abs(colorMaps[att].min * factor);
    const max = Math.abs(colorMaps[att].max * factor);

    document.getElementById("colorMapMin").innerText = min.toFixed(2);
    document.getElementById("colorMapMax").innerText = max.toFixed(2);
  };

  const onStart = () => {
    if (currentPointCloud) {
      instance.remove(currentPointCloud);
      currentPointCloud = null;
    }

    if (currentSunExposure) {
      currentSunExposure.dispose();
      currentSunExposure = null;
    }

    const yyyy = params.date.getUTCFullYear();
    const mm = params.date.getUTCMonth();
    const dd = params.date.getUTCDay();
    const start = new Date(Date.UTC(yyyy, mm, dd, params.startTime));
    const end = new Date(Date.UTC(yyyy, mm, dd, params.endTime));
    inputs.forEach((obj) => (obj.visible = true));

    const sunExposure = new SunExposure({
      instance,
      showHelpers: params.helpers,
      objects: inputs,
      limits: Extent.fromBox3(
        lambert93,
        params.volume ?? params.originalVolume,
      ),
      spatialResolution: params.spatialResolution,
      colorMap: colorMaps["irradiation"],
      start,
      end,
      temporalResolution: params.temporalResolutionMinutes * 60,
    });

    currentSunExposure = sunExposure;

    sunExposure.addEventListener("progress", (e) => {
      const percent = (e.progress * 100).toFixed(0);
      console.log(`sun computation progress: ${percent}%`);

      progressBar.style.width = `${percent}%`;

      if (e.progress >= 1) {
        progressBar.parentElement.style.display = "none";
      }
    });

    progressBar.parentElement.style.display = "block";

    document.getElementById("attribute-group").style.display = "none";

    params.abortController = new AbortController();

    const startButton = document.getElementById("start");
    const cancelButton = document.getElementById("cancel");

    startButton.classList.add("d-none");
    cancelButton.classList.remove("d-none");

    sunExposure
      .compute({ signal: params.abortController.signal })
      .then((results) => {
        currentPointCloud = results.entity;
        if (!params.showInputs) {
          inputs.forEach((obj) => (obj.visible = false));
        }

        instance.notifyChange();
        colorMaps.meanIrradiance.min = results.variables.meanIrradiance.min;
        colorMaps.meanIrradiance.max = results.variables.meanIrradiance.max;

        colorMaps.irradiation.min = results.variables.irradiation.min;
        colorMaps.irradiation.max = results.variables.irradiation.max;

        colorMaps.hoursOfSunlight.min = results.variables.hoursOfSunlight.min;
        colorMaps.hoursOfSunlight.max = results.variables.hoursOfSunlight.max;

        results.entity.setAttributeColorMap(
          "meanIrradiance",
          colorMaps.meanIrradiance,
        );
        results.entity.setAttributeColorMap(
          "irradiation",
          colorMaps.irradiation,
        );
        results.entity.setAttributeColorMap(
          "hoursOfSunlight",
          colorMaps.hoursOfSunlight,
        );

        setActiveAttribute(params.activeAttribute);

        document.getElementById("attribute-group").style.display = "flex";
      })
      .catch(console.warn)
      .finally(() => {
        startButton.classList.remove("d-none");
        cancelButton.classList.add("d-none");
        progressBar.parentElement.style.display = "none";
      });
  };

  bindButton("start", onStart);
  bindButton("cancel", () => params.abortController.abort());

  bindDropDown("attribute", (att) => {
    params.activeAttribute = att;
    setActiveAttribute(att);
  });
}

const drawTool = new DrawTool({ instance });

function drawAoiShape() {
  drawTool
    .createPolygon({
      showVertices: true,
    })
    .then((shape) => {
      const box = new THREE.Box3().setFromPoints([...shape.points]);
      box.min.setZ(-100000);
      box.max.setZ(100000);
      box.intersect(params.originalVolume);
      params.volume = box;
      const boxHelper = new THREE.Box3Helper(box, "cyan");
      boxHelper.updateMatrixWorld(true);
      if (params.boxHelper) {
        instance.remove(params.boxHelper);
      }
      params.boxHelper = boxHelper;
      instance.add(boxHelper);
      instance.remove(shape);
      instance.notifyChange(boxHelper);
    });
}

bindNumberInput("spatial-resolution", (v) => (params.spatialResolution = v));
bindNumberInput(
  "temporal-resolution",
  (v) => (params.temporalResolutionMinutes = v),
);
bindDropDown("scenario", (s) => {
  run(s).catch(console.error);
});
bindNumberInput("start-time", (h) => (params.startTime = h));
bindNumberInput("end-time", (h) => (params.endTime = h));
bindTextInput("date", (date) => {
  params.date = new Date(date);
});
bindButton("draw-aoi", drawAoiShape);
bindToggle("show-helpers", (v) => (params.helpers = v));
bindToggle("show-inputs", (v) => {
  params.showInputs = v;
  params.inputs.forEach((obj) => (obj.visible = v));
  instance.notifyChange();
});

run("box").catch(console.error);

Inspector.attach("inspector", instance);
index.html
<!doctype html>
<html lang="en">
  <head>
    <title>Sun exposure</title>
    <meta charset="UTF-8" />
    <meta name="name" content="sun-exposure" />
    <meta
      name="description"
      content="Use the &lt;a href=&#34;../apidoc/classes/interactions_SunExposure.SunExposure.html&#34; target=&#34;_blank&#34;&gt;&lt;code&gt;SunExposure&lt;/code&gt;&lt;/a&gt; tool to compute solar exposition for a time interval."
    />
    <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: 24rem">
      <div class="card">
        <div class="card-header">Parameters</div>
        <div class="card-body">
          <!-- Scenario selector -->
          <div class="input-group">
            <label class="input-group-text col-4" for="scenario"
              >Scenario</label
            >
            <select
              class="form-select"
              id="scenario"
              autocomplete="off"
              title="Sets the solar parameter"
            >
              <option value="box">Plane + Box</option>
              <option value="plane">Plane only</option>
              <option value="sphere">Plane + Sphere</option>
              <option value="terrain">Terrain only</option>
              <option value="city-block">Photogrammetry mesh</option>
            </select>
          </div>

          <!-- Date -->
          <label for="date" class="form-label mt-3">Date (UTC)</label>
          <input
            type="date"
            class="form-control"
            id="date"
            value="2025-06-21"
            autocomplete="off"
          />

          <label for="start-time" class="form-label mt-3"
            >Start/end times (h)</label
          >

          <div class="d-flex">
            <input
              type="number"
              min="0"
              max="23"
              value="8"
              step="1"
              class="form-control"
              id="start-time"
              autocomplete="off"
            />

            <input
              type="number"
              min="0"
              max="23"
              value="16"
              step="1"
              class="form-control ms-2"
              id="end-time"
              autocomplete="off"
            />
          </div>

          <!-- Spatial resolution -->
          <label for="spatial-resolution" class="form-label mt-3"
            >Spatial resolution (m)</label
          >
          <input
            type="number"
            min="0.25"
            max="100"
            value="10"
            step="0.1"
            class="form-control"
            id="spatial-resolution"
            autocomplete="off"
          />
          <!-- Temporal resolution -->
          <label for="temporal-resolution" class="form-label mt-3"
            >Temporal resolution (minutes)</label
          >
          <input
            type="number"
            min="1"
            max="120"
            value="60"
            step="1"
            class="form-control"
            id="temporal-resolution"
            autocomplete="off"
          />

          <!-- Show/Hide helpers -->
          <div class="form-check form-switch mt-3">
            <input
              class="form-check-input"
              type="checkbox"
              role="switch"
              id="show-helpers"
              autocomplete="off"
            />
            <label class="form-check-label" for="show-helpers"
              >Show helpers</label
            >
          </div>

          <button
            type="button"
            class="btn btn-secondary w-100 mt-3"
            id="draw-aoi"
          >
            Draw simulation area
          </button>

          <button type="button" class="btn btn-primary w-100 mt-3" id="start">
            Compute sun exposure
          </button>
          <button
            type="button"
            class="btn btn-danger w-100 mt-3 d-none"
            id="cancel"
          >
            Cancel
          </button>

          <div class="progress mt-3" role="progressbar" style="display: none">
            <div
              class="progress-bar bg-info progress-bar-striped progress-bar-animated text-dark"
              id="progress"
              style="width: 0%"
            >
              Computing...
            </div>
          </div>

          <!-- Show/Hide inputs -->
          <div class="form-check form-switch mt-3">
            <input
              class="form-check-input"
              type="checkbox"
              role="switch"
              checked
              id="show-inputs"
              autocomplete="off"
            />
            <label class="form-check-label" for="show-inputs"
              >Show objects</label
            >
          </div>

          <!-- Active attribute selector -->
          <div
            class="input-group mt-3"
            id="attribute-group"
            style="display: none"
          >
            <label class="input-group-text col-4" for="attribute"
              >Variable</label
            >
            <select
              class="form-select"
              id="attribute"
              autocomplete="off"
              title="Sets the solar parameter"
            >
              <option value="irradiation">Irradiation (KWh/m²)</option>
              <option value="meanIrradiance">Mean irradiance (W/m²)</option>
              <option value="hoursOfSunlight">Hours of sunlight</option>
            </select>

            <!-- Gradient preview -->
            <div class="mt-3 w-100">
              <canvas
                id="gradient"
                height="32"
                class="w-100 border rounded"
                style="height: 32px; image-rendering: pixelated"
              ></canvas>

              <div class="w-100">
                <p class="float-start mb-0" id="colorMapMin">0</p>
                <p class="float-end mb-0" id="colorMapMax">100</p>
              </div>
            </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": "sun-exposure",
    "dependencies": {
        "@giro3d/giro3d": "2.0.0"
    },
    "devDependencies": {
        "vite": "^3.2.3"
    },
    "scripts": {
        "start": "vite",
        "build": "vite build"
    }
}
vite.config.js
import { defineConfig } from "vite";

export default defineConfig({
  build: {
    target: 'esnext',
  },
  optimizeDeps: {
    esbuildOptions: {
      target: 'esnext',
    },
  },
})