Skip to content

Sky, Fog, and Terrain

Combine 3D terrain, a sky layer, and atmospheric fog to create a realistic, immersive landscape. Adjust the sky colors and blend values live using the controls below.

No external libraries needed. Sky, fog, and terrain are all built into MapMetrics GL.

The Three Layers Explained

Think of these as three separate things stacked on top of each other:

☁️  Sky    → paints the area above the horizon (blue sky, sunrise colors)
🌫️  Fog    → adds haze to distant mountains and the horizon
🏔️  Terrain → pushes the land surface into 3D

Sky Properties

The sky style property controls the background above the horizon. Use map.setSky() to update it at runtime:

PropertyWhat it controls
sky-colorThe main sky color (top of the sky)
horizon-colorColor at the horizon line
fog-colorSky color blending into the fog zone
sky-horizon-blend0 = sharp sky edge · 1 = gradual blend into horizon
horizon-fog-blend0 = sharp horizon · 1 = smooth blend into fog zone
fog-ground-blend0 = fog starts high · 1 = fog blends all the way to ground
javascript
// Set initial sky in the style
style: {
  sky: {
    'sky-color': '#199EF3',
    'sky-horizon-blend': 0.5,
    'horizon-color': '#fbe4ff',
    'horizon-fog-blend': 0.5,
    'fog-color': '#ffffff',
    'fog-ground-blend': 0.5,
  }
}

// Update sky at runtime
map.setSky({
  'sky-color': '#ff6b35',    // sunset orange
  'horizon-color': '#ffd700',
  'fog-color': '#ff8c69',
});

Fog Properties

The fog property adds atmospheric haze. Toggle it with map.setFog():

javascript
// Enable fog
map.setFog({
  color: '#ffffff',          // fog color at ground level
  'high-color': '#245cdf',   // fog color at altitude
  'horizon-blend': 0.05,     // how fast fog kicks in near horizon
  'space-color': '#000000',  // outer space color (globe view)
  'star-intensity': 0.15,    // star brightness in space zone
});

// Disable fog
map.setFog(null);

Preset Sky Themes

Themesky-colorhorizon-colorfog-color
Day#199EF3#fbe4ff#ffffff
Sunset#ff6b35#ffd700#ff8c69
Night#0a0a2e#1a1a4e#0d0d2b

Complete Example

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link href="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.css" rel="stylesheet" />
    <script src="https://cdn.mapmetrics-atlas.net/versions/latest/mapmetrics-gl.js"></script>
    <style>
      body { margin: 0; }
      #map { height: 100vh; width: 100%; }
      #controls { position: absolute; top: 10px; left: 10px; background: white; padding: 12px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.2); font-size: 13px; min-width: 280px; }
      .row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; gap: 8px; }
      .row label { flex: 1; color: #374151; }
      .row input[type=range] { flex: 1; }
      .row span { min-width: 30px; text-align: right; color: #6b7280; }
      .btns { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }
      button { padding: 6px 12px; border: none; border-radius: 6px; cursor: pointer; color: white; font-size: 12px; }
    </style>
  </head>
  <body>
    <div id="map"></div>
    <div id="controls">
      <div class="row"><label>sky-color</label><input id="sky-color" type="color" value="#199EF3" /></div>
      <div class="row"><label>horizon-color</label><input id="horizon-color" type="color" value="#fbe4ff" /></div>
      <div class="row"><label>fog-color</label><input id="fog-color" type="color" value="#ffffff" /></div>
      <div class="row"><label>sky-horizon-blend</label><input id="sky-horizon-blend" type="range" min="0" max="1" step="0.01" value="0.5" /><span id="v-shb">0.50</span></div>
      <div class="row"><label>horizon-fog-blend</label><input id="horizon-fog-blend" type="range" min="0" max="1" step="0.01" value="0.5" /><span id="v-hfb">0.50</span></div>
      <div class="row"><label>fog-ground-blend</label><input id="fog-ground-blend" type="range" min="0" max="1" step="0.01" value="0.5" /><span id="v-fgb">0.50</span></div>
      <div class="btns">
        <button id="fog-on" style="background:#3b82f6;">Fog ON</button>
        <button id="fog-off" style="background:#6b7280;">Fog OFF</button>
        <button id="preset-day" style="background:#0ea5e9;">Day</button>
        <button id="preset-sunset" style="background:#f97316;">Sunset</button>
        <button id="preset-night" style="background:#1e1b4b;">Night</button>
      </div>
    </div>
    <script>
      const map = new mapmetricsgl.Map({
        container: 'map',
        zoom: 10,
        center: [11.39085, 47.27574],
        pitch: 65,
        maxPitch: 85,
        style: {
          version: 8,
          sources: {
            osm: {
              type: 'raster',
              tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
              tileSize: 256,
              attribution: '&copy; OpenStreetMap Contributors',
              maxzoom: 19,
            },
            terrainSource: {
              type: 'raster-dem',
              tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
              tileSize: 256,
              encoding: 'terrarium',
              maxzoom: 15,
            },
          },
          layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
          terrain: { source: 'terrainSource', exaggeration: 1.5 },
          sky: {
            'sky-color': '#199EF3',
            'sky-horizon-blend': 0.5,
            'horizon-color': '#fbe4ff',
            'horizon-fog-blend': 0.5,
            'fog-color': '#ffffff',
            'fog-ground-blend': 0.5,
          },
          fog: { color: '#ffffff', 'high-color': '#245cdf', 'horizon-blend': 0.05 },
        },
      });

      map.addControl(new mapmetricsgl.NavigationControl({ visualizePitch: true }), 'top-right');
      map.addControl(new mapmetricsgl.TerrainControl({ source: 'terrainSource', exaggeration: 1.5 }), 'top-right');

      const sky = {
        'sky-color': '#199EF3', 'horizon-color': '#fbe4ff', 'fog-color': '#ffffff',
        'sky-horizon-blend': 0.5, 'horizon-fog-blend': 0.5, 'fog-ground-blend': 0.5,
        'atmosphere-blend': ['interpolate', ['linear'], ['zoom'], 0, 1, 12, 0],
      };

      map.on('load', () => {
        document.getElementById('sky-color').addEventListener('input', e => { sky['sky-color'] = e.target.value; map.setSky(sky); });
        document.getElementById('horizon-color').addEventListener('input', e => { sky['horizon-color'] = e.target.value; map.setSky(sky); });
        document.getElementById('fog-color').addEventListener('input', e => { sky['fog-color'] = e.target.value; map.setSky(sky); });

        [['sky-horizon-blend','v-shb'], ['horizon-fog-blend','v-hfb'], ['fog-ground-blend','v-fgb']].forEach(([k, vid]) => {
          document.getElementById(k).addEventListener('input', e => {
            sky[k] = parseFloat(e.target.value);
            document.getElementById(vid).textContent = sky[k].toFixed(2);
            map.setSky(sky);
          });
        });

        document.getElementById('fog-on').addEventListener('click', () => map.setFog({ color: '#ffffff', 'high-color': '#245cdf', 'horizon-blend': 0.05 }));
        document.getElementById('fog-off').addEventListener('click', () => map.setFog(null));

        const presets = {
          day:    { 'sky-color': '#199EF3', 'horizon-color': '#fbe4ff', 'fog-color': '#ffffff', 'sky-horizon-blend': 0.5, 'horizon-fog-blend': 0.5, 'fog-ground-blend': 0.5 },
          sunset: { 'sky-color': '#ff6b35', 'horizon-color': '#ffd700', 'fog-color': '#ff8c69', 'sky-horizon-blend': 0.4, 'horizon-fog-blend': 0.6, 'fog-ground-blend': 0.7 },
          night:  { 'sky-color': '#0a0a2e', 'horizon-color': '#1a1a4e', 'fog-color': '#0d0d2b', 'sky-horizon-blend': 0.3, 'horizon-fog-blend': 0.4, 'fog-ground-blend': 0.8 },
        };
        ['day', 'sunset', 'night'].forEach(name => {
          document.getElementById('preset-' + name).addEventListener('click', () => {
            Object.assign(sky, presets[name]);
            document.getElementById('sky-color').value = sky['sky-color'];
            document.getElementById('horizon-color').value = sky['horizon-color'];
            document.getElementById('fog-color').value = sky['fog-color'];
            document.getElementById('sky-horizon-blend').value = sky['sky-horizon-blend'];
            document.getElementById('v-shb').textContent = sky['sky-horizon-blend'].toFixed(2);
            document.getElementById('horizon-fog-blend').value = sky['horizon-fog-blend'];
            document.getElementById('v-hfb').textContent = sky['horizon-fog-blend'].toFixed(2);
            document.getElementById('fog-ground-blend').value = sky['fog-ground-blend'];
            document.getElementById('v-fgb').textContent = sky['fog-ground-blend'].toFixed(2);
            map.setSky(sky);
          });
        });
      });
    </script>
  </body>
</html>
jsx
import React, { useEffect, useRef, useState, useCallback } from 'react';
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';

const PRESETS = {
  day:    { 'sky-color': '#199EF3', 'horizon-color': '#fbe4ff', 'fog-color': '#ffffff', 'sky-horizon-blend': 0.5, 'horizon-fog-blend': 0.5, 'fog-ground-blend': 0.5 },
  sunset: { 'sky-color': '#ff6b35', 'horizon-color': '#ffd700', 'fog-color': '#ff8c69', 'sky-horizon-blend': 0.4, 'horizon-fog-blend': 0.6, 'fog-ground-blend': 0.7 },
  night:  { 'sky-color': '#0a0a2e', 'horizon-color': '#1a1a4e', 'fog-color': '#0d0d2b', 'sky-horizon-blend': 0.3, 'horizon-fog-blend': 0.4, 'fog-ground-blend': 0.8 },
};

const SkyFogTerrain = () => {
  const mapContainer = useRef(null);
  const map = useRef(null);
  const [sky, setSkyState] = useState(PRESETS.day);
  const [fogEnabled, setFogEnabled] = useState(true);

  useEffect(() => {
    if (map.current) return;
    map.current = new mapmetricsgl.Map({
      container: mapContainer.current,
      zoom: 10,
      center: [11.39085, 47.27574],
      pitch: 65,
      maxPitch: 85,
      style: {
        version: 8,
        sources: {
          osm: {
            type: 'raster',
            tiles: ['https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'],
            tileSize: 256,
            attribution: '&copy; OpenStreetMap Contributors',
            maxzoom: 19,
          },
          terrainSource: {
            type: 'raster-dem',
            tiles: ['https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'],
            tileSize: 256,
            encoding: 'terrarium',
            maxzoom: 15,
          },
        },
        layers: [{ id: 'osm', type: 'raster', source: 'osm' }],
        terrain: { source: 'terrainSource', exaggeration: 1.5 },
        sky: { ...PRESETS.day, 'atmosphere-blend': ['interpolate', ['linear'], ['zoom'], 0, 1, 12, 0] },
        fog: { color: '#ffffff', 'high-color': '#245cdf', 'horizon-blend': 0.05 },
      },
    });

    map.current.addControl(new mapmetricsgl.NavigationControl({ visualizePitch: true }), 'top-right');
    map.current.addControl(new mapmetricsgl.TerrainControl({ source: 'terrainSource', exaggeration: 1.5 }), 'top-right');

    return () => { map.current?.remove(); map.current = null; };
  }, []);

  const updateSky = useCallback((patch) => {
    setSkyState(prev => {
      const next = { ...prev, ...patch };
      map.current?.setSky({ ...next, 'atmosphere-blend': ['interpolate', ['linear'], ['zoom'], 0, 1, 12, 0] });
      return next;
    });
  }, []);

  const applyPreset = (name) => {
    updateSky(PRESETS[name]);
  };

  const toggleFog = (on) => {
    setFogEnabled(on);
    if (on) map.current?.setFog({ color: '#ffffff', 'high-color': '#245cdf', 'horizon-blend': 0.05 });
    else map.current?.setFog(null);
  };

  const btn = (active) => ({ padding: '6px 14px', border: 'none', borderRadius: 6, cursor: 'pointer', color: 'white', background: active ? '#3b82f6' : '#6b7280' });

  return (
    <div>
      <div ref={mapContainer} style={{ height: 500, width: '100%' }} />

      <div style={{ marginTop: 12, background: '#f8fafc', border: '1px solid #e2e8f0', borderRadius: 8, padding: '14px 16px', fontSize: 13 }}>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px 20px' }}>

          {[['sky-color', 'sky-color'], ['horizon-color', 'horizon-color'], ['fog-color', 'fog-color']].map(([key, label]) => (
            <label key={key} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
              <span style={{ minWidth: 130, color: '#374151' }}>{label}</span>
              <input type="color" value={sky[key]} onChange={e => updateSky({ [key]: e.target.value })}
                style={{ width: 44, height: 28, border: 'none', cursor: 'pointer', borderRadius: 4 }} />
            </label>
          ))}

          {[['sky-horizon-blend', 'sky-horizon-blend'], ['horizon-fog-blend', 'horizon-fog-blend'], ['fog-ground-blend', 'fog-ground-blend']].map(([key, label]) => (
            <label key={key} style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <span style={{ minWidth: 130, color: '#374151' }}>{label}</span>
              <input type="range" min="0" max="1" step="0.01" value={sky[key]}
                onChange={e => updateSky({ [key]: parseFloat(e.target.value) })} style={{ flex: 1 }} />
              <span style={{ minWidth: 30, textAlign: 'right', color: '#6b7280' }}>{sky[key].toFixed(2)}</span>
            </label>
          ))}

        </div>

        <div style={{ marginTop: 10, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          <button style={btn(fogEnabled)} onClick={() => toggleFog(true)}>Fog ON</button>
          <button style={btn(!fogEnabled)} onClick={() => toggleFog(false)}>Fog OFF</button>
          <button style={{ ...btn(false), background: '#0ea5e9' }} onClick={() => applyPreset('day')}>Day</button>
          <button style={{ ...btn(false), background: '#f97316' }} onClick={() => applyPreset('sunset')}>Sunset</button>
          <button style={{ ...btn(false), background: '#1e1b4b' }} onClick={() => applyPreset('night')}>Night</button>
        </div>
      </div>
    </div>
  );
};

export default SkyFogTerrain;

For more information, visit the MapMetrics GitHub repository.