Skip to main content

Decode in a browser

Two packages: @4dgs/core decodes, @4dgs/browser fetches. The core depends on neither the network nor the filesystem — it takes anything that can report its size and read a byte range — so the code below is the same code that runs on a server and in a test over a byte array.

See the feature matrix for what the conformance suite proves today.

Scope

This guide ends where decoding ends: typed arrays of gaussian state at a time t, ready to hand to whatever draws them. Rendering is out of scope for this project — see AGENTS.md.

A whole scene

import { decodeScene } from "@4dgs/core";
import { HttpRangeReadable } from "@4dgs/browser";

const scene = await decodeScene(new HttpRangeReadable("https://example.com/scene.4dgs"));

console.log(scene.gaussians.count, scene.header.durationSec);

const state = scene.gaussians.stateAt(1.5, scene.header.cutoff);
// state.indices — which gaussians exist at t = 1.5
// state.centers — their positions, moved by their own velocity
// state.opacity — their alpha, faded by their own marginal

decodeScene reads front to back in bounded blocks and never holds the resource in one piece. A file that was cut short still decodes: everything complete before the cut is returned, and scene.truncated says the file ended early rather than throwing it away.

The audio track, if the scene has one, is scene.audio. Most scenes have none, and then it is null — a value, not an error. The header answers that question on its own, with no extra request.

One instant, not the whole file

A scene fitted from a capture partitions into chunks with their own time ranges, so displaying an instant reads the index and then only that instant's byte ranges.

import { IndexedDecoder } from "@4dgs/core";
import { HttpRangeReadable } from "@4dgs/browser";

const scene = await IndexedDecoder.open(new HttpRangeReadable(url));

console.log(scene.bytesForTime(1.5), "bytes to display t = 1.5");

for (const entry of scene.chunksForTime(1.5)) {
const { gaussians } = await scene.readChunk(entry, { maxShBand: 1 });
}

bytesForTime is worth asking before readChunk. Whether an instant is cheap to reach is a property of the content, not of the container: gaussians with finite validity windows partition into many small chunks, while content baked from a temporal field can leave the index with a single entry that covers the whole timeline. Both are conforming files. Only one of them seeks cheaply, and a consumer deserves to find out from a number rather than from a download.

maxShBand is the same idea for appearance. Each spherical harmonic band has its own byte range, so a viewer that has decided to evaluate one band never transfers the others.

A local file

import { BlobReadable } from "@4dgs/browser";

const scene = await decodeScene(new BlobReadable(fileInput.files[0]));

Codecs

deflate is the format's default and needs nothing: the core decodes it with DecompressionStream. A file using zstd needs a decompressor supplied explicitly, which keeps it out of bundles that will never meet one.

import { DEFAULT_CODECS, decodeScene } from "@4dgs/core";
import { withZstd } from "@4dgs/codecs";

const scene = await decodeScene(readable, { codecs: withZstd(DEFAULT_CODECS) });