A GPU-accelerated WebGL renderer for arbitrarily large n-dimensional scientific datasets stored in Zarr format. Delivers maximum visualization performance limited only by your graphics hardware, display resolution, and network bandwidthโnot by software constraints. Features advanced HDR rendering, real-time effects, and intuitive navigation controls.
โถ Try it in your browser โ 88 live demos as interactive scenes, no install. To open your own compiled archive, the viewer is deployed on its own at luxarviewer.dev?src=<url-to-your-scene>.
src/config/ with TypeScript types@luxar/viewer ships as a side-effect-free ES module. Importing the
package does not patch your console, inject CSS into your body, or
mutate :root โ the viewer only touches DOM you give it via the canvas
option, plus the UI overlays it mounts into the container you provide
(defaulting to document.body).
npm install @luxar/viewer three # three is a peer dep
import { LuxarApp } from '@luxar/viewer';
import '@luxar/viewer/styles.css'; // component styles, prefixed under .luxar-*
const canvas = document.querySelector<HTMLCanvasElement>('#viewer-canvas')!;
const app = new LuxarApp();
await app.init({
canvas,
src: 'https://example.com/data.zarr',
updateBrowserUrl: false, // default: don't rewrite host URL on dataset change
});
// Later (e.g. when the host route unmounts):
app.dispose(); // removes all listeners, GPU resources, UI
The styles.css bundle also ships a Tailwind-like set of atomic utility
classes for embedders to reuse โ flexbox, justify/gap, padding/margin,
text color/size/weight/align, surface, border, radius, shadow, backdrop
blur, transition, display, position, overflow, cursor, opacity, and
z-index groups. Every selector is namespaced under .luxar- (so it never
collides with host-page styles) and resolves to the viewer's --luxar-*
theme custom properties, so utilities pick up the active theme with no
hardcoded colors or spacing. See
src/styles/base/README.md for the full
group table.
LuxarAppOptions| Option | Type | Default | Notes |
|---|---|---|---|
canvas |
HTMLCanvasElement |
โ | The canvas the viewer renders into. Required. |
container |
HTMLElement |
document.body |
Host element the viewer mounts all overlays/panels/toasts/dialogs into. A non-body container is promoted to a containing block (contain: layout) so fixed overlays scope to it; restored on dispose(). |
src |
string |
config | Initial Zarr URL. Empty/missing shows the dataset browser. |
debug |
boolean |
false |
Exposes window.__luxarDebug for Playwright / dev console. |
loaderConfig |
LoaderConfig |
โ | Cache and prefetch flags (noCache, cacheDebug, clearCache, noPrefetch, prefetchDebug). |
gpuPoolMaxBytes |
number | null |
config | Session-wide GPU geometry budget in bytes. null auto-sizes from device memory, measured heap, and device class; 0 disables byte-budget eviction; a positive value pins it. |
updateBrowserUrl |
boolean |
false |
Opt in to mirroring picked datasets into the browser URL. bootstrapStandalone() sets this to true. |
wasmPath |
string |
โ | Override for bundlers that don't resolve import.meta.url for WASM (webpack 4, Parcel 1, etc.). Serving the published package unbundled usually costs one benign 404 before the next candidate wins. |
workerPath |
string |
โ | Same, for the data worker. |
renderer |
'webgl' | 'webgpu' |
'webgl' |
Force the rendering backend. 'webgpu' uses WebGPURenderer + TSL NodeMaterial, falling back to WebGL2 when no adapter. |
webgpuForceWebGL |
boolean |
false |
Diagnostic: with renderer: 'webgpu', route through Three.js's internal WebGL2 backend while keeping the WebGPU/TSL API surface. |
perfTimestamp |
boolean |
false |
Opt in to WebGPU timestamp-query GPU profiling. Tiny runtime cost; ignored under WebGL. |
openCacheStats |
boolean |
false |
Open the data-loading monitor (Cache tab, expanded) once the scene is wired up โ useful for profiling cache behaviour. |
pinnedDPR |
number |
โ | Pin DPR to [0.25, native] and disable adaptive DPR; intended for deterministic tests, captures, and bug reproduction. |
lodFade |
boolean |
true |
Cross-fade adjacent replacement LOD levels instead of swapping abruptly. |
lodEnergyComp |
boolean |
true |
Compensate incomplete stream ladders by their committed energy fraction to reduce brightness popping. |
lodFinest |
boolean |
false |
Force the finest replacement LOD regardless of projected coverage; useful for high-quality still or video capture. |
lodBias |
number |
1 |
Bias replacement LOD selection in screen-area units; 2 selects one occupancy-halved level finer and 4 selects two. Below 1, a partition-anchored ladder cannot reach its finest level; below 0.5, neither can a whole-object ladder. |
depthSort |
boolean |
true |
Enable worker-based back-to-front sorting for order-dependent geometry; disable for deterministic comparisons. |
allowLinks |
boolean |
true |
Allow element-authored links to navigate. Set false to keep element-click / element-contextmenu events and copy actions while suppressing navigation and link menu items. |
factories |
AppFactories |
โ | Construction overrides for the heavy components built by init() (scene manager, recording panel, โฆ). For tests and advanced embedders; omit for the production path. |
Beyond init()/dispose(), LuxarApp exposes flat methods so a host page can
drive the viewer without the built-in UI. All throw if called before init(),
except shortcutForAction() and getDatasetFault(). The former returns
undefined until input is available; the latter returns null until a dataset is loaded.
// Dataset
await app.switchDataset('https://example.com/other.zarr'); // reload in place
const fault = app.getDatasetFault(); // terminal post-load fault, or null
// nD dimensions
const dims = app.getDimensions(); // { ndim, displayed, currentStep, metadata, ranges } (cloned)
app.setDimensionValue(/* index */ 3, /* value */ 12);
await app.awaitDimensionUpdate(); // resolve once the slice data has loaded
// Camera
app.recenterCamera(); // fit/recenter on the scene (the 'F' key)
const pose = app.getCameraPose();
app.setCameraPose(pose); // e.g. restore a saved view
// Viewport โ auto-resizes to the canvas via a ResizeObserver; call manually
// after a synchronous layout change you know the observer won't catch in time.
app.resize();
// Screenshot (async โ WebGPU readback is async)
const blob = await app.screenshot({ format: 'png' }); // 'png' | 'webp' | 'jpeg'
// Keyboard input
app.registerContext('annotation', {
priority: 100,
passthrough: true,
fallbackContexts: ['navigation'],
allowRegisteredBindings: true,
});
app.registerBinding('annotation', {
actionId: 'annotation.accept',
key: 'x',
handler: acceptAnnotation,
description: 'Accept annotation',
help: { section: 'panels', group: 'Annotation', order: 200 },
});
app.pushContext('annotation');
app.popContext();
app.unregisterBinding('annotation', 'x');
app.unregisterContext('annotation');
app.setInputEnabled(false);
const helpKey = app.shortcutForAction('help.toggle');
Events โ subscribe with on(event, listener), which returns an unsubscribe:
const off = app.on('dataset-loaded', ({ src }) => console.log('loaded', src));
app.on('dataset-error', ({ src, error }) => console.error(src, error));
app.on('dataset-fault', ({ src, error }) => console.error(src, error));
app.on('dimensions-changed', (dims) => updateMyUI(dims));
app.on('selection', (sel) => console.log(sel)); // { nodeName, elementIndex, hitNodeName } | null
app.on('element-click', (event) => console.log(event));
app.on('element-contextmenu', (event) => console.log(event));
// off();
element-click and element-contextmenu fire after a no-drag left- or
right-click and include the resolved element, gesture, and link. Subscribe
before init() / switchDataset() so label-less scenes provision picking;
pass allowLinks: false to observe or replace navigation without allowing it.
Note on
selection: fires with the element under the cursor (ornullwhen the hover clears) on any dataset. WhatelementIndexcounts is per-node โ seeSelectionPayloadfor the exact contract: a labelled node reports an on-disk index (for a labelled lines node, the picked segment's start-vertex row), while an unlabelled one reports the visible-buffer slot. Subscribe before the dataset loads (i.e. beforeinit()/switchDataset()) โ the GPU picking pipeline is provisioned at load time only when a listener exists, so picking stays zero-cost for pages that never consume it. Hover-driven; useelement-clickfor click gestures.nodeNameis the user-facing layer (the outermostkind=partitionwrapper when there is one), whileelementIndexis local to the leaf actually hit โ index it againsthitNodeName, which equalsnodeNamewhen the node is not partitioned.
LuxarApp embeds the viewer. LuxarLayer is for the other case: the host
already has a Three.js scene and wants Luxar's data as one more thing in it,
sharing a single WebGL context, camera, and set of controls.
import { LuxarLayer } from '@luxar/viewer';
const layer = new LuxarLayer({
renderer, // host-owned
getCamera: () => camera, // live getter
getViewportSize: () => renderer.getSize(new THREE.Vector2()),
scene, // host-owned
});
await layer.load('https://example.com/imaging.luxar.zarr');
function animate() {
requestAnimationFrame(animate);
layer.update(); // BEFORE the host renders
renderer.render(scene, camera);
}
await layer.dispose();
The layer owns no renderer, camera, controls, post-processing, or UI โ it
contributes a THREE.Group plus the per-frame LOD and depth-sort bookkeeping.
The host must call update() each frame before rendering, resize() after a
viewport or camera-projection change, and pass requestRender if it renders
on demand rather than continuously. renderOrder defaults to 10 and is stamped
onto every nested Luxar Group; host transparent groups should use explicit
lower/higher values. gpuPoolMaxBytes controls the session-wide geometry budget:
null auto-sizes from device memory, measured heap, and device class, 0
disables byte-budget eviction, and a positive value pins bytes. On WebGL context
loss, call handleContextLost() so Luxar backs off that budget; after rebuilding
the host renderer and post-processing, call handleContextRestored().
nD navigation coalesces, so a host can drive it from a slider at frame rate:
const t = layer.findDimension('time');
if (t !== null) {
layer.prefetchDimensionValue(t, frame + 1); // warm the next slice
void layer.setDimensionValue(t, frame); // don't await during playback
}
alignTo(matrix) places the data in the host's world space (for a host that
normalizes its own coordinates); it may be called before or after load().
Read getDimensionNames() rather than assuming a centre-column order โ producers
disagree, and guessing renders a silently transposed scene. setVisible() hides
without discarding caches or in-flight fetches; lazy LOD loads resume on the next
update after re-showing, while hidden resident levels are preferred for eviction
under GPU-budget pressure. setExposure() scales exposure relative to the scene's
authored value, which a host needs because that value was tuned against a different
post chain than its own. On a Mesh in the default opaque mode, this scales cutout
coverage rather than brightness: values below alphaCutoff discard the surface,
while normal blending provides smooth transparency.
Note that a scene's tone_mapping does not apply in layer mode: Luxar
tone-maps in a post-processing pass the layer does not own, so a host wanting a
filmic rolloff over additive geometry must set renderer.toneMapping itself.
Same single-instance rule as LuxarApp, and the two are mutually exclusive. See
docs/specs/LUXAR_LAYER_SPEC.md for the normative public
contract and src/core/layer/README.md for implementation rationale.
ThemeManager, the worker pool, and several UI components are still page-singletons. Mounting two LuxarApp instances at once will share state..luxar-* classnames, but a host page that already styles .luxar-foo will collide.LuxarApp.init() throws a friendly error if window/document are unavailable.A runnable example with a non-trivial host page lives in
examples/embed/. The host-owned renderer counterpart lives in
examples/layer/.
^20.19.0 || >=22.12.0 (engines.node)Browser support: any browser with WebGL 2.0. See Browser Compatibility for what has actually been tested โ Chromium, Firefox and WebKit all pass the E2E smoke subset, and WebKit runs without the on-disk chunk cache.
# Clone the repository
git clone <repository-url>
cd luxar/packages/luxar-viewer
# Install dependencies
pnpm install
# Start development server
pnpm dev
The viewer will be available at http://localhost:5173
# View a specific Zarr dataset
http://localhost:5173/?src=/path/to/your/dataset.zarr
# View demo dataset (if available)
http://localhost:5173
Luxar Viewer supports three navigation modes:
| Key | Action |
|---|---|
| V | Cycle control modes: Orbit -> Fly -> Ortho -> Orbit |
| I | Toggle inertial mode (Fly mode only) |
| F | Recenter camera on scene |
| C | Toggle cinematic mode |
| Input | Action |
|---|---|
| Left Mouse Drag | Pan camera |
| Right Click + Drag | Rotate camera around scene |
| Mouse Wheel | Zoom in/out |
| Shift + Mouse Wheel | Roll (view-axis rotation) |
| Input | Action |
|---|---|
| W/S | Move forward/backward |
| A/D | Strafe left/right |
| Alt+W / Alt+S | Move up/down |
| Arrow Keys | Look up/down/left/right |
| Left Mouse Drag | Strafe (screen-space translation) |
| Right Mouse Drag | Free look (rotate camera) |
| Mouse Wheel | Forward/backward velocity impulse |
| Shift + Mouse Wheel | Roll (view-axis rotation) |
| I | Toggle inertial physics (drift/momentum) |
| Input | Action |
|---|---|
| Left Mouse Drag | Pan (Napari/Google Maps convention) |
| Mouse Wheel | Zoom in/out |
| Shift + Mouse Wheel | Roll (view-axis rotation) |
| Input | Action |
|---|---|
| Space | Toggle fullscreen mode |
| H | Show/hide help overlay |
| R | Toggle advanced rendering controls panel |
| P | Toggle performance statistics |
| N | Toggle nD dimension panel |
| O | Toggle dataset browser |
| Ctrl+L | Toggle debug console |
| Esc | Exit fullscreen / Close panels |
| Input | Action |
|---|---|
| Number keys (1-9) | Select which non-displayed dimension to navigate |
[ and ] |
Step backward/forward in the selected dimension |
| Dimension Sliders | Click and drag to navigate through dimensions |
Luxar Viewer expects Zarr datasets with the following structure:
dataset.zarr/
โโโ .zmetadata # Consolidated metadata (optional)
โโโ .zattrs # Scene attributes including dimensions
โโโ positions/ # nD coordinates (Float32, shape: [N, D])
โ โโโ .zarray
โ โโโ [chunks...]
โโโ colors/ # RGB colors (Uint8, shape: [N, 3]) - optional
โ โโโ .zarray
โ โโโ [chunks...]
โโโ radii/ # Point radii (Float32, shape: [N]) - optional
โ โโโ .zarray
โ โโโ [chunks...]
โโโ sharpness/ # Edge falloff (Float32, shape: [N]) - optional
โโโ .zarray
โโโ [chunks...]
{
"type": "points",
"transform": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], // 4x4 transform matrix (optional)
"sceneDimensions": {
// Required for nD data
"dimensions": [
{ "name": "x", "unit": "ฮผm", "range": [-100, 100], "display": true },
{ "name": "y", "unit": "ฮผm", "range": [-100, 100], "display": true },
{ "name": "z", "unit": "ฮผm", "range": [-50, 50], "display": true },
{ "name": "time", "unit": "s", "range": [0, 10], "display": false, "step": 0.1 }
]
}
}
[N, D] where D matches dimension count[N, 3] (RGB values 0-255)[N] (per-point radius)[N] (edge falloff, normalized 0-1; 0.5 = Gaussian)Luxar Viewer supports visualization of n-dimensional data beyond traditional 3D:
Every Zarr dataset defines its dimensions at the scene level:
Points in nD space are treated as hyperspheres. When viewing a 3D slice:
spatial=True (non-displayed dimensions default to non-spatial)r_eff = sqrt(rยฒ - dยฒ) where d is distance from slice// Dataset with x, y, z (displayed) and time, channel (non-displayed)
// Press '1' to select time dimension
// Use '[' and ']' to step through time
// Press '2' to select channel dimension
// Sliders update automatically
The advanced rendering controls panel (located on the left side) provides real-time adjustment of:
The viewer source tree is organized into 17 subpackages, each with its own
README.md documenting its files, public surface, invariants, and
dependencies in detail. The top-level shape:
src/
โโโ index.ts # Public-API barrel (side-effect-free)
โโโ lib-styles-entry.ts # CSS-only build entry
โ
โโโ core/ # Application bootstrap, lifecycle, debug interface
โโโ config/ # Unified configuration system (sections/ + zarr-bridge/)
โโโ cache/ # 3-tier cache: L0 decompressed, L1 memory, L2 OPFS
โโโ data/ # Zarr loading, nD slicing, per-geometry loaders
โโโ rendering/ # GLSL/TSL materials, post-processing, picking, GPU buffer pool
โโโ scene/ # SceneManager + animation + scene-manager helpers
โโโ controls/ # Orbit / Fly / Ortho camera controls
โโโ input/ # Keyboard / mouse handlers, context routing
โโโ ui/ # GUI library, panels, monitors, recording panel
โโโ styles/ # CSS (base, components, themes, embed vs standalone)
โโโ themes/ # Theme manager + dark/light/glass theme definitions
โโโ types/ # Type definitions and ambient declarations
โโโ utils/ # Cross-cutting utilities (log, event bus, HDR, platform)
โโโ wasm/ # Rust kernels + TypeScript fallback (parity-tested)
โโโ workers/ # Worker pool, data worker, validation
โโโ profiling/ # UpdateProfiler for hierarchical timing
โโโ tests/ # Unit (vitest), e2e harnesses, mocks, builders, benchmarks
For per-subpackage details โ file tables, public exports, invariants โ read
the README.md inside the subfolder. The major subpackages also document
their own subpackages (e.g. rendering/README.md links to materials/,
picking/, post-processing/, material-manager/, node-factory/,
gpu-buffer-pool/).
See also: src/README.md for the navigational hub and
the enforced layer order, CONVENTIONS.md for project-
wide conventions, and ARCHITECTURE-DIAGRAMS.md
for high-level diagrams.
# Development
pnpm dev # Start development server with hot reload
pnpm build # Build for production (includes WASM build)
pnpm preview # Preview production build
# Code Quality
pnpm lint # Run ESLint
pnpm typecheck # Run TypeScript type checking
pnpm format # Format code with Prettier
pnpm check # Run all quality checks (typecheck + lint + test)
# Unit Testing
pnpm test # Run unit tests with Vitest
pnpm test:coverage # Run tests with coverage report
pnpm test:ui # Run tests with interactive UI
pnpm test:watch # Run tests in watch mode
pnpm test:with-fixtures # Generate test fixtures, then run tests
# E2E Testing (Playwright)
# Prerequisite: examples + fixtures must exist. Run once locally:
# make run-examples
# pnpm test:generate-fixtures
# The Make targets refresh their required examples and generated fixtures.
# GitHub CI runs the Chromium mobile/touch suite; the full, smoke, cross-browser,
# and visual suites remain local entry points. Visual snapshots are Linux-only
# developer aids and are not validated by green CI.
pnpm test:e2e # Run all E2E tests
pnpm test:e2e:smoke # Run the non-GPU smoke subset
pnpm test:e2e:smoke:strict # Run smoke with strict console handling
pnpm test:e2e:mobile # Run the Chromium mobile/touch suite used by CI
pnpm test:e2e:browsers # Run the Firefox/WebKit cross-browser subset
pnpm test:e2e:visual # Run visual tests (snapshot checks on Linux)
pnpm test:e2e:visual:update # Refresh Linux visual baselines
pnpm test:e2e:ui # Run E2E tests with interactive UI
pnpm test:e2e:debug # Run E2E tests in debug mode
pnpm test:e2e:report # Show E2E test report
# WASM
pnpm build:wasm # Build Rust WASM module
pnpm build:wasm:dev # Build WASM in development mode
pnpm test:wasm # Run Rust unit tests (cargo test)
pnpm bench:wasm # Run WASM vs TypeScript benchmarks
# Fixtures & Media
pnpm test:generate-fixtures # Generate test fixtures from Python
pnpm readme-images # Generate README screenshot images
# AI Debugging
pnpm agent:debug # Run Playwright agent driver (headless)
pnpm agent:debug:visible # Run agent driver with visible browser
The native launcher binaries (produced by make build-launchers in the
repo root and used by luxar export --native ...) honor:
LUXAR_LAUNCHER_NO_WEBVIEW=1 โ Skip the embedded WebView and open the
exported scene in the system default browser instead. Useful for
smoke-testing the launcher without a graphical session. It does not let
the prebuilt Linux binary run without libwebkit2gtk โ WebKit is linked
at build time, so the webkit2gtk-4.1 runtime must be present to start.LUXAR_CACHE_BUDGET_MB=<N> โ Total in-memory cache pool (L0 + L1 +
S-cache) the launcher passes to the viewer via ?cacheBudgetMB=
(default 2048). WebKit WebViews don't implement performance.memory,
so the viewer can't auto-size its caches from the JS heap. The same value
supplies the auto GPU-geometry/LOD residency signal through the implied
non-cache remainder; the default therefore raises that budget from 512 to
1432 MB when deviceMemory is unavailable. Lower it on a constrained machine
(e.g. =512).Luxar Viewer uses a unified configuration system in src/config/. Edit src/config/index.ts to customize:
export const config: AppConfig = {
camera: {
initialPosition: { x: 0, y: 0, z: 8 },
fovMin: 10,
fovMax: 170,
},
scene: {
backgroundColor: 0x000000, // Pitch black โ zero radiance under the HDR exposure chain
},
animation: {
idleTimeoutMs: 2000, // Auto-pause after 2 seconds
},
renderingControls: {
defaults: {
fov: 47, // Field of view in degrees (50mm Normal)
bloomEnabled: false, // Bloom effect (opt-in via zarr viewer_config)
bloomStrength: 0.25, // Bloom intensity multiplier
bloomRadius: 1.0, // Blur radius for bloom spread
bloomThreshold: 0.01, // Luminance threshold for bloom
fxaaEnabled: false, // FXAA anti-aliasing
msaaEnabled: false, // MSAA (hardware-accelerated, fast and sharp)
ssaaEnabled: false, // SSAA (supersampling, highest quality, heavy cost)
// ... more rendering options
},
},
// ... more options
};
The material manager in src/rendering/material-manager.ts provides optimized material handling. Per-geometry getters return cached materials keyed by property bucketing:
// Supported blending modes (canonical set: BLENDING_MODES in src/types/blending.ts)
type BlendingMode = 'additive' | 'volumetric' | 'normal' | 'max' | 'opaque' | 'luminous';
// Per-geometry getters โ see src/rendering/material-manager.ts for
// PointMaterialProperties / LineMaterialProperties / GSplatMaterialProperties
const pointMat = materialManager.getPointMaterial({
blendingMode: 'additive',
opacity: 1.0,
gamma: 2.2,
});
const lineMat = materialManager.getLineMaterial({/* ... */});
const gsplatMat = materialManager.getGSplatMaterial({/* ... */});
Point rendering supports per-point attributes:
Customize bloom and tone mapping in src/config/index.ts under renderingControls.defaults:
renderingControls: {
defaults: {
bloomEnabled: false, // Enable/disable bloom effect
bloomThreshold: 0.01, // Luminance threshold (0.0 = everything glows)
bloomStrength: 0.25, // Bloom intensity multiplier
bloomRadius: 1.0, // Bloom spread
bloomLevels: 8, // Mipmap levels (1-12, lower = faster)
toneMapping: 'ACES', // Default. Options: None, Linear, Reinhard, Cineon, ACES, AgX, Neutral
// (use 'None' for exact colormap-LUT fidelity inside [0, 1])
exposure: 0.0, // Log2 stops (0 = neutral, +1 = 2x brighter)
},
},
Luxar Viewer supports multiple anti-aliasing techniques with important compatibility notes:
renderingControls: {
defaults: {
fxaaEnabled: false, // FXAA: Fast post-process AA (disabled by default)
msaaEnabled: false, // MSAA: Hardware-accelerated, fast and sharp
msaaSamples: 4, // MSAA sample count (2, 4, 8)
ssaaEnabled: false, // SSAA: Supersampling, highest quality, heavy cost
ssaaMultiplier: 2.0, // SSAA resolution multiplier (1.5x, 2x, 4x)
},
},
Anti-Aliasing Notes:
TARGET_CHUNK_BYTES in luxar.typing_utils.constantsBuilt-in performance monitoring in src/ui/performance-monitor.ts is a compact,
theme-matched readout (no third-party dependency) that docks into the control
rail. It shows one metric at a time and cycles on click/Enter:
// A vendored square readout; the control rail docks its `.element`.
const monitor = new PerformanceMonitor();
// Visibility control โ measurement is driven by the animation loop's
// `frame-start` / `frame-end` events on the event bus. The monitor only
// subscribes while visible, so it incurs no cost when hidden.
monitor.show(); // Show (subscribes to frame timing)
monitor.hide(); // Hide (unsubscribes)
monitor.toggle(); // Toggle visibility (bound to the P key / rail gauge)
monitor.cycleMode(); // Cycle the metric: FPS -> frame time (ms) -> graph
monitor.visible; // boolean getter for current visibility
monitor.element; // the widget element (mounted by the control rail)
cycleMode() rotates FPS -> ms -> graphWhite screen on load
Poor performance
Zarr loading errors
Anti-aliasing selection
nD navigation not working
sceneDimensions are defined in dataset .zattrsrange and step valuesRequires WebGL 2.0, which is the default backend. WebGPU is opt-in via
?renderer=webgpu and falls back to an internal WebGL2 backend when no adapter
is available. The build targets esnext with no browserslist, so there is no
toolchain-derived version floor.
Verified 2026-09-04 on macOS arm64 by running the E2E smoke subset (13 tests) against Playwright's bundled engines:
| Engine | Smoke subset | Notes |
|---|---|---|
| Chromium | 13/13 pass | L2 (OPFS) disk cache initialises |
| Firefox | 13/13 pass | L2 (OPFS) disk cache initialises |
| WebKit | 13/13 pass | runs without the L2 disk cache โ the OPFS store's init / write probe fails, so chunk data is not persisted between sessions |
Reproduce after running pnpm test:generate-fixtures and pnpm exec playwright install firefox webkit, then run pnpm test:e2e:browsers. The checked-in visual
snapshot corpus is Chromium-only, so this command ignores snapshot assertions
and compares functional behavior rather than pixels.
Not verified: the full E2E suite on any engine but Chromium; Safari and Edge
themselves โ Playwright's WebKit is a WebKit build, not Safari, and Edge is
Chromium-based but untested; and any performance comparison between engines.
WebKit lacks main-thread FileSystemFileHandle.createWritable(), so Safari and
the native WKWebView launcher fall back to L1-only caching; see the
opfs-unavailable cache badge.
?src=<path> โ Path to a Zarr dataset (trailing slashes are normalized away)?theme=<id> โ Select dark, light, frosted-glass, or liquid-glass?debug โ Expose window.__luxarDebug for Playwright / dev console?title=<text> โ Browser tab title; luxar serve --open derives it from the dataset file name, a scene's authored viewer_config.title overrides it, and it is dropped when you switch datasets?kiosk โ Force kiosk mode on as a hard operator override (locks a scene; cannot unlock authored kiosk mode)?control / ?control=<ws-url> โ Attach to the serving app's remote-control hub; an explicit URL must be same-origin unless ?controlAllowCrossOrigin is also present?controlToken=<secret> โ Shared control-hub token, matching luxar serve --control-token?controlAllowCrossOrigin โ Permit an explicit ?control= URL to cross the page origin (LAN kiosks with an authenticated hub only)?panel=<module-url> โ On control.html, load an alternative same-origin control-panel module?bakeEnv โ Capture the scene environment and expose the encoded result through the debug API?probe=<auto|node:<path>|x,y,z> โ Select the environment-capture probe used by ?bakeEnv?envResolution=<16-1024> โ Set the cube-face resolution used by ?bakeEnv?noCache โ Disable all cache tiers (S-cache + L0 + L1 + L2) for this session?noSliceCache โ Disable only S-cache; L0/L1/L2 remain active?noOpfs โ Disable only the L2 persistent (OPFS) tier; L0/L1/S-cache remain active. For environments whose OPFS stalls; the automatic circuit breaker covers the un-flagged case?opfsReadConcurrency=<N> โ Override the page-wide concurrent OPFS read cap (default 64) for diagnosis?cacheDebug โ Verbose cache logging?clearCache โ Clear stored cache tiers before loading; the per-load S-cache starts empty?cacheStats โ Open the data-loading monitor on its Cache tab after initialization?noPrefetch โ Disable adjacent-chunk prefetching (caches still active)?prefetchDebug โ Verbose prefetch logging?noLodFade โ Disable replacement-LOD cross-fading (enabled by default)?noLodEnergy โ Disable stream-ladder energy compensation (enabled by default)?lodFinest โ Force the finest replacement LOD regardless of projected coverage?lodBias=<N> โ Bias replacement LOD selection in screen-area units (2 = one level finer on occupancy-halved ladders; positive values only). Because finite screen-area coverage tops out at 1, values below 1 make a partition-anchored finest threshold of 1 unreachable, and values below 0.5 make a whole-object finest threshold of 0.5 unreachable; near-plane saturation can still select finest?noBlendWarmup โ Disable the WebGL blend-variant program warm-up (enabled by default): each reachable blend-mode program is otherwise pre-linked off the interaction path after a dataset load, so the first Layers-panel blend switch does not pay the link cost on the click?noLinks โ Disable element-authored navigation and link menu items while preserving copy actions and element-click / element-contextmenu events?depthSort=0 โ Disable worker depth sorting (false and off are also accepted)?noDensityGuard โ Disable the projected-density guard (per-node keep-fraction thinning + refinement rung cap on over-drawn nodes; enabled by default) for the session?densityCap=<N> โ Session-only override of the density guard's blendable cap, in elements per drawing-buffer pixel (configured default 4)?renderer=webgpu โ Use WebGPURenderer (TSL NodeMaterial) instead of the default WebGLRenderer?renderer=webgpu&webgpuForceWebgl โ Keep the WebGPU/TSL API surface while Three.js routes through its internal WebGL2 backend (diagnostic)?perfTimestamp โ Enable WebGPU timestamp-query profiling for performance tests?dpr=<value> โ Pin a fixed device pixel ratio for the session (clamped to [0.25, native]) and lock adaptive resolution off?input=<touch|mouse> โ Force the session's JS input profile: pointer flags, hover capability, touch points, and device tier. This changes device-class fallback budgets (touch only โ mouse keeps the detected tier), primary-tip pen routing, the Safari gesture-canceller gate, and whether the help overlay lists its Touch section; touch additionally applies the mobile rendering budgets (adaptive-DPR floor and refresh ceiling, high-DPR cap, GPU-byte and element-texture ceilings, data-worker count) and skips the blend-variant program warm-up. Stylesheets and non-pen gesture routing still follow the real media features and PointerEvent.pointerType, so a faithful check needs device emulation or a real device. Detected by default, including iPadOS masquerading as macOS?lineJoin=<none|miter> โ Force the line join style for the session; applies only to linePrimitive=screen-space (the capsule partitions joints unconditionally)?linePrimitive=<capsule|screen-space> โ Select the line rendering primitive for the session (#1352), overriding the Settings โ Advanced โ Line primitive policy; default policy auto builds the capsule (gaussian-like 2D point-to-segment profile: stable end-on discs, seamless partitioned joints) except for very large line nodes, which build the leaner screen-space quad. The third primitive, volumetric, was deleted after the capsule flip?gpuBudgetMB=<N> โ Override the shared GPU-geometry/LOD retention budget; 0 means unbounded?cacheBudgetMB=<N> โ Override the total in-memory cache pool (L0 + L1 + S-cache) in megabytes; used where performance.memory is unavailable (WKWebView, Safari), and also supplies the implied non-cache remainder as a GPU-geometry/LOD residency signal (replacing the 512 MB fallback in either direction when deviceMemory is unavailable, capped at 2 GB)import { LuxarApp } from './src/core/app.js';
import { config } from './src/config/index.js';
const app = new LuxarApp();
await app.init({
canvas: document.getElementById('app'),
src: '/path/to/dataset.zarr',
});
// Access components (available after init)
const { sceneManager, animationController, renderingControls } = app.components;
// Modify bloom defaults before initialization (or for next scene load)
config.renderingControls.defaults.bloomEnabled = true;
config.renderingControls.defaults.bloomStrength = 0.2;
config.renderingControls.defaults.bloomRadius = 0.8;
config.renderingControls.defaults.bloomThreshold = 0.1;
// Available components:
// sceneManager - 3D scene, renderer, camera, controls
// animationController - Render loop, per-frame callbacks
// renderingControls - UI panel for rendering settings
// adaptiveDPRManager - Dynamic resolution scaling
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)Copyright (c) 2025-2026 The Luxar Authors
This project is licensed under the BSD-3-Clause License. See the LICENSE file for details.
Built with โค๏ธ for the scientific visualization community