@gyeonghokim/copc-tileset
v1.0.3
Published
Stream COPC (Cloud Optimized Point Cloud) files directly into CesiumJS — no pre-tiling, no backend.
Maintainers
Readme
copc-tileset
Stream COPC (Cloud Optimized Point Cloud) files directly into CesiumJS — no pre-tiling, no conversion, no backend. Point a URL at a .copc.laz file on any static HTTP host and it streams into the globe.
How it works
A COPC file is already an octree with level-of-detail built in. copc-tileset reads it over HTTP range requests (via copc.js) and turns each octree node into a 3D Tiles tile on the fly, inside a Service Worker. Those tiles feed a Cesium3DTileset, so Cesium's engine drives view-dependent LOD, frustum culling, request scheduling and GPU memory — while attenuation, Eye Dome Lighting, custom shaders and picking come for free from Cesium's native point-cloud pipeline.
Only the nodes the camera actually needs are fetched and decoded, one at a time — the streaming is genuinely incremental, never a whole-file download.
Installation
npm install @gyeonghokim/copc-tileset # or: yarn add / pnpm addcesium is a peer dependency.
Quick Start
import { Viewer } from "cesium";
import {
CopcProvider,
CopcPointCloudPrimitive,
registerCopcServiceWorker,
} from "@gyeonghokim/copc-tileset";
// 1. Register the Service Worker that serves tiles. Do this once, at startup.
// Serve the bundled worker from your app (see "Service Worker setup" below).
await registerCopcServiceWorker("/copc-sw.js");
const viewer = new Viewer("cesiumContainer");
// 2. Open a COPC file (reads header, VLRs, octree info over range requests).
const provider = await CopcProvider.fromUrl(
"https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz",
);
// 3. Create the point cloud and add it to the scene.
const pointCloud = await CopcPointCloudPrimitive.fromProvider(provider, {
pointCloudShading: { attenuation: true, eyeDomeLighting: true },
});
viewer.scene.primitives.add(pointCloud);
// `Cesium3DTileset`-backed, so frame it via its bounding sphere.
viewer.camera.flyToBoundingSphere(pointCloud.boundingSphere);Requirements
Your .copc.laz file just needs to be served over HTTP(S) with:
- Range request support (most static hosts and object storage, e.g. S3, support this by default)
- CORS enabled, if served from a different origin than your app
Your app must be built with a bundler that supports ?url asset imports — Vite, webpack 5, Parcel, or Rspack. Point decoding runs laz-perf (WebAssembly) inside the Service Worker, and the library resolves its .wasm via a ?url import so your bundler emits it and serves it at the right path. With a bundler that ignores ?url, the .wasm is not emitted and every tile fails to decode.
API
| | |
|---|---|
| CopcProvider.fromUrl(url, options?) | Reads the COPC header, VLRs and octree hierarchy via copc.js. options.headers are sent with each range request (e.g. for auth). Returns Promise<CopcProvider>. |
| CopcPointCloudPrimitive.fromProvider(provider, options?) | Builds a Cesium3DTileset-backed point cloud fed by the Service Worker. Add it to viewer.scene.primitives; exposes boundingSphere and the underlying .tileset. Returns Promise<CopcPointCloudPrimitive>. |
| registerCopcServiceWorker(scriptUrl, options?) | Registers the tile-serving Service Worker. Call once before creating a primitive. |
CopcPointCloudPrimitive options (all also settable at runtime as properties):
pointSize— fixed point size in pixelsmaximumScreenSpaceError— screen-space error that drives octree LOD refinement (default16)dynamicScreenSpaceError— reduce detail for far tiles in dense scenes (defaulttrue)cacheBytes— GPU memory budget for loaded tilespointCloudShading— attenuation and Eye Dome Lighting, mirroring Cesium's nativePointCloudShading(attenuation,maximumAttenuation,eyeDomeLighting,eyeDomeLightingStrength, …). Use this — not a custom shader — for distance-based point sizing and EDL.customShader— a CesiumCustomShaderfor attribute-driven colouring / filtering (classification, intensity, …)
Examples
Classification-based colouring (custom shader):
import { CustomShader } from "cesium";
pointCloud.customShader = new CustomShader({
fragmentShaderText: `
void fragmentMain(FragmentInput fsInput, inout czm_modelMaterial material) {
float c = fsInput.metadata.Classification;
if (c == 2.0) material.diffuse = vec3(0.55, 0.4, 0.25); // ground
else if (c == 5.0) material.diffuse = vec3(0.1, 0.6, 0.1); // high vegetation
}`,
});Point picking — each point is a Cesium3DTileFeature with its per-point attributes:
import { Cesium3DTileFeature, ScreenSpaceEventHandler, ScreenSpaceEventType } from "cesium";
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
handler.setInputAction((movement) => {
const feature = viewer.scene.pick(movement.position);
if (feature instanceof Cesium3DTileFeature) {
console.log("Classification:", feature.getProperty("Classification"));
console.log("Intensity:", feature.getProperty("Intensity"));
console.log("GpsTime:", feature.getProperty("GpsTime"));
}
}, ScreenSpaceEventType.LEFT_CLICK);Alongside other 3D Tiles — it's a regular Cesium3DTileset, so it composes with buildings, terrain and photogrammetry with correct depth ordering; just add both to scene.primitives.
A full interactive demo (dataset switcher, EDL/attenuation toggles, point size, picking) lives in examples/ — run it with npm run dev.
Service Worker setup
Tiles are generated on the fly by a Service Worker so there is no backend. Your app must serve the bundled worker (src/sw/copc-sw.ts) from its own origin and register it with registerCopcServiceWorker(url). Virtual tile URLs are relative to your app base, so the worker's default scope covers them — no special scope or Service-Worker-Allowed header is needed, even on sub-path hosts like GitHub Pages. See examples/vite.config.ts for a Vite setup that emits copc-sw.js.
registerCopcServiceWorker resolves only once the worker controls the page (otherwise the first tileset.json request would fall through to your static host). On a visitor's first load the freshly activated worker takes control via clients.claim(); in the rare case it activates without claiming, the helper performs a single guarded page reload so the next load is controlled.
Sample Data
All three are served from a public S3 bucket with HTTP Range and CORS enabled, so they work in the browser out of the box.
| Dataset | Size | URL |
|---|---|---|
| Autzen Stadium | 77 MB | https://s3.amazonaws.com/hobu-lidar/autzen-classified.copc.laz |
| Millsite Reservoir | 1.35 GB | https://s3.amazonaws.com/hobu-lidar/millsite.copc.laz |
| SoFi Stadium | 1.9 GB | https://s3.amazonaws.com/hobu-lidar/sofi.copc.laz |
Built On
copc.js · CesiumJS · 3D Tiles · COPC Specification
Inspired by TIFFImageryProvider, developed for the 2026 Open Source Developer Contest (Gaia3D designated task).
