Skip to content

Options and Tuning

Jangmyun edited this page Aug 19, 2026 · 3 revisions

Options & Tuning

Every field on the third argument to CopcDataSource.load() is optional. This page groups them by what you're trying to trade off — visual quality, memory, bandwidth/latency, and coordinates — and gives concrete guidance for each.

const dataSource = await CopcDataSource.load(url, viewer, {
  sseThreshold: 250,
  maxVisibleNodes: 100,
  maxCacheNodes: 150,
  concurrency: 5,
  pixelSize: 2,
  // ...
});

Full reference

Option Default Trades off
proj 'EPSG:4326' Source CRS id. Auto-detected from WKT when omitted. See Coordinate Systems.
projDef null proj4 definition string for proj when proj4 doesn't already know it.
geoidOffset 0 Meters added to every height (local geoid–ellipsoid separation).
zFactor auto Z-unit → meters. Detected from the WKT vertical unit.
xyFactor auto XY-unit → meters (bounding-sphere sizing).
concurrency 5 Parallel Worker threads. Also the default for maxConcurrentRequests. Bandwidth/CPU vs. responsiveness.
maxConcurrentRequests = concurrency HTTP Range Requests in flight at once, set independently of concurrency.
debounceMs 100 Min interval between full LoD passes. Responsiveness vs. churn.
maxCacheNodes 150 LRU cap by node count. Memory vs. re-fetch on backtrack.
maxCacheBytes unset LRU cap by estimated bytes, on top of maxCacheNodes. Whichever limit is hit first evicts.
maxVisibleNodes 100 Nodes per LoD pass. Detail vs. memory/CPU.
maxPoints 5,000,000 Total points across selected nodes per LoD pass, on top of maxVisibleNodes. Detail vs. memory/CPU.
pixelSize 2 Point size in px. Live-adjustable.
sseThreshold 250 SSE (px) to subdivide. Detail vs. everything. Live-adjustable.
autoFrame true Whether load() flies the camera to the dataset.
colorMode 'rgb' How points are coloured ('rgb' | 'intensity' | 'classification' | 'elevation'). Live-adjustable, no re-decode.
opacity 1 Alpha multiplier, 0-1. Below 1 switches to translucent (no per-point depth sort). Live-adjustable.
classificationFilter all codes LAS classification codes to draw; everything else dropped. Live-adjustable.
intensityRange auto Raw intensity values at the two ends of the 'intensity' ramp. Grows to [0, highest seen] unless pinned.
heightOffset (live property, not a load option) 0 Meters shifted along local "up" post-load, for correcting a residual geoid/vertical-datum mismatch without re-loading. Live-adjustable via dataSource.heightOffset.

Visual quality

sseThreshold — the master detail dial

Screen-space error is roughly how many pixels the gap between points would span at the current distance. A node is subdivided into its children when its SSE exceeds this threshold.

  • Lower (e.g. 100) → subdivides sooner → denser points, more nodes loaded, more memory and bandwidth.
  • Higher (e.g. 400) → coarser, cheaper, faster.
  • Default 250 is a balance for typical aerial/drone datasets.

It is live-adjustabledataSource.sseThreshold = 150 triggers an immediate re-selection with no reload. Wire it to a slider (the example viewer does).

pixelSize

Point size in pixels. Also live via dataSource.pixelSize. For sparse clouds a larger pixelSize (3–4) fills gaps and reads as more solid; for dense clouds keep it at 1–2 to avoid a mushy blob. This changes only a GPU uniform — no re-fetch.

Memory

maxCacheNodes — the LRU cap

Total nodes kept in memory (loaded or hidden) before the least-recently-used unselected node is torn down. Selected nodes are pinned and never evicted.

  • Raise it if users backtrack a lot (orbiting, zooming in and out): a larger cache avoids re-fetching+re-decoding nodes they just left.
  • Lower it on memory-constrained devices.
  • Rule of thumb: keep it comfortably above maxVisibleNodes (default 150 vs 100) so the current view plus some history fits.

maxCacheBytes — the byte-based LRU cap

maxCacheNodes alone is a poor proxy for memory since a node's point count varies widely across a hierarchy — two datasets with the same node cap can hold very different amounts of actual data. maxCacheBytes (unset by default) bounds the same LRU by estimated size instead, evicting on whichever of the two limits is hit first. Each node's size is estimated as pointCount * 21 bytes (the fixed per-point buffer layout: positions 12B + colors 4B + intensities 2B + classifications 1B + elevations 2B). Set it when you know a hard memory budget (e.g. a kiosk device); leave it unset otherwise, since a sensible value depends on the dataset's typical points-per-node.

maxVisibleNodes — the point budget

Hard cap on how many nodes a single LoD pass will select. Because selection is a max-heap by SSE, hitting the cap keeps the highest-error (most visually important) nodes and drops the rest. Raise it for more on-screen detail at the cost of memory and per-frame CPU; lower it to guarantee a budget on weak hardware.

maxPoints — the point-count budget

maxVisibleNodes caps node count, but nodes don't hold a uniform number of points across a hierarchy, so the same node budget can mean very different rendering cost (draw calls, GPU memory, points on screen) on different datasets. maxPoints (default 5,000,000) bounds the render set directly by total point count across selected nodes, whichever of the two limits is hit first.

Styling

colorMode, opacity, classificationFilter, and intensityRange are covered in depth in the README's Styling section. All four are live-adjustable setters on CopcDataSource — a style change is a GPU uniform update, never a re-fetch or re-decode, so tune these freely without worrying about network/CPU cost.

Bandwidth & latency

concurrency — parallel decode

Number of Worker threads decoding in parallel (default 5). Also the default width for maxConcurrentRequests when that option is left unset.

  • Higher → more parallel CPU-bound LAZ decode work, but more memory (each Worker carries its own laz-perf WASM instance).
  • Lower → gentler on the CPU.
  • Ignored when you pass your own workerPool (the pool's size wins).

maxConcurrentRequests — parallel fetch, decoupled from decode

How many HTTP Range Requests may be in flight at once, independent of concurrency. Decoding is CPU-bound and saturates at a handful of workers; fetching is latency-bound, so a high-RTT link can usefully keep far more requests in flight than there are decode workers. Raising concurrency to buy fetch parallelism used to also spawn that many Workers — memory spent on a stage that was never the bottleneck. Set maxConcurrentRequests higher than concurrency on high-latency links to widen fetching without widening decode.

debounceMs — LoD pass throttle

Minimum interval between full LoD re-selection passes while the camera is moving (a lighter frustum-only visibility check still runs every frame, and a full pass always fires on moveEnd).

  • Higher (e.g. 200) → less selection churn and fewer speculative loads during fast fly-throughs.
  • Lower (e.g. 50) → snappier refinement mid-motion, more work.

Coordinates

proj, projDef, zFactor, xyFactor, and geoidOffset are covered in depth in Coordinate Systems. Short version:

  • Leave them unset and let WKT auto-detection do its job.
  • Override proj/projDef only when the file's WKT is missing or unrecognized.
  • Auto-detected zFactor/xyFactor still apply on top of an explicit proj/projDef, because a compound CRS's vertical unit can differ from the CRS you override with.
  • Use geoidOffset if heights sit consistently above/below the terrain (orthometric vs. ellipsoidal datum mismatch).
  • heightOffset is the live equivalent for correcting a mismatch discovered after load, without re-loading: dataSource.heightOffset = -20 shifts every already-loaded (and future) node along its local "up" by translating the model matrix, not the geometry, so it applies instantly with no re-decode.

Diagnostics

stats — measuring the streaming claim

dataSource.stats is a read-only getter (CopcStats) that reports what a session has actually spent, so "only the nodes the camera can see are fetched" is a number instead of an assertion:

Field What it reports
fileBytes Total COPC file size, read from a range response's Content-Range.
requestCount HTTP range responses received (merged sibling fetches count once).
transferredBytes Bytes actually received — the Range-support probe, the header, hierarchy pages, and node point data combined.
pendingNodes Nodes currently fetching, decoding, or uploading. 0 means the current view is fully resolved.
fetch / decode / upload Per-stage StageTimingcount (nodes completed, unbounded) plus p50/p95 latency in ms over a rolling window of the most recent 256 nodes.

upload.count normally trails decode.count: a node that finished decoding but was never drawn (camera moved on, or it lost a budget cut) never reaches the GPU, so it never reaches the upload stage. Comparing transferredBytes against fileBytes gives a rough sense of how much of a large dataset a given camera path actually touched.

Recommended presets

Weak device / mobile

{ sseThreshold: 350, maxVisibleNodes: 60, maxCacheNodes: 90, concurrency: 3 }

Quality workstation

{ sseThreshold: 150, maxVisibleNodes: 160, maxCacheNodes: 250, concurrency: 8 }

Bandwidth-constrained origin

{ concurrency: 2, debounceMs: 200, sseThreshold: 300 }