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
250is 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
maxCacheNodesif 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.
maxVisibleNodesbounds draw calls (one per node).maxPointsbounds 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 toconcurrency(or the size of aworkerPoolyou 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: 1to 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/projDefonly when the file's WKT is missing or unrecognized. - Auto-detected
zFactor/xyFactorstill apply on top of an explicitproj/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
geoidOffsetat load time or adjustdataSource.heightOffsetlive.heightOffsetmoves 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 intogeoidOffset.
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 }