Options and Tuning - Jangmyun/copcesium GitHub Wiki

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, styling, and coordinates β€” and gives concrete guidance for each.

const dataSource = await CopcDataSource.load(url, viewer, {
  sseThreshold: 250,
  maxVisibleNodes: 100,
  maxPoints: 5_000_000,
  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 at decode time (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 Worker threads decoding in parallel. CPU and memory vs. decode throughput. Ignored when you pass your own workerPool.
maxConcurrentRequests = concurrency HTTP Range Requests in flight at once. Fetch parallelism vs. load on the origin.
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 none LRU cap by estimated bytes (pointCount Γ— 21), on top of maxCacheNodes.
maxVisibleNodes 100 Nodes per LoD pass. Detail vs. memory/CPU.
maxPoints 5,000,000 Total points per LoD pass, on top of maxVisibleNodes. Detail vs. GPU cost.
prefetchFrustumFactor 1.8 How far beyond the view to prefetch. ≀ 1 disables. Smooth panning vs. bandwidth.
maxPrefetchNodes 50 Prefetch-only nodes per LoD pass.
sseThreshold 250 SSE (px) to subdivide. Detail vs. everything. Live-adjustable.
pixelSize 2 Point size in px ('fixed' mode). Live-adjustable.
pointSizeMode 'fixed' 'fixed' or distance-'attenuated' point size. Live-adjustable.
attenuationFactor 100 Numerator of attenuationFactor / sqrt(dist_m). Live-adjustable.
minPixelSize 1 Lower clamp for attenuated size. Live-adjustable.
maxPixelSize 6 Upper clamp for attenuated size. Live-adjustable.
colorMode 'rgb' 'rgb' / 'intensity' / 'classification' / 'elevation'. Live-adjustable.
opacity 1 Alpha multiplier, 0–1. Live-adjustable.
classificationFilter all codes LAS classification codes to draw. Live-adjustable.
intensityRange auto Raw intensity at the two ends of the 'intensity' ramp. Live-adjustable.
autoFrame true Whether load() flies the camera to the dataset.

heightOffset is not a load() option β€” it's a live property on the data source (default 0). See Coordinates.

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-adjustable β€” dataSource.sseThreshold = 150 triggers an immediate re-selection with no reload. Wire it to a slider (the example viewers do).

Point size: pixelSize and pointSizeMode

In the default 'fixed' mode every point is drawn at 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.

pointSizeMode: 'attenuated' makes size depend on distance instead:

gl_PointSize = clamp(attenuationFactor / sqrt(distance_m), minPixelSize, maxPixelSize)

Close-up points grow to cover the gaps between them, while distant (already dense) nodes don't overdraw. Tune attenuationFactor per dataset density β€” raise it if close-ups still show gaps, lower it if near points look bloated β€” and use minPixelSize/maxPixelSize to bound the result.

All five are live properties on the data source and only change GPU uniforms β€” no re-fetch. attenuationFactor must be a positive finite number, and minPixelSize ≀ maxPixelSize must hold; violations throw RangeError.

Memory

maxCacheNodes and maxCacheBytes β€” the LRU caps

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

  • Raise maxCacheNodes if users backtrack a lot (orbiting, zooming in and out): a larger cache avoids re-fetching and 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.

Node count is a poor proxy for memory when point counts per node vary a lot. maxCacheBytes adds a byte budget, estimated as pointCount Γ— 21 bytes per node (the fixed per-point buffer layout). Eviction happens when either limit is exceeded. It's unset by default because a sensible value depends on the dataset's typical points-per-node.

maxVisibleNodes and maxPoints β€” the render budget

Hard caps on what a single LoD pass will select; the pass stops at whichever is hit first. Because selection is a max-heap by SSE, hitting a cap keeps the highest-error (most visually important) nodes and drops the rest.

  • maxVisibleNodes bounds draw calls (one per node).
  • maxPoints bounds total points on screen, which is what actually drives GPU memory and fill cost. It matters most on datasets with very dense nodes, where 100 nodes can mean very different point counts.

Raise them for more on-screen detail at the cost of memory and per-frame work; lower them to guarantee a budget on weak hardware.

Bandwidth & latency

concurrency and maxConcurrentRequests

These bound two different stages:

  • concurrency β€” Worker threads decoding in parallel (default 5). Decoding is CPU-bound and saturates at a handful of workers, and each worker carries its own laz-perf WASM instance.
  • maxConcurrentRequests β€” HTTP Range Requests in flight at once. Defaults to concurrency (or the size of a workerPool you pass in). Fetching is latency-bound, so a high-latency link can keep far more requests usefully in flight than there are cores.

On a slow or distant origin, raise maxConcurrentRequests rather than concurrency β€” it widens fetching without spawning workers that have nothing to do. Lower both to be gentler on the network and the origin server. Note that sibling nodes selected in the same pass are already merged into a single request where their byte ranges are contiguous.

prefetchFrustumFactor and maxPrefetchNodes β€” prefetching

Each LoD pass also looks at a frustum widened by prefetchFrustumFactor (default 1.8, scaling the view's width and height). Nodes inside that wider region but off-screen are loaded and cached but not drawn, so a pan reveals data that's already there. Up to maxPrefetchNodes (default 50) such nodes are considered per pass, and they are only fetched when the visible loads leave a free slot, so prefetch never delays what's on screen.

  • Raise the factor for smoother fast pans, at the cost of bandwidth for data that may never be viewed.
  • Set prefetchFrustumFactor: 1 to disable prefetching (e.g. on a metered or very slow connection).

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.

Styling

Every point reaches the GPU with its baked colour, raw intensity, classification, and normalized elevation, and the colour is chosen in the vertex shader. All styling settings are therefore uniform updates β€” no HTTP request, no LAZ decode, and the node cache is untouched.

ds.colorMode = 'classification';     // 'rgb' | 'intensity' | 'classification' | 'elevation'
ds.classificationFilter = [2, 6];    // only ground and buildings
ds.classificationFilter = undefined; // back to everything
ds.intensityRange = [0, 4000];       // pin the intensity ramp
ds.intensityRange = undefined;       // back to auto
ds.opacity = 0.5;
Option Notes
colorMode 'rgb' uses the file's colour, falling back per point to the classification palette, then flat grey. 'classification' applies the ASPRS palette even when the file has RGB. 'elevation' ramps blue β†’ red over the header's full Z range. 'intensity' is greyscale over intensityRange.
intensityRange When unset, it grows to [0, highest intensity seen] as nodes load, because LAS producers rarely use the full 16-bit range. Pin it when you need consistent shading across sessions.
classificationFilter Codes 0–255; anything else throws RangeError. Filtered points are dropped in the vertex shader, so filtering hides points rather than freeing memory.
opacity 0–1 (validated at load and on every assignment). Below 1, points are alpha-blended with no per-point depth sort, so overlapping points may blend out of order.

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.
  • If heights sit consistently above or below the terrain (orthometric vs. ellipsoidal datum mismatch), either pass geoidOffset at load time or adjust dataSource.heightOffset live. heightOffset moves each node's model matrix, so it applies instantly to already-loaded nodes without a reload β€” handy for finding the right value with a slider, then baking it into geoidOffset.

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 (or a HEAD request's Content-Length when CORS hides it).
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 StageTiming β€” count (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. Prefetched nodes count toward transferredBytes too.

Recommended presets

Weak device / mobile

{ sseThreshold: 350, maxVisibleNodes: 60, maxPoints: 2_000_000, maxCacheNodes: 90, concurrency: 3, prefetchFrustumFactor: 1 }

Quality workstation

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

High-latency origin

{ concurrency: 4, maxConcurrentRequests: 12 }

Bandwidth-constrained origin

{ maxConcurrentRequests: 2, debounceMs: 200, sseThreshold: 300, prefetchFrustumFactor: 1 }