--- 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