Your CesiumJS scene. Multi-model point clouds. Streamed on CPU.

Two tags. Full-resolution datasets, as many as you want to load.

The Unlimited Detail engine renders point clouds on the CPU, leaving your GPU budget free for Cesium's terrain, 3D Tiles, imagery, and glTF. Protected under Unlimited Detail patents.

Cesium handles terrain, 3D Tiles, imagery, and glTF beautifully. udSDK slots in as another primitive alongside them: stream multiple original-resolution point cloud datasets, rendered on the CPU, with no preprocessing pipeline and no draw on the GPU memory you've budgeted for the rest of the scene.

Overview

A drop-in bridge. Not a rewrite.

udsdk-cesium.js adds a custom Primitive to your viewer's primitive collection. The primitive runs udSDK each frame, copies its colour + depth buffers into Cesium-managed GL textures, and submits a screen-space DrawCommand that writes depth through czm_writeLogDepth — so point cloud pixels share Cesium's log-depth buffer and interleave correctly with terrain, 3D Tiles, and other primitives. Not painted on top.

Quickstart

A complete, runnable page

Two script tags (plus the Cesium widgets CSS) and one function call. Opens on a LiDAR scan of Japan, with the camera auto-flown to the dataset's bounding sphere and correct occlusion against the OSM basemap. The live demo on the right runs exactly this code.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>udSDK + CesiumJS</title>
    <script src="https://cesium.com/downloads/cesiumjs/releases/1.141/Build/Cesium/Cesium.js"></script>
    <link href="https://cesium.com/downloads/cesiumjs/releases/1.141/Build/Cesium/Widgets/widgets.css" rel="stylesheet" />
    <script src="https://nuclideon.com/cdn/integrations/cesium/1.0.0/udsdk-cesium.js"></script>
    <style> html, body, #cesiumContainer { height: 100%; margin: 0; } </style>
  </head>
  <body>
    <div id="cesiumContainer"></div>
    <script>
      const viewer = new Cesium.Viewer("cesiumContainer", {
        scene3DOnly: true,
        baseLayer: new Cesium.ImageryLayer(
          new Cesium.OpenStreetMapImageryProvider({ url: "https://tile.openstreetmap.org/" })
        ),
        baseLayerPicker: false,
        geocoder: false,
      });

      udSDKCesium.attach(viewer, {
        models: ["https://nucl-demo.s3.amazonaws.com/Japan/JapanALS_Split/1_0_0.uds"],
      });
    </script>
  </body>
</html>

Pin to a versioned path. The CDN host prefix may change — the version segment will not.

What you get

Five things the wrapper does for you

The hard parts of the integration — handled by the wrapper, so they don't surface in your code.

Zero integration boilerplate

A script tag and one function call. No build tooling, no framework lock-in, no bundler config. Works in Sandcastle-style script-tag environments.

Depth-correct compositing

Point clouds interleave with Cesium content via `czm_writeLogDepth`. Terrain, 3D Tilesets, glTF models, and other primitives correctly occlude and are occluded — point clouds are not a flat overlay painted on top of the scene.

Domain license or per-user sign-in

Two paths to udCloud: a Nuclideon-issued domain license signs all visitors in silently on that domain (no popup, no account), or individual users sign in with their own udCloud accounts. `attach()` tries the silent domain path first and falls back to OAuth.

Self-hostable

The `serverAddress` option points the SDK at a private udCloud tenant if you run your own infrastructure.

Versioned CDN

Immutable versioned paths. Pin your version; upgrade on your schedule.

Requirements

Three things your host page must provide

Before you wire up `attach()`, the page serving your HTML needs to satisfy these. The cross-origin isolation requirement has real deployment implications — read it carefully.

Cross-origin isolation

udSDK uses SharedArrayBuffer for its worker pool, which requires the host document to be cross-origin isolated. Send these headers on the HTML document. credentialless is the easier choice — require-corp forces every cross-origin resource (Cesium CDN, basemap tiles, udCloud, your .uds hosts) to send its own CORP header, which is brittle across origins you don't control. These headers are per-document, not site-wide — scope them to the viewer path in your CDN or reverse proxy.

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: credentialless

CesiumJS UMD build on the page

Use the CDN UMD bundle, which exposes a global Cesium. Tested against 1.141; the wrapper depends on the WebGL2-only shader path Cesium added in 1.95. The ES module build (@cesium/engine via a bundler) isn't supported by the drop-in — a custom integration is needed for that case.

<script src="https://cesium.com/downloads/cesiumjs/releases/1.141/Build/Cesium/Cesium.js"></script>
<link href="https://cesium.com/downloads/cesiumjs/releases/1.141/Build/Cesium/Widgets/widgets.css" rel="stylesheet" />

A sign-in path for your users

Two options, pick one. Domain license: Nuclideon issues a license bound to your hosting domain. Any page served from that domain signs visitors in silently — no popup, no per-user account. Talk to us to arrange one. Per-user udCloud accounts: each end user signs in with their own account via the OAuth popup at udcloud.nuclideon.com. .uds point cloud files can be hosted anywhere the browser can fetch from with CORS — they don't need to live in udCloud.

API Reference

attach() options and controller

udSDKCesium.attach(viewer, options) returns a controller. Defaults are sensible; override only what you need.

Full options reference

All options are optional. Pass a bare `{}` to take defaults.

udSDKCesium.attach(viewer, {
  // Name shown on the udCloud OAuth consent screen.
  // Default: "cesium-<window.location.hostname>"
  clientName: "Acme Portal",

  // Models to auto-load after sign-in. Strings or {url, zOffset, zone, classification, flyTo} objects.
  // zone defaults to 4978 (WGS84 ECEF) — Cesium's native world frame.
  models: [
    "https://my-bucket.example.com/site.uds",
    { url: "https://.../offset.uds", zOffset: 150, zone: 4978 },
    { url: "https://.../classified.uds", classification: true },
  ],

  // "auto" shows a built-in sign-in button when needed; false = customer drives it
  signInUI: "auto",
  signInLabel: "Sign in to udCloud",

  // Point at a self-hosted or regional udCloud. Default: https://udcloud.nuclideon.com/
  serverAddress: "https://udcloud.acme.internal/",

  // "auto" tries silent domain-license sign-in first; false always shows the OAuth popup
  tryDomainLogin: "auto",

  // Make the primitive visible immediately. Set false to start hidden.
  autoEnable: true,

  // Fly the camera to the first loaded model's bounding sphere (from header data)
  autoFlyTo: true,

  // Override the SDK CDN base. Defaults to the udSDK release this
  // wrapper version was pinned to at build time
  // (https://nuclideon.com/cdn/udsdk/2.6.1/js/).
  baseUrl: null,

  // Callback for emscripten loader progress strings
  onStatus: (text) => console.log(text),
});

Controller API

`attach()` returns a controller exposing sign-in state, model management, a rendering toggle, and an EventTarget for lifecycle events.

const ud = udSDKCesium.attach(viewer, { /* ... */ });
await ud.ready;                        // bootstrap + initial sign-in flow complete

// Sign-in state
ud.signedIn;                           // boolean
await ud.signIn();                     // manual OAuth (must be from user gesture)

// Model management
await ud.loadModel("https://.../extra.uds");
await ud.loadModel({ url: "...", zOffset: 50, zone: 4978, classification: true });

// Rendering toggle
ud.enabled = false;                    // hide point clouds without unloading
ud.enabled = true;

// Underlying objects, if you want to reach in
ud.viewer;                             // the Cesium.Viewer
ud.primitive;                          // the Primitive added to viewer.scene.primitives

// Events (EventTarget)
ud.addEventListener("ready", () => {});
ud.addEventListener("signed-in", (e) => {
  // e.detail.method is "domain" (silent domain-license sign-in) or "oauth" (popup)
});
ud.addEventListener("sign-in-error", (e) => {});
ud.addEventListener("model-loaded", (e) => { /* e.detail.url, e.detail.handle, e.detail.slot */ });
ud.addEventListener("model-error", (e) => { /* e.detail.url, e.detail.code */ });

// Teardown
ud.destroy();
Under the hood

How the bridge actually works

A walk through the render pipeline the wrapper installs. Skip this section if you just want to ship; it's here for the integration leads who need to know what's running inside their scene before they sign off.

Custom Primitive in the scene's primitive collection

The wrapper builds a Cesium Primitive and adds it to viewer.scene.primitives. Each frame, the primitive's update() is called from inside Cesium's render loop. It runs udSDK, copies the colour + depth output into two Cesium-managed GL textures, and pushes a screen-space DrawCommand onto the frame's command list. Cesium then dispatches it alongside its own commands.

Cesium log depth via czm_writeLogDepth

The fragment shader unprojects each pixel's udSDK depth value back to world space, computes distance from the camera, and writes the result through Cesium's built-in czm_writeLogDepth. This shares Cesium's log-depth buffer, so the hardware depth test handles occlusion against terrain, 3D Tiles, glTF models, and other primitives automatically — no z-fighting, no ordering tricks.

Camera matrices each frame

View and projection matrices from Cesium's frame state (frameState.camera.viewMatrix and frameState.camera.frustum.projectionMatrix) are fed into udSDKJS_SetMatrix("view", ...) and udSDKJS_SetMatrix("projection", ...) every frame, so udSDK renders from the same viewpoint as the rest of the scene.

Emscripten worker bootstrap

udSDK is a threaded emscripten build. Since the wrapper loads cross-origin from the CDN, it fetches udSDKjs.js as text, wraps it in a Blob, and passes the Blob URL to Module.mainScriptUrlOrBlob so the pthread pool can spawn same-origin workers. Browsers refuse new Worker(crossOriginURL), so this detour is unavoidable.

RGBA8-packed depth buffer

udSDK delivers depth as raw IEEE-754 little-endian float32 bytes. The wrapper uploads them as an RGBA8 texture (4 bytes per pixel = 1 float per pixel) and decodes back to float in the fragment shader. This avoids depending on R32F renderable-texture support, which keeps the integration working across the wider set of devices Cesium itself supports.

OAuth flow preserves the user gesture

udSDKJS_ConnectStart returns an approve URL. The wrapper navigates an already-opened popup to it from inside the click handler — popups opened after an await get blocked. udSDKJS_ConnectComplete polls udCloud for completion.

Domain-first sign-in

Before showing any UI, the wrapper tries udSDKJS_CreateSharedFrom_udCloud(clientName, null). The null key argument tells udCloud to authenticate against a domain license bound to the serving origin. If the domain is licensed, no popup appears and every visitor is signed in. If it isn't, the built-in OAuth button renders and the user signs in with their own udCloud account.

Asyncify yield is swallowed locally

udSDK's wasm uses emscripten Asyncify and yields by throwing the literal string "unwind". If that escaped into Cesium's render loop, Cesium would catch it as a fatal renderError and stop rendering for good. The primitive's update() catches it locally and drops the frame instead — the last good colour/depth buffer stays bound on the textures, so it's visually unnoticeable.

Known limitations

Things to know before you ship

The rough edges in the current build. Workarounds are noted where they exist.

One instance per page

Emscripten's Module is a global. You can't run two independent udSDK bridges on one document.

serverAddress only honored on the OAuth path

Domain login ignores the serverAddress override in the current udSDK build — a fix is in progress. If you need a custom tenant today, set tryDomainLogin: false to force the OAuth path.

Cross-origin isolation is mandatory

A page that can't set the COOP/COEP headers — for example, an existing Cesium app hosted on a domain you don't control — can't use the drop-in as-is. You'll need a reverse proxy, an iframe into a page you do control, or a non-threaded udSDK build (on the roadmap).

ES module build of Cesium is not supported

The wrapper looks up window.Cesium. If you're consuming Cesium as ES modules via a bundler, either also load the UMD build for the wrapper to find, or fork the wrapper to pass Cesium in via an option. We're considering a published @nuclideon/udsdk-cesium npm package — talk to us if that would unblock you.

WebGL context loss isn't recovered

Colour and depth textures aren't recreated after a lost WebGL context. The page needs reloading. Uncommon in practice but worth flagging.

Ship point clouds in your CesiumJS viewer.

The script is live at the CDN. Wire it up in an afternoon. If you need a private tenant or a self-hosted udCloud, talk to us first.