Skip to main content

Overview

Threebox provides realistic sunlight simulation using the SunCalc library to calculate accurate sun positions based on date, time, and coordinates. This enables:
  • Dynamic directional lighting that follows the sun
  • Atmospheric sky layer with accurate sun position
  • Terrain/ground layer with sun-based opacity
  • Automatic light intensity based on sun altitude

setSunlight

Updates the sun light position and intensity based on date/time and coordinates.
tb.setSunlight(date, coords)
date
Date
default:"new Date()"
The date and time to calculate sun position for. Defaults to current time.
coords
{lng, lat} | [lng, lat]
Geographic coordinates for sun calculation. Defaults to map.getCenter(). Can be an object with lng/lat properties or a [longitude, latitude] array.
Requires: realSunlight: true in Threebox constructor options. Example:
const tb = new Threebox(map, gl, {
  realSunlight: true,
  realSunlightHelper: true // Shows light direction helper
})

// Set sunlight for specific date/time
const sunset = new Date('2024-06-21T20:30:00')
tb.setSunlight(sunset, [-122.4194, 37.7749])

// Update to current time at map center
tb.setSunlight()

// Animate sun through the day
let hour = 6
setInterval(() => {
  const date = new Date()
  date.setHours(hour)
  tb.setSunlight(date)
  hour = (hour + 1) % 24
}, 1000)

Behavior

When setSunlight() is called:
  1. Sun Position Calculation: Calculates azimuth and altitude using SunCalc
  2. Directional Light: Positions and orients the main directional light
  3. Light Intensity: Sets intensity based on sun altitude (0 when below horizon)
  4. Hemisphere Light: Updates ambient hemisphere light intensity
  5. Mapbox Light: Updates Mapbox’s built-in lighting to match
  6. Sky Layer: Updates atmospheric sky if enabled
  7. Ground/Terrain: Updates terrain opacity if enabled

Light Properties Updated

dirLight.position
THREE.Vector3
Position calculated from sun azimuth and altitude
dirLight.intensity
number
Intensity = Math.max(sin(altitude), 0) (0-1 range)
hemiLight.intensity
number
Intensity = Math.max(sin(altitude), 0.1) (minimum 0.1)

Mapbox Light Synchronization

The method also updates Mapbox’s native lighting:
map.setLight({
  anchor: 'map',
  position: [3, 180 + azimuth_degrees, 90 - altitude_degrees],
  intensity: Math.cos(altitude),
  color: `hsl(40, ${50 * Math.cos(altitude)}%, ${Math.max(20, 20 + 96 * Math.sin(altitude))}%)`
}, { duration: 0 })
This ensures fill-extrusion layers are lit consistently with 3D objects.

getSunSky

Calculates sun position for the sky atmospheric layer.
const sunPosition = tb.getSunSky(date, sunPos)
date
Date
Date/time for calculation. Defaults to current time if omitted.
sunPos
{azimuth, altitude}
Pre-calculated sun position. If omitted, calculates from date and map center.
Returns: [sunAzimuth, sunAltitude] - Array with azimuth and altitude in degrees.
sunAzimuth
number
Sun azimuth in degrees (0-360)
sunAltitude
number
Sun altitude in degrees (0-90)
Example:
const skyPosition = tb.getSunSky(new Date())
console.log('Azimuth:', skyPosition[0], 'Altitude:', skyPosition[1])

// Use with pre-calculated position
const sunPos = tb.getSunPosition(new Date(), [-122.4194, 37.7749])
const skyPos = tb.getSunSky(null, sunPos)

updateSunSky

Updates the Mapbox sky atmospheric layer with new sun position.
tb.updateSunSky(sunPosition)
sunPosition
[azimuth, altitude]
required
Sun position array from getSunSky() with azimuth and altitude in degrees
Requires: sky: true in Threebox constructor options. Example:
const tb = new Threebox(map, gl, {
  realSunlight: true,
  sky: true // Enables atmospheric sky layer
})

// Update sky to match a specific time
const skyPos = tb.getSunSky(new Date('2024-12-21T12:00:00'))
tb.updateSunSky(skyPos)

// Automatic update (if realSunlight is enabled)
tb.setSunlight() // Calls updateSunSky internally

Sky Layer

The sky atmospheric layer is created with:
map.addLayer({
  id: 'sky',
  type: 'sky',
  paint: {
    'sky-opacity': [
      'interpolate',
      ['linear'],
      ['zoom'],
      0, 0,
      5, 0.3,
      8, 1
    ],
    'sky-type': 'atmosphere',
    'sky-atmosphere-sun': [0.0, 90.0], // Updated by updateSunSky
    'sky-atmosphere-sun-intensity': 5
  }
})

updateSunGround

Updates terrain/ground layer opacity based on sun altitude.
tb.updateSunGround(sunPosition)
sunPosition
{azimuth, altitude}
required
Sun position object from getSunPosition() with azimuth and altitude in radians
Requires: A terrain layer to be configured (typically with terrain: true option). Example:
const tb = new Threebox(map, gl, {
  realSunlight: true,
  terrain: true // Enables terrain layer
})

// Update ground lighting
const sunPos = tb.getSunPosition(new Date(), map.getCenter())
tb.updateSunGround(sunPos)

// Automatic update
tb.setSunlight() // Calls updateSunGround internally

Opacity Calculation

The ground layer opacity is calculated as:
const opacity = Math.max(Math.min(1, sunPos.altitude * 4), 0.25)
map.setPaintProperty(terrainLayer, 'raster-opacity', opacity)
  • Minimum opacity: 0.25 (night)
  • Maximum opacity: 1.0 (day)
  • Gradual transition based on sun altitude

Supporting Methods

getSunPosition

Calculates sun position using SunCalc library.
const sunPos = tb.getSunPosition(date, coords)
date
Date
required
Date and time for calculation
coords
[lng, lat]
required
Geographic coordinates
Returns: {azimuth, altitude} - Sun position in radians
azimuth
number
Sun azimuth in radians (-π to π)
altitude
number
Sun altitude in radians (-π/2 to π/2)

getSunTimes

Calculates sun times (sunrise, sunset, etc.) for a given date and location.
const times = tb.getSunTimes(date, coords)
date
Date
required
Date for calculation
coords
[lng, lat]
required
Geographic coordinates
Returns: Object with Date values for various sun phases:
sunrise
Date
Sunrise (top edge of sun appears on horizon)
sunriseEnd
Date
Sunrise ends (bottom edge touches horizon)
goldenHourEnd
Date
Morning golden hour ends
solarNoon
Date
Solar noon (sun at highest position)
goldenHour
Date
Evening golden hour starts
sunsetStart
Date
Sunset starts
sunset
Date
Sunset (sun disappears below horizon)
dusk
Date
Dusk (evening nautical twilight starts)
nauticalDusk
Date
Nautical dusk
night
Date
Night starts
nadir
Date
Darkest moment (sun lowest position)
nightEnd
Date
Night ends
nauticalDawn
Date
Nautical dawn
dawn
Date
Dawn (morning civil twilight starts)

Complete Example

const map = new mapboxgl.Map({
  container: 'map',
  style: 'mapbox://styles/mapbox/satellite-v9',
  center: [-122.4194, 37.7749],
  zoom: 15,
  pitch: 60
})

map.on('style.load', () => {
  map.addLayer({
    id: 'custom_layer',
    type: 'custom',
    renderingMode: '3d',
    onAdd: function (map, gl) {
      window.tb = new Threebox(map, gl, {
        realSunlight: true,
        realSunlightHelper: true,
        sky: true,
        terrain: true
      })

      // Get sun times for today
      const times = tb.getSunTimes(new Date(), map.getCenter())
      console.log('Sunrise:', times.sunrise)
      console.log('Sunset:', times.sunset)

      // Animate through the day
      let currentTime = new Date(times.sunrise)
      const sunsetTime = times.sunset.getTime()

      function animateSun() {
        if (currentTime.getTime() < sunsetTime) {
          tb.setSunlight(currentTime)
          currentTime = new Date(currentTime.getTime() + 60000) // +1 minute
          setTimeout(animateSun, 100)
        }
      }

      animateSun()

      // Load a building
      tb.loadObj({
        obj: '/models/building.glb',
        type: 'gltf',
        scale: 1,
        units: 'meters'
      }, (model) => {
        model.setCoords([-122.4194, 37.7749, 0])
        tb.add(model)
      })
    },
    render: function (gl, matrix) {
      tb.update()
    }
  })
})

Notes

  • Sun calculations use the SunCalc library (https://github.com/mourner/suncalc)
  • Azimuth is measured from north (0°) clockwise
  • Altitude is measured from horizon (0°) to zenith (90°)
  • Light intensity automatically becomes 0 when sun is below horizon
  • The realSunlightHelper option shows a visual indicator of light direction
  • Sky and terrain layers update automatically when setSunlight() is called
  • Performance: setSunlight() includes caching to avoid redundant calculations

Build docs developers (and LLMs) love