Luxar Viewer API Documentation - v2026.9.22
    Preparing search index...

    Luxar Viewer API Documentation - v2026.9.22

    ๐ŸŒŒ Luxar Viewer

    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>.

    • ๐ŸŽจ Advanced HDR Rendering: 16-bit floating-point precision with ACES filmic tone mapping
    • โœจ Real-time Bloom Effects: Professional-quality UnrealBloomPass with customizable parameters
    • ๐Ÿ–ฑ๏ธ Intuitive Navigation: Smooth camera controls optimized for scientific data exploration
    • ๐Ÿ“ฑ Responsive Design: Seamless fullscreen support and dynamic viewport management
    • โšก Unlimited Performance: GPU-accelerated pipeline designed to scale with hardware capabilities
    • ๐ŸŽฏ Geometry Rendering: Four first-class geometry types โ€” points, lines, Gaussian splats, and shaded triangle meshes
    • ๐Ÿ“Š Performance Monitoring: Built-in FPS and timing metrics for optimization
    • ๐ŸŒŠ Streaming Ready: Chunked Zarr format enables progressive loading of massive datasets
    • ๐Ÿ”Œ Extensible Architecture: Modular design ready for additional geometry types and rendering modes
    • ๐ŸŽ›๏ธ nD Navigation: Beautiful dimension sliders UI for exploring higher-dimensional data
    • ๐Ÿ” Radius-Based Slicing: Natural visualization of nD data using hypersphere intersection
    • โŒจ๏ธ Keyboard Controls: Intuitive keyboard navigation for dimension selection and stepping
    • โš™๏ธ Anti-Aliasing: FXAA / MSAA / SSAA with known compatibility notes
    • ๐Ÿงฉ Unified Configuration: Centralized config system in src/config/ with TypeScript types
    • ๐Ÿ“ธ Recording Panel: Screenshots (PNG/WebP/JPEG/EXR), image-sequence ZIPs, EXR-sequence ZIPs, and video capture (WebM/MP4/MKV via mediabunny) with turntable mode
    • ๐Ÿ“ Scale Bar: Physical scale bar overlay using dimension unit metadata
    • ๐Ÿ”„ nD Transforms: Inverse-query transforms for non-displayed dimensions (affine and categorical)
    • ๐ŸŽฏ Material Caching: Optimized material management with intelligent caching strategy

    @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.

    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 (or null when the hover clears) on any dataset. What elementIndex counts is per-node โ€” see SelectionPayload for 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. before init() / 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; use element-click for click gestures. nodeName is the user-facing layer (the outermost kind=partition wrapper when there is one), while elementIndex is local to the leaf actually hit โ€” index it against hitNodeName, which equals nodeName when 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.

    • Multiple viewers on the same page. ThemeManager, the worker pool, and several UI components are still page-singletons. Mounting two LuxarApp instances at once will share state.
    • Shadow DOM isolation. The viewer uses regular DOM. The CSS is prefixed under .luxar-* classnames, but a host page that already styles .luxar-foo will collide.
    • SSR / non-browser rendering. 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/.

    • Node.js 22.22+ (Node.js 22 LTS recommended) and pnpm โ€” the floor for developing in this repo (jsdom 30 test toolchain); the published library package supports ^20.19.0 || >=22.12.0 (engines.node)
    • Modern web browser with WebGL 2.0 support
    • Zarr dataset (see Data Format section)

    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:

    • Orbit Mode (default): Quaternion-based rotation around a target point (no gimbal lock)
    • Fly Mode: First-person navigation with WASD movement and inertial physics
    • Ortho Mode: Orthographic pan + zoom for 2D viewing
    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 }
    ]
    }
    }
    • Positions: Float32 arrays with shape [N, D] where D matches dimension count
    • Colors: Uint8 arrays with shape [N, 3] (RGB values 0-255)
    • Radii: Float32 arrays with shape [N] (per-point radius)
    • Sharpness: Float32 arrays with shape [N] (edge falloff, normalized 0-1; 0.5 = Gaussian)
    • Transform: Optional 4x4 transformation matrix for positioning/scaling

    Luxar Viewer supports visualization of n-dimensional data beyond traditional 3D:

    Every Zarr dataset defines its dimensions at the scene level:

    • Displayed dimensions: The 3D subset shown in the viewer (max 3)
    • Non-displayed dimensions: Additional dimensions navigated via sliders/keyboard
    • Dimension metadata: Names, units, ranges, and navigation step sizes

    Points in nD space are treated as hyperspheres. When viewing a 3D slice:

    • Point visibility depends on hypersphere intersection with viewing hyperplane
    • Larger radius = visible across more slices only for dimensions declared spatial=True (non-displayed dimensions default to non-spatial)
    • Effective radius shrinks as: r_eff = sqrt(rยฒ - dยฒ) where d is distance from slice
    • Natural representation of uncertainty or spread in higher dimensions
    • Beautiful sliders: Napari-inspired design for intuitive navigation
    • Status bar: Shows current position in nD space with units
    • Keyboard shortcuts: Quick dimension selection and stepping
    • Smart initialization: Non-displayed dimensions start at their minimum values
    // 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:

    • Bloom Settings: Threshold, strength, radius, and resolution scale
    • HDR Controls: Exposure and tone mapping parameters
    • Point Rendering: HDR multiplier and falloff parameters
    • FXAA: Fast approximate anti-aliasing (recommended for additive blending)
    • MSAA: Multi-sample anti-aliasing with sample count selection (2x, 4x, 8x)
    • SSAA: Super-sample anti-aliasing with resolution multipliers (1.5x, 2x, 4x)
    • Real-time preview: Changes are applied immediately with smooth animations
    • Settings persistence: User preferences are saved between sessions
    • Performance impact indicators: Visual feedback on rendering cost
    • Preset management: Quick access to optimized configurations

    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:

    • radius: Individual point sizes for visual hierarchy
    • sharpness: Control edge falloff (0.5 = soft glow, 10.0 = sharp edges)
    • Automatic compensation: Shader adjusts intensity based on sharpness

    HDR Post-Processing

    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:

    • MSAA: Hardware-accelerated, fast and sharp โ€” great default for most scenes. Note: MSAA has limitations with additive blending (used by GSplats); consider FXAA for scenes with Gaussian splats
    • SSAA + MSAA: SSAA above 1x temporarily suspends MSAA allocation. Combining multisampling with an already-upscaled SSAA target is redundant and can exceed browser framebuffer-allocation limits; returning to 1x or disabling SSAA restores the configured MSAA setting.
    • FXAA: Fastest post-process AA, may slightly blur the image
    • SSAA: Highest quality (supersampling), significant performance cost
    • Element Count: Optimize for datasets with millions of elements
    • Chunk Size: Zarr chunk sizes of 16KB-256KB (target 64KB) โ€” matches TARGET_CHUNK_BYTES in luxar.typing_utils.constants
    • LOD: Consider implementing level-of-detail for very large datasets
    • Compression: Use Zarr compression (e.g., blosc) to reduce network transfer

    Built-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)
    • Real-time FPS: rolling frame-rate average
    • Frame timing: milliseconds per frame (EMA)
    • History graph: a scrolling FPS sparkline
    • Metric cycling: cycleMode() rotates FPS -> ms -> graph
    • Idle optimization: only subscribes to frame timing while visible
    1. Ideal Dataset Size: 100K-10M elements for smooth interaction
    2. Chunk Strategy: Use roughly square chunks (e.g., 1000x1000 elements)
    3. Network: Serve Zarr data from same domain to avoid CORS issues
    4. Browser: Chrome and Firefox offer best WebGL performance
    5. Hardware: Dedicated GPU recommended for large datasets

    White screen on load

    • Check browser console for errors
    • Verify Zarr dataset URL is accessible
    • Ensure CORS headers are set if serving from different domain

    Poor performance

    • Check dataset size (>10M elements may be slow)
    • Reduce bloom quality in config
    • Verify GPU acceleration is enabled in browser
    • Try disabling MSAA/SSAA and using FXAA instead

    Zarr loading errors

    • Verify dataset structure matches expected format
    • Check that positions and colors arrays exist
    • Ensure proper Zarr metadata (.zarray files)
    • Verify scene dimensions are defined for nD datasets

    Anti-aliasing selection

    • Try MSAA first (fast, sharp, hardware-accelerated)
    • Use FXAA for lightweight post-process smoothing
    • Use SSAA only for final renders (heavy performance cost)

    nD navigation not working

    • Verify sceneDimensions are defined in dataset .zattrs
    • Check that non-displayed dimensions have proper range and step values
    • Ensure dimension count matches position data shape

    Requires 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.

    • WebGL 2.0 support required
    • Float texture support (for HDR rendering)
    • Minimum 2GB GPU memory recommended for large datasets
    • ?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
    1. Fork the repository
    2. Create a feature branch (git checkout -b feature/amazing-feature)
    3. Commit changes (git commit -m 'Add amazing feature')
    4. Push to branch (git push origin feature/amazing-feature)
    5. Open a Pull Request
    • Follow TypeScript strict mode
    • Use ESLint and Prettier for code formatting
    • Add JSDoc comments for public APIs
    • Test with various dataset sizes
    • Ensure WebGL resource cleanup

    Copyright (c) 2025-2026 The Luxar Authors

    This project is licensed under the BSD-3-Clause License. See the LICENSE file for details.

    • Three.js - 3D rendering engine
    • Zarrita - Zarr format support
    • Vite - Development tooling
    • Contributors - Thanks to all who helped improve this project

    Built with โค๏ธ for the scientific visualization community