npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@supertt/three-cesium-bridge

v0.1.0

Published

Synchronize Cesium and three.js behind a single floating-origin ECEF<->local transform and one shared camera.

Readme

@supertt/three-cesium-bridge

Synchronize Cesium and three.js behind one floating-origin coordinate frame and one shared camera.

Cesium keeps its geospatial authority (ECEF coordinates, double precision, 3D Tiles, terrain, imagery). three.js renders a transparent overlay that follows Cesium's camera, so you can drop Object3Ds onto the globe at real longitude/latitude coordinates without precision jitter.

Why not merge the two renderers?

Cesium works in Earth-centered coordinates (~6.4×10&sup6; m) and needs double precision on the CPU plus a high/low split on the GPU to avoid ~0.5 m float32 jitter. three.js is a local, float32 renderer. Forcing them onto one matrix loses one of those properties. This bridge keeps both engines intact and links them with a floating origin: a local tangent frame (X=East, Y=Up, Z=South) that is re-centered as the camera moves.

Install

npm install three cesium @cesium/engine @supertt/three-cesium-bridge

three, cesium, and @cesium/engine are peer dependencies. This package is ESM-only@cesium/engine is ESM-only, and bundling it would create duplicate Cartesian3/Quaternion instances, so a CJS build is intentionally not provided.

Run the demo

The repo ships a working demo in example/main.ts: a glTF house clamped onto real Cesium World Terrain at a geospatial position, a waving custom-shader flag, and terrain-clamped markers — all glued to the globe as you zoom and orbit.

npm install
npm run example   # vite dev server

Open the printed URL (http://localhost:5173 by default). It runs out of the box: Cesium ships a default Ion token that serves real satellite imagery (Cesium World Imagery) + real elevation (Cesium World Terrain) with no sign-up. For anything long-lived, get a free token at https://ion.cesium.com and set Cesium.Ion.defaultAccessToken = 'YOUR_TOKEN' before creating the viewer. The scene is centered on San Francisco; change the lon/lat in example/main.ts to drop it anywhere on Earth.

Quick start

import * as Cesium from 'cesium';
import * as THREE from 'three';
import { CesiumThreeBridge } from '@supertt/three-cesium-bridge';

const viewer = new Cesium.Viewer(container, { baseLayer: false });
viewer.camera.setView({
  destination: Cesium.Cartesian3.fromDegrees(-122.4, 37.8, 5000),
});

const renderer = new THREE.WebGLRenderer({ alpha: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera();

const bridge = new CesiumThreeBridge({ viewer, renderer, scene, camera });

const box = new THREE.Mesh(
  new THREE.BoxGeometry(50, 50, 50),
  new THREE.MeshStandardMaterial({ color: 0xff3333 }),
);
bridge.addObject(box, Cesium.Cartesian3.fromDegrees(-122.4, 37.8, 100));

bridge.start(); // or call bridge.render() in your own loop

Loading a glTF model at a position

Cesium owns the coordinates; three.js owns the model and its shaders. Load a model with three's GLTFLoader and clamp it onto real terrain at a longitude/latitude with bridge.placeOnTerrain():

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';

const gltf = await new GLTFLoader().loadAsync('/models/house.glb');
bridge.placeOnTerrain(
  gltf.scene,                                     // three.js Object3D
  Cesium.Cartographic.fromDegrees(-122.4, 37.8),  // lon, lat (radians-free)
  {
    heightOffset: 13,     // meters above the terrain surface
    heading: 35,          // compass heading in degrees (0° = north)
    alignToTerrain: true, // lean with the slope (default)
  },
);

That's the whole matrix-sync story. Every frame the bridge re-projects the model's ECEF position into the floating-origin local frame and renders it with Cesium's camera (same position, same frustum), so the model is glued to the globe — it moves, scales and parallaxes with the terrain as you orbit and zoom, instead of being a screen-fixed sprite. There is nothing to update yourself; just don't mutate gltf.scene.position (the bridge owns it).

How a "world-coordinate" model fits the terrain

A three.js model has no idea about the Earth. placeOnTerrain bridges that gap in two steps:

  1. Height — it samples the terrain elevation at that longitude/latitude (sampleHeightMostDetailed, falling back to globe.getHeight) and places the model's origin at height + heightOffset above the ellipsoid.
  2. Slope — with alignToTerrain: true (default) it also samples four neighbors to estimate the terrain surface normal, then tilts the model so its up-axis follows that normal. On flat ground the model stands straight up; on a hillside it leans with the slope instead of floating or burying itself.

The height and normal re-sample automatically as finer terrain tiles load, so the model "settles" onto the surface.

Adjusting position and orientation

An anchor is a live object — you can move and rotate it after placement:

const anchor = bridge.placeOnTerrain(gltf.scene, Cesium.Cartographic.fromDegrees(lon, lat));

// Move to a new coordinate (re-samples terrain height + slope).
bridge.updateAnchorPosition(gltf.scene, Cesium.Math.toRadians(-122.42), Cesium.Math.toRadians(37.81));

// Face a new compass heading (no re-sample needed).
bridge.updateAnchorHeading(gltf.scene, 90);

// Float higher above the ground.
bridge.updateAnchorHeightOffset(gltf.scene, 20);

For a fixed-height object (no terrain clamping), use addObject(object, ecef, orientation?) with an explicit ECEF position and an optional Cesium.Quaternion orientation.

API

CesiumThreeBridge

  • addObject(object, ecef, orientation?) — anchor an Object3D at an ECEF position (Cesium.Cartesian3.fromDegrees(lon, lat, height)). Returns the placed record. Optional orientation is a Cesium.Quaternion in ECEF.
  • placeOnTerrain(object, cartographic, options?) — clamp an Object3D to the terrain surface. cartographic is a Cesium.Cartographic (lon/lat in radians). Options: heightOffset (meters above the surface, default 0), heading (compass heading in degrees, default 0), alignToTerrain (tilt the up-axis to follow the terrain surface normal, default true), and orientation (raw ECEF Cesium.Quaternion override). The object's height and slope follow the terrain as finer tiles load. Falls back to globe.getHeight when sampleHeight is unsupported.
  • updateAnchorPosition(object, longitude, latitude) — move an existing terrain anchor to new coordinates (radians) and re-sample its height/slope.
  • updateAnchorHeading(object, headingDeg) — rotate an existing terrain anchor to a new compass heading.
  • updateAnchorHeightOffset(object, heightOffset) — change an anchor's height offset above the surface.
  • refreshTerrainAnchors() — force an immediate re-sample of every terrain anchor.
  • refreshOcclusion() — force an immediate re-evaluation of terrain occlusion.
  • removeObject(object) — detach and stop tracking (works for both fixed and terrain-anchored objects).
  • setOrigin(ecef) / rebase() — re-center the local frame (terrain anchors are re-projected to their sampled height automatically).
  • update() — sync camera + reposition objects (no draw).
  • render()update() + draw the three.js scene.
  • start() / stop() / dispose() — manage the auto-render loop mounted on Cesium's postRender event and the transparent overlay canvas.

EcefLocalFrame

  • toLocalPosition(ecef) / toEcefPosition(local) — position transforms.
  • rotateToLocal(ecefDir) — direction transform (no translation).
  • rotationToLocal(ecefQuaternion) — orientation transform.

terrainAnchorEcef(placement)

Pure helper: computes the ECEF position of a terrain anchor from its sampled height and offset. Useful for testing or computing anchor positions yourself.

Terrain geometry helpers

Pure, exported for advanced use:

  • geodeticUp(ecef) — the ellipsoid surface normal (unit, ECEF) at a position.
  • axisAngleQuaternion(axis, angle) — rotation about a unit axis.
  • quaternionBetween(from, to) — shortest-arc rotation carrying from onto to.
  • normalFromHeights(...) / terrainSurfaceNormal(...) — estimate the terrain surface normal from neighboring heights; these back alignToTerrain.

isRayOccludedByTerrain(camera, target, getHeight, sampleCount?)

Pure helper: line-of-sight test — returns true if terrain rises above the ray from camera to target (both ECEF). getHeight(cartographic) returns the terrain height (or undefined for not-yet-loaded tiles, which are skipped).

syncCamera

Copies a Cesium camera's pose + perspective frustum into a three.js camera.

Coordinate frame

| Local axis | Geospatial direction | | ---------- | -------------------- | | +X | East | | +Y | Up (geodetic normal) | | +Z | South |

Right-handed, Y-up, matching three.js conventions.

Terrain

placeOnTerrain lets you build scenes that sit on Cesium's terrain. Enable it on the viewer as usual (viewer.scene.setTerrain(...)), then anchor three.js objects to the surface instead of a fixed height:

const marker = new THREE.Mesh(
  new THREE.CylinderGeometry(6, 6, 30, 16),
  new THREE.MeshStandardMaterial({ color: 0x22cc66 }),
);
bridge.placeOnTerrain(marker, Cesium.Cartographic.fromDegrees(-122.4, 37.8005), {
  heightOffset: 15,  // float 15 m above the ground
  heading: 0,        // compass heading
  alignToTerrain: true, // lean with the slope (default)
});

Height anchoring is two-stage: an immediate best-effort placement from globe.getHeight, then an accurate async refinement from sampleHeightMostDetailed. Slope alignment (alignToTerrain) samples four neighbors to estimate the surface normal and tilts the object to match. Anchors re-sample automatically when Cesium finishes loading a terrain batch (tileLoadProgressEvent reaching 0), and their sampled height is remembered across floating-origin rebases. If sampleHeightSupported is false (e.g. certain globe providers), it silently stays on globe.getHeight.

Depth occlusion

Enabled by default, the bridge hides any placed or terrain-anchored object that terrain blocks from view. Each frame (throttled to intervalMs) it casts a ray from the camera to each object's ECEF position and samples sampleCount points along it; if globe.getHeight at any sample rises above the line of sight, the object is hidden (object.visible = false).

const bridge = new CesiumThreeBridge({
  viewer, renderer, scene, camera,
  occlusion: { sampleCount: 32, intervalMs: 50 }, // tune accuracy vs. cost
});

Pass occlusion: false to disable, or call isRayOccludedByTerrain yourself to test visibility. Occlusion also accounts for the earth's curvature — an object over the horizon is hidden. It is a per-object approximation: it occludes against terrain but not against 3D Tiles (e.g. buildings); see Limitations.

Limitations (v1)

  • Overlay compositing: three.js renders as a transparent canvas stacked on top of Cesium; there is no shared depth buffer. Per-object terrain occlusion is implemented (see "Depth occlusion"), but a three.js object is still not occluded by 3D Tiles (e.g. buildings) or correctly depth-ordered against translucent Cesium surfaces. Full depth compositing (rendering three.js through Cesium's view/projection + depth) is the planned next milestone.
  • Perspective cameras only for the automatic frustum sync.

Development

npm install
npm run typecheck   # tsc --noEmit
npm test            # vitest unit tests (math round-trip, camera sync, rebase invariance)
npm run build       # tsup -> dist (esm + dts)
npm run example     # vite dev server for the demo

Headless end-to-end check (optional, needs npx playwright install chromium once):

npm run build && npx vite build && node smoke.mjs

Publishing

Publishing is automated: prepublishOnly runs typecheck + test + build before the package is uploaded, so a publish can't ship broken or stale code.

npm login          # one-time; authorizes this machine (and the @supertt scope)
npm publish        # scoped packages default to private, but publishConfig sets --access public

The package is published as @supertt/three-cesium-bridge under your own npm username scope — no organization is required.

License

This package is licensed under the MIT License.

Peer dependencies (installed separately by you, not bundled into this package):

| Package | License | Notes | | ------- | ------- | ----- | | three | MIT | | | cesium | Apache-2.0 | ships a default Ion access token (demo imagery/terrain) | | @cesium/engine | Apache-2.0 | math + scene engine used for ECEF transforms |

Because three, cesium, and @cesium/engine are peerDependencies (not bundled), installing @supertt/three-cesium-bridge does not redistribute their code — you install those three yourself. If you then bundle them into an application you distribute, the Apache-2.0 terms for Cesium/@cesium/engine require you to keep their copyright notices and include a copy of the Apache-2.0 license; three.js's MIT terms require keeping its copyright notice. The most recent copies of each license live in the respective upstream repositories linked above.