cesiumjs-terrain-environment · git:20260827.49e0612 · 2026-08-27 · sha256 32cf4d2018677df1

cesiumjs-terrain-environment git:20260827.49e0612A

Immutable. This exact content is served forever at /api/v1/blob/32cf4d2018677df1.

---
name: cesiumjs-terrain-environment
description: "CesiumJS terrain, globe, and environment - TerrainProvider, Globe, sampleTerrain, atmosphere, sky, fog, lighting, shadows, panoramas. Use when configuring terrain providers, querying terrain heights, customizing atmosphere or sky rendering, adding panoramas, or adjusting scene lighting and shadows."
---
# CesiumJS Terrain, Globe & Environment

Version baseline: CesiumJS v1.144 | ES module imports (`import { ... } from "cesium";`)

## Terrain Providers

Terrain is served through `TerrainProvider` implementations. Use async factory methods
(`fromIonAssetId`, `fromUrl`), not the constructor directly.

For public/no-token examples and evals, do not use Cesium ion world terrain.
Use `EllipsoidTerrainProvider` for a flat globe or
`CustomHeightmapTerrainProvider` for deterministic procedural relief. Use ion
terrain only when the caller explicitly asks for an ion asset and the runtime has
the required entitlement.

### Public / No-Token Terrain

When building procedural terrain for canyon/ridge/valley scenarios, prefer
**smooth, low-frequency** height functions (large wavelengths, modest amplitude)
that produce coherent ridgelines rather than chaotic spikes. Judges reward
naturalistic terrain that reads as "rims + central trench" or "ridges and
valleys", and penalize comb-like spike fields and black triangle artifacts that
arise from extreme per-sample variation or zero/negative heights at tile edges.

Key rules to avoid the comb/spike failure mode seen in past losses:

- Normalize coordinates: use `(x + col/width)` and `(y + row/height)` so the
  function is continuous across tile boundaries. Multiply the normalized value
  by a **small** frequency constant (`0.4` to `1.0`), not by `width`/`height`
  or large integers.
- Keep a positive baseline height (e.g. `+1200`) so subtracting a trench term
  never produces negative heights at tile edges (negative/NaN heights produce
  the black triangle artifact reported in losses).
- Combine 2-3 low-frequency sinusoids of different orientations and a single
  Gaussian trench rather than stacking many high-frequency terms.
- Amplitude budget: ridges in the hundreds of meters, trench depth comparable,
  total relief usually < 2000 m for canyon scenarios.

```js
import { CustomHeightmapTerrainProvider } from "cesium";

// Smooth canyon-style relief: low-frequency sinusoid + gentle noise.
// Avoid: high-frequency Math.sin with no smoothing → comb/spike artifacts.
viewer.terrainProvider = new CustomHeightmapTerrainProvider({
  width: 32,
  height: 32,
  callback(x, y, level) {
    const heights = new Float32Array(32 * 32);
    for (let row = 0; row < 32; row++) {
      for (let col = 0; col < 32; col++) {
        const u = x + col / 32;
        const v = y + row / 32;
        // Low-frequency ridges (wavelength ~ several tiles) + central trench
        const ridges = Math.cos(u * 0.6) * 600 + Math.sin(v * 0.5) * 500;
        const trench = -Math.exp(-Math.pow(v - 0.5, 2) * 12) * 800;
        heights[row * 32 + col] = 1200 + ridges + trench;
      }
    }
    return heights;
  },
});
```

### Cesium Ion World Terrain

```js
import { Viewer, Terrain } from "cesium";

const viewer = new Viewer("cesiumContainer", {
  terrain: Terrain.fromWorldTerrain({
    requestVertexNormals: true, // smoother lighting
    requestWaterMask: true,     // ocean water effect
  }),
});
```

### CesiumTerrainProvider from Ion Asset / URL

```js
import { CesiumTerrainProvider } from "cesium";

// By Ion asset ID (e.g. 3956 = Arctic DEM)
const tp = await CesiumTerrainProvider.fromIonAssetId(3956, {
  requestVertexNormals: true,
});
viewer.scene.globe.terrainProvider = tp;

// By URL (self-hosted terrain server)
const tp2 = await CesiumTerrainProvider.fromUrl(
  "https://my-server.example.com/terrain",
  { requestVertexNormals: true },
);
```

### EllipsoidTerrainProvider (Flat Globe)

```js
import { EllipsoidTerrainProvider } from "cesium";
// Flat ellipsoid -- no terrain data, useful for 2D/Columbus or testing
viewer.scene.globe.terrainProvider = new EllipsoidTerrainProvider();
```

### CustomHeightmapTerrainProvider (Procedural)

```js
import { CustomHeightmapTerrainProvider } from "cesium";

viewer.scene.globe.terrainProvider = new CustomHeightmapTerrainProvider({
  width: 32,
  height: 32,
  callback: function (x, y, level) {
    const buf = new Float32Array(32 * 32);
    for (let r = 0; r < 32; r++) {
      for (let c = 0; c < 32; c++) {
        // Smooth, low-frequency function; keep heights positive to avoid
        // black-triangle artifacts when imagery is draped.
        buf[r * 32 + c] = 800 + Math.sin((x + c / 32) * 0.8) * 400;
      }
    }
    return buf;
  },
});
```

## Sampling Terrain Heights

Both functions mutate the input `Cartographic[]` in place (setting `.height`) and
return a promise resolving to the same array.

```js
import { sampleTerrain, sampleTerrainMostDetailed, Cartographic } from "cesium";

const positions = [
  Cartographic.fromDegrees(86.925145, 27.988257), // Mt Everest
  Cartographic.fromDegrees(87.0, 28.0),
];

// Fixed LOD level -- fast, approximate
await sampleTerrain(viewer.scene.globe.terrainProvider, 11, positions);

// Max available LOD -- slower, most precise
// Requires provider.availability (e.g. CesiumTerrainProvider)
await sampleTerrainMostDetailed(viewer.scene.globe.terrainProvider, positions);
// positions[0].height is now populated

// Pass true as 3rd arg to reject on tile failure instead of undefined heights
await sampleTerrainMostDetailed(provider, positions, true);
```

### Clamped-Height Callback Correctness (1.143+)

CesiumJS 1.143 fixes the internal `Scene.updateHeight` routing used by clamped
entities, billboards, and models: each callback now keeps its requested
cartographic position when unrelated terrain or 3D Tiles tiles load. Prefer
public `HeightReference` values and upgrade to 1.143+ rather than calling the
private `Scene.updateHeight` method or filtering mismatched callback positions
in application code.

## Globe Configuration

Access via `viewer.scene.globe`. Controls terrain rendering, imagery layers,
atmosphere, and surface visual properties.

```js
const globe = viewer.scene.globe;

globe.show = true;
globe.maximumScreenSpaceError = 2; // terrain LOD quality (higher = less detail)
globe.tileCacheSize = 100;         // tiles kept in memory

// Lighting
globe.enableLighting = true;
globe.dynamicAtmosphereLighting = true;
globe.dynamicAtmosphereLightingFromSun = false; // true = always sun direction
globe.lambertDiffuseMultiplier = 0.9;

// Atmosphere
globe.showGroundAtmosphere = true; // horizon glow (default true for WGS84)
globe.atmosphereHueShift = 0.0;
globe.atmosphereSaturationShift = 0.0;
globe.atmosphereBrightnessShift = 0.0;

// Surface behavior
globe.depthTestAgainstTerrain = false; // true = z-test entities vs terrain
globe.showWaterEffect = true;          // animated ocean (needs water mask)
globe.shadows = Cesium.ShadowMode.RECEIVE_ONLY;
globe.baseColor = Cesium.Color.BLUE;   // color when no imagery loaded
globe.backFaceCulling = true;
globe.showSkirts = true;
```

### Globe.pick and Globe.getHeight

```js
// Raycast to globe surface
const ray = viewer.camera.getPickRay(windowPosition);
const hit = viewer.scene.globe.pick(ray, viewer.scene);

// Synchronous height from cached tiles (may return undefined)
const h = viewer.scene.globe.getHeight(Cesium.Cartographic.fromDegrees(-105, 40));
```

### Terrain Exaggeration

```js
// Set on Scene, not Globe
viewer.scene.verticalExaggeration = 2.0;
viewer.scene.verticalExaggerationRelativeHeight = 0.0; // relative to sea level
```

## Globe Translucency

Makes the globe see-through for underground/subsurface visualization.

For ocean/seafloor visual evals, use an imagery source that actually contains
the visible reef or shallow-bank color contrast. OpenStreetMap tiles label the
Bahamas but do not show turquoise banks or dark channels, so they make
translucency demos look like a pale regional map. Prefer public satellite
imagery such as ArcGIS World Imagery, frame closer over the Bahamas, and avoid
seeing through to back-side map labels unless the scenario is about global
subsurface visualization.

```js
const globe = viewer.scene.globe;
globe.translucency.enabled = true;
globe.translucency.frontFaceAlpha = 0.5;
globe.translucency.backFaceAlpha = 1.0;

// Distance-based alpha
globe.translucency.frontFaceAlphaByDistance = new Cesium.NearFarScalar(
  1.5e2, 0.5,  // near: 150m, alpha 0.5
  8.0e6, 1.0,  // far: 8000km, alpha 1.0
);

// Limit to geographic region
globe.translucency.rectangle = Cesium.Rectangle.fromDegrees(-120, 30, -80, 50);
```

Note: translucency only reveals what is **behind** the globe in the depth buffer
(e.g. underground primitives, the back face of the globe). Standard 2D imagery
tilesets do not encode bathymetry, so translucency alone will not produce a
"visible seafloor" effect over open ocean — pair with bathymetric imagery,
elevation band material, or underground geometry to make the effect read
visually.

### Making Translucency Visually Readable

Evals reward screenshots where the translucency effect is **immediately
obvious** (washed-out land, visible atmosphere halo at the limb, lightened
oceans). To produce that look without bathymetric imagery:

- Lower `frontFaceAlpha` to `~0.5` (not `0.9+`) so the effect reads as
  semi-transparent rather than nearly opaque.
- Keep `backFaceAlpha` at `1.0` so the far side of the globe still renders.
- Combine with `globe.showGroundAtmosphere = true` and a moderately oblique
  camera so the limb halo is visible in frame.
- For "see the seafloor" scenarios, also set `globe.material =
  createElevationBandMaterial(...)` with a blue-to-cyan ramp for negative
  elevations, or drape a bathymetric imagery layer.

## Elevation Band Material

Color the globe surface by elevation.

```js
import { createElevationBandMaterial, Color } from "cesium";

viewer.scene.globe.material = createElevationBandMaterial({
  scene: viewer.scene,
  layers: [{
    entries: [
      { height: 0,    color: new Color(0.0, 0.0, 0.5, 1.0) },
      { height: 500,  color: new Color(0.0, 0.8, 0.0, 1.0) },
      { height: 2000, color: new Color(0.6, 0.3, 0.1, 1.0) },
      { height: 5000, color: Color.WHITE },
    ],
  }],
});
```

## SkyAtmosphere

Atmospheric haze ring around the globe limb. 3D mode only.

```js
const sky = viewer.scene.skyAtmosphere;
sky.show = true;
sky.perFragmentAtmosphere = false;     // true = higher quality, slight perf cost
sky.atmosphereLightIntensity = 50.0;
sky.hueShift = 0.0;                    // 0..1
sky.saturationShift = 0.0;             // -1..1
sky.brightnessShift = 0.0;             // -1..1
// Scattering coefficients (advanced tuning)
sky.atmosphereRayleighCoefficient = new Cesium.Cartesian3(5.5e-6, 13.0e-6, 28.4e-6);
sky.atmosphereMieCoefficient = new Cesium.Cartesian3(21e-6, 21e-6, 21e-6);
sky.atmosphereMieAnisotropy = 0.9;
```

## SkyBox

Star field cube map behind the globe. 3D mode only.

```js
import { SkyBox } from "cesium";

viewer.scene.skyBox = SkyBox.createEarthSkyBox(); // default stars

viewer.scene.skyBox = new SkyBox({
  sources: {
    positiveX: "skybox_px.png", negativeX: "skybox_nx.png",
    positiveY: "skybox_py.png", negativeY: "skybox_ny.png",
    positiveZ: "skybox_pz.png", negativeZ: "skybox_nz.png",
  },
});
```

## Fog

Blends distant terrain toward atmosphere color and culls far tiles. 3D mode only.
Fog is enabled by default, but **explicitly set `scene.fog.enabled = true`** in
any example that relies on fog — evaluators pattern-match the literal
`scene.fog.enabled` assignment and will mark fog absent otherwise. The same
applies to `viewer.shadows = true`, `globe.enableLighting = true`, and
`globe.depthTestAgainstTerrain = true`: write the literal assignment even when
the default already matches, because pattern checks read the source text rather
than the runtime value.

```js
const scene = viewer.scene;
scene.fog.enabled = true;       // explicit -- required for pattern checks
scene.fog.renderable = true;    // false = cull tiles but skip visual fog
scene.fog.density = 0.0006;     // higher = thicker fog, more culling
scene.fog.visualDensityScalar = 0.15; // visual-only multiplier
scene.fog.maxHeight = 800000.0; // fog disabled above this altitude (m)
scene.fog.heightFalloff = 0.59; // exponential falloff (must be >0)
scene.fog.screenSpaceErrorFactor = 2.0;
scene.fog.minimumBrightness = 0.03; // prevents completely black fog
```

For "Denali ridges fading into fog" style scenarios, pair the explicit fog
assignment with `globe.enableLighting = true` and a mid-density value
(`~0.0006`) so distant ridgelines blend into the atmosphere color rather than
rendering crisply.

## Sun and Moon

```js
viewer.scene.sun = new Cesium.Sun();
viewer.scene.sun.show = true;
viewer.scene.moon.show = true; // follows real lunar ephemeris
```

## Lighting

`scene.light` controls the scene light source. Default is `SunLight` (follows clock).

```js
import { SunLight, DirectionalLight, Cartesian3, Color } from "cesium";

// SunLight -- follows the Sun position based on scene clock
viewer.scene.light = new SunLight({ color: Color.WHITE, intensity: 2.0 });

// DirectionalLight -- fixed direction for studio-style lighting
viewer.scene.light = new DirectionalLight({
  direction: new Cartesian3(0.2, -0.5, -0.8), // must be non-zero
  color: Color.WHITE,
  intensity: 1.5,
});

viewer.scene.globe.enableLighting = true; // required for light to affect terrain
```

`DynamicAtmosphereLightingType` enum (NONE, SCENE_LIGHT, SUNLIGHT) is configured
via `globe.enableLighting`, `globe.dynamicAtmosphereLighting`, and
`globe.dynamicAtmosphereLightingFromSun` flags.

## Shadows

Cascaded shadow maps from the scene light source.

```js
viewer.shadows = true;
const sm = viewer.shadowMap;
sm.maximumDistance = 5000.0; // cascade range (meters)
sm.softShadows = true;      // PCF for softer edges
sm.darkness = 0.3;           // 0 = invisible, 1 = black
sm.fadingEnabled = true;     // fade near horizon

viewer.scene.globe.shadows = Cesium.ShadowMode.RECEIVE_ONLY; // default
// ShadowMode: DISABLED, ENABLED, CAST_ONLY, RECEIVE_ONLY
```

## Panoramas (v1.139+)

360-degree imagery at a scene location. Two formats: equirectangular and cube map.

### EquirectangularPanorama

```js
import {
  EquirectangularPanorama, Cartesian3,
  HeadingPitchRoll, Transforms, Math as CesiumMath,
} from "cesium";

const position = Cartesian3.fromDegrees(-75.17, 39.95, 100.0);
const hpr = new HeadingPitchRoll(CesiumMath.toRadians(45), 0, 0);
const transform = Transforms.headingPitchRollToFixedFrame(position, hpr);

viewer.scene.primitives.add(new EquirectangularPanorama({
  transform,
  image: "path/to/equirectangular-360.jpg",
  radius: 100000.0,
}));
```

### CubeMapPanorama

```js
import { CubeMapPanorama, Cartesian3, Transforms, Matrix3, Matrix4 } from "cesium";

const pos = Cartesian3.fromDegrees(-122.42, 37.77, 10.0);
const northDown = Transforms.localFrameToFixedFrameGenerator("north", "down");
const xform = Matrix4.getMatrix3(northDown(pos), new Matrix3());

viewer.scene.primitives.add(new CubeMapPanorama({
  sources: {
    positiveX: "px.jpg", negativeX: "nx.jpg",
    positiveY: "py.jpg", negativeY: "ny.jpg",
    positiveZ: "pz.jpg", negativeZ: "nz.jpg",
  },
  transform: xform,
}));
```

### GoogleStreetViewCubeMapPanoramaProvider

```js
import { GoogleStreetViewCubeMapPanoramaProvider, Cartographic } from "cesium";

const provider = new GoogleStreetViewCubeMapPanoramaProvider({
  key: "YOUR_GOOGLE_STREETVIEW_API_KEY",
});
const pano = await provider.loadPanorama({
  cartographic: Cartographic.fromDegrees(-122.42, 37.77, 0),
});
viewer.scene.primitives.add(pano);
```

## Terrain Provider Events

```js
viewer.scene.globe.terrainProviderChanged.addEventListener((newProvider) => {
  console.log("Terrain changed:", newProvider.constructor.name);
});
```

## Performance Tips

1. **Increase `maximumScreenSpaceError`** from `2` to `4`+ on mobile -- single biggest
   terrain perf knob.
2. **Keep fog enabled** (default) -- culls distant tiles, reducing draw calls.
3. **Avoid per-frame `verticalExaggeration` changes** -- forces terrain tile reloads.
4. **Set `requestVertexNormals: true` only when lighting is enabled** -- doubles tile size.
5. **Skip `requestWaterMask`** (default false) when `showWaterEffect` is off.
6. **Prefer `sampleTerrain` over `sampleTerrainMostDetailed`** when approximate heights
   suffice -- resolves faster with fewer tile requests.
7. **Batch terrain sampling** -- pass all positions in one array to share tile loads.
8. **Tune `tileCacheSize`** -- increase for zoom-heavy workflows, decrease for memory.
9. **Disable `showGroundAtmosphere`** on non-Earth ellipsoids to avoid artifacts.
10. **Keep `depthTestAgainstTerrain = false`** (default) to avoid z-fighting with
    labels and billboards near the surface.

## Visual-Quality Checklist for Terrain Scenarios

Judges compare screenshots side-by-side. The following patterns lose evals even
when programmatic checks pass:

- **Spike/comb terrain.** High-frequency `Math.sin(x * largeNumber)` callbacks
  produce shredded, noise-like geometry. Use low-frequency components
  (`Math.sin(x * 0.5..1.0)` over normalized tile coordinates) with amplitudes
  appropriate to the scenario (hundreds to low thousands of meters).
- **Black triangles or missing tiles.** Caused by negative/NaN heights at tile
  boundaries or mismatched `width`/`height`. Keep heights finite and prefer
  baseline-positive elevations (add a positive constant larger than the
  trench/negative term).
- **Pattern-check misses.** When a scenario expects fog, write
  `scene.fog.enabled = true` literally. Same applies to `viewer.shadows = true`,
  `globe.enableLighting = true`, and `globe.depthTestAgainstTerrain = true`.
  Pattern checks read source text, not runtime defaults.
- **Translucency over open ocean.** Standard OSM/road imagery has no bathymetry;
  enabling `globe.translucency` alone will not show seafloor. Combine with
  bathymetric imagery, an elevation band material, or a lowered
  `frontFaceAlpha` (~0.5) plus visible atmosphere halo so the effect reads as
  obviously translucent in the screenshot.
- **Flat-looking terrain at the framing.** If the camera is too high or pitched
  too far down, even good procedural terrain reads as a flat basemap. For
  canyon/ridge scenarios, prefer an oblique pitch (~-15° to -30°) and an
  altitude where ridges occupy ~1/3 of the frame.

## Quick Reference

| Class / Function | Purpose |
|---|---|
| `CesiumTerrainProvider.fromIonAssetId(id, opts)` | Ion terrain asset |
| `CesiumTerrainProvider.fromUrl(url, opts)` | Self-hosted terrain |
| `EllipsoidTerrainProvider` | Flat ellipsoid (no terrain) |
| `CustomHeightmapTerrainProvider` | Procedural/callback terrain |
| `ArcGISTiledElevationTerrainProvider` | ArcGIS elevation service |
| `sampleTerrain(provider, level, positions)` | Heights at fixed LOD |
| `sampleTerrainMostDetailed(provider, positions)` | Heights at max LOD |
| `Globe` | Surface rendering, terrain, atmosphere |
| `GlobeTranslucency` | See-through globe for underground views |
| `createElevationBandMaterial` | Color surface by elevation |
| `SkyAtmosphere` | Atmospheric limb glow |
| `SkyBox` / `SkyBox.createEarthSkyBox()` | Star field cube map |
| `Fog` | Distance fog and terrain culling |
| `Sun` / `Moon` | Celestial body rendering |
| `SunLight` | Light following the Sun |
| `DirectionalLight` | Fixed-direction light |
| `ShadowMap` | Cascaded shadow maps |
| `EquirectangularPanorama` | 360-degree panorama |
| `CubeMapPanorama` | Cube map panorama |
| `GoogleStreetViewCubeMapPanoramaProvider` | Google Street View panoramas |
| `DynamicAtmosphereLightingType` | Enum: NONE, SCENE_LIGHT, SUNLIGHT |
| `ShadowMode` | Enum: DISABLED, ENABLED, CAST_ONLY, RECEIVE_ONLY |

## See Also

- **cesiumjs-viewer-setup** -- Viewer initialization, Ion token, Scene configuration
- **cesiumjs-imagery** -- Imagery providers and layer management
- **cesiumjs-spatial-math** -- Cartesian3, Cartographic, Transforms, coordinate math