SuperSplat Viewer

August 24, 2026 · View on GitHub

NPM Version NPM Downloads License Discord Reddit X

| User Manual | Blog | Forum |

This is the official viewer for SuperSplat.

supersplat-viewer

The web app compiles to a simple, self-contained static website.

URL Parameters

The app supports a number of URL parameters (these are subject to change):

Content

ParameterDescriptionDefault
settingsURL of the settings.json file./settings.json
contentURL of the scene file (.ply, .sog, .compressed.ply, .meta.json, .lod-meta.json)./scene.compressed.ply
skyboxURL of an equirectangular skybox image
posterURL of an image to show while loading
collisionURL of a collision asset (.glb mesh, or voxel data). voxel is accepted as an alias.

UI

ParameterDescription
nouiHide the UI overlay
noanimStart with animation paused
ministatsShow runtime CPU/GPU performance graphs
langOverride the UI language (de, en, es, fr, ja, ko, pt-BR, ru, zh-CN; default: detect from browser)

Renderer

By default the viewer uses WebGPU when available (falling back automatically when not). The flag below forces the WebGL renderer (also required for WebXR / AR / VR):

ParameterDescription
webglForce the WebGL renderer (required for AR/VR)
aaEnable antialiasing (WebGL only)
nofxDisable post effects
hprOverride highPrecisionRendering from settings (?hpr, ?hpr=1, ?hpr=true, ?hpr=enable to enable)
budgetOverride the splat budget, in millions of splats
colorizeRender with LOD colorization
fullloadLoad all streaming LOD data before the first frame
heatmapUse heatmap mode for the voxel collision debug overlay. Requires WebGPU and voxel collision data; press V or use the collision toolbar button to show the overlay.
debugOpen the developer debug panel on load (Ctrl+Shift+D to toggle)

NPM Package

Embedding the viewer

If you generate a page around the viewer, use renderViewerHtml. It returns a complete document, with your asset URLs and settings supplied through a single JSON block:

import { renderViewerHtml } from '@playcanvas/supersplat-viewer';

const document = renderViewerHtml({
    bootstrap: {
        settings,                      // an object, or omit and set settingsUrl
        contentUrl: 'scene.sog',
        posterUrl: 'poster.jpg'
    },
    baseHref: '/viewer/',              // serving from a sub-path
    backgroundColor: [0, 0, 0],        // components are 0..1, not 0..255
    headExtras: '<script src="analytics.js"></script>',
    inlineCss: true                    // no sibling index.css needed
});

Called with no options it returns the document the package ships, unmodified. URL parameters on the served page override the bootstrap's asset URLs, so an embed stays overridable per instance — except an inline settings object, which takes precedence over ?settings=.

Set both inlineCss and inlineJs for a single self-contained file, with the splat passed as a data: URI in contentUrl and the settings supplied inline through the bootstrap's settings object — without an inline settings object the page still fetches ./settings.json from a sibling file. A data: URI has no filename and the splat format is chosen by the name's extension, so name its content with the bootstrap's contentFilename (e.g. scene.sog). The flags are independent, so a server that serves the bundle from its own route can inline only the stylesheet.

html is still exported as a raw string, but is deprecated: its formatting is not part of this package's API and changes between releases, so pattern-matching it is unsupported. css and js remain exported for serving (or writing) the stylesheet and bundle alongside a rendered document that doesn't inline them.

Settings

The /settings subpath exports the schema types plus helpers for generating, validating and migrating a settings.json file:

import {
    defaultSettings,
    importSettings,
    validateSettings,
    POST_EFFECT_RANGES,
    type ExperienceSettings
} from '@playcanvas/supersplat-viewer/settings';

// a complete settings object every tool agrees on; pass 'object' to frame a subject
// from outside rather than a captured space from within
const settings: ExperienceSettings = defaultSettings();

// throws on invalid input, naming the offending field
validateSettings(json);

// additionally check the authoring bounds — stricter than what the viewer will render,
// so existing files may fail. Producers writing new settings should enable it
validateSettings(json, { limits: true });

// migrates older versions forward; does not mutate its argument
const migrated = importSettings(json);

// the bounds are data, so an editor UI can drive a slider from the same numbers
const { min, max, step } = POST_EFFECT_RANGES.bloom.intensity;

CAMERA_FOV_RANGE, POST_EFFECT_RANGES, ANIM_TRACK_LIMITS and ANNOTATION_LIMITS are exported as data, so an editor UI can drive sliders from the same bounds the validator uses. They are frozen at runtime.

Local Development

To initialize a local development environment for SuperSplat Viewer, ensure you have Node.js 20 or later installed. Follow these steps:

  1. Clone the repository:

    git clone https://github.com/playcanvas/supersplat-viewer.git
    cd supersplat-viewer
    
  2. Install dependencies:

    npm install
    
  3. Start the development build and local web server:

    npm run develop
    
  4. Open your browser at http://localhost:3000.

Debug engine build

By default the viewer links against the release build of the PlayCanvas engine. Set ENGINE=debug to link against the engine's debug build instead, which includes runtime assertions and unminified, readable source for easier debugging:

ENGINE=debug npm run develop

This also works with npm run build and npm run watch.

Settings Schema

The settings.json file uses the schema below (defined in TypeScript and exported from @playcanvas/supersplat-viewer/settings). Legacy v1 settings produced by older SuperSplat releases are automatically migrated to v2 on load.

type AnimTrack = {
    name: string,
    duration: number,
    frameRate: number,
    loopMode: 'none' | 'repeat' | 'pingpong',
    interpolation: 'step' | 'spline',
    smoothness: number,
    keyframes: {
        times: number[],
        values: {
            position: number[],
            target: number[],
            fov: number[],
        }
    }
};

type CameraPose = {
    position: [number, number, number],
    target: [number, number, number],
    fov: number
};

type Camera = {
    initial: CameraPose
};

type Annotation = {
    position: [number, number, number],
    title: string,
    text: string,
    extras?: any,
    camera: Camera
};

type PostEffectSettings = {
    sharpness: { enabled: boolean, amount: number },
    bloom:     { enabled: boolean, intensity: number, blurLevel: number },
    grading:   { enabled: boolean, brightness: number, contrast: number, saturation: number, tint: [number, number, number] },
    vignette:  { enabled: boolean, intensity: number, inner: number, outer: number, curvature: number },
    fringing:  { enabled: boolean, intensity: number }
};

type ExperienceSettings = {
    version: 2,
    tonemapping: 'none' | 'linear' | 'filmic' | 'hejl' | 'aces' | 'aces2' | 'neutral',
    highPrecisionRendering: boolean,
    soundUrl?: string,
    background: {
        color: [number, number, number],
        skyboxUrl?: string
    },
    postEffectSettings: PostEffectSettings,
    animTracks: AnimTrack[],
    cameras: Camera[],
    annotations: Annotation[],
    startMode: 'default' | 'animTrack' | 'annotation'
};

Example settings.json

{
    "version": 2,
    "tonemapping": "none",
    "highPrecisionRendering": false,
    "background": {
        "color": [0, 0, 0]
    },
    "postEffectSettings": {
        "sharpness": { "enabled": false, "amount": 0 },
        "bloom":     { "enabled": false, "intensity": 0.1, "blurLevel": 2 },
        "grading":   { "enabled": false, "brightness": 1, "contrast": 1, "saturation": 1, "tint": [1, 1, 1] },
        "vignette":  { "enabled": false, "intensity": 0.5, "inner": 0.3, "outer": 0.75, "curvature": 1 },
        "fringing":  { "enabled": false, "intensity": 0.5 }
    },
    "animTracks": [],
    "cameras": [
        {
            "initial": {
                "position": [0, 1, -1],
                "target": [0, 0, 0],
                "fov": 60
            }
        }
    ],
    "annotations": [],
    "startMode": "default"
}