files.ts
src/core/files.ts · raw · all examples · site home
Exports: SplatFormatSplatRequestInitSplatLoadOptionsSplatWriteOptionsSplatFiles
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
// Splat files to and from ArrayData on this thread: PLY (standard or
// compressed), SPZ, RAD and RDX,
// told apart by content, then by the name's extension. Every format loads
// into the bundle named after the asked layout (default extSplats, plus
// lodTree when the file has a tree): PLY, SPZ and RAD decode straight into
// it; RDX keeps its other bundles and converts its splats when they are in
// another layout. Spherical harmonics load into an SH bundle (core/sh.ts,
// `sh`: default ext; RDX keeps its stored codec unless `sh` is given), or,
// for RAD's clustered SH, into bundle shLabel and an ShPalette returned
// beside the splats. worker/splatFiles.ts runs these on a worker or here.
import type { ArrayData } from "./ArrayData";
import type { CoordinateSystem } from "./axes";
import { CompressedPlyFile } from "./compressedPly";
import { PlyFile } from "./ply";
import { readResponse, span, type Progress } from "./progress";
import { RAD_CHUNK_MAGIC, RAD_MAGIC, RadFile } from "./rad";
import { RDX_MAGIC, RdxFile } from "./rdx";
import { Sh, type ShPalette, type ShTarget } from "./sh";
import { Splats, type LoadedSplats, type SplatLayout, type SplatReadOptions } from "./splats";
import { SPZ_MAGIC, SpzFile } from "./spz";
export type SplatFormat = "ply" | "compressed-ply" | "spz" | "rad" | "rdx";
// fetch options for a splat file's URL (headers, credentials, mode, cache,
// referrer...); the load's own signal aborts it
export type SplatRequestInit = Omit<RequestInit, "signal" | "body" | "window" | "method">;
// `name` tells formats apart by extension when their first bytes do not
export type SplatLoadOptions<L extends SplatLayout = "extSplats"> = SplatReadOptions<L> & { name?: string };
// Options of every writer; each format takes the ones it knows (ply: pbr,
// compatibility; compressed-ply: sort; spz: version (4: SPZ 4's NGSP
// container), pbr, lod, fractionalBits, antialiased, coordinateSystem (the
// system version 4 records, default meta.coordinateSystem), zstdLevel
// (version 4, Node's zstd); rad:
// chunkSize, radSh; rdx: chunkSize; ply,
// compressed-ply, spz, rad: sh, default true, writes the SH bundle; all:
// shPalette, the palette of the data's shLabel bundle, written as RAD's
// clustered SH and expanded to per-splat SH by the others)
export interface SplatWriteOptions {
pbr?: boolean;
compatibility?: boolean;
sort?: boolean;
version?: 2 | 3 | 4;
coordinateSystem?: CoordinateSystem;
zstdLevel?: number;
lod?: boolean;
fractionalBits?: number;
antialiased?: boolean;
chunkSize?: number;
sh?: boolean;
radSh?: "s8" | "f16" | "f32";
shPalette?: ShPalette;
}
export class SplatFiles {
// The format of a file from its first bytes, else its name's extension;
// compressed PLY files are "ply" (PlyFile.read reads both)
static detectFormat(bytes: Uint8Array, name = ""): SplatFormat {
const magic = bytes.byteLength >= 4 ? new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(0, true) : 0;
if (magic === RDX_MAGIC) {
return "rdx";
}
if (magic === RAD_MAGIC || magic === RAD_CHUNK_MAGIC) {
return "rad";
}
if (bytes[0] === 0x70 && bytes[1] === 0x6c && bytes[2] === 0x79) {
return "ply";
}
const extension = name.toLowerCase().replace(/[?#].*$/, "").split(".").pop();
// NGSP: SPZ 4's plaintext header
if ((bytes[0] === 0x1f && bytes[1] === 0x8b) || magic === SPZ_MAGIC || extension === "spz") {
return "spz";
}
throw new Error(`Unknown splat file format${name ? `: ${name}` : ""}`);
}
static async decode<L extends SplatLayout = "extSplats">(bytes: Uint8Array<ArrayBuffer>, {
name = "", progress, layout = "extSplats" as L, encoding, sh, coordinateSystem,
}: SplatLoadOptions<L> = {}): Promise<LoadedSplats<L>> {
const options = { progress, layout, encoding, sh, coordinateSystem };
switch (SplatFiles.detectFormat(bytes, name)) {
case "rdx": return { splats: SplatFiles.withSh(Splats.convert(await RdxFile.decode(bytes, { progress }), { layout, encoding }), sh) };
case "rad": return RadFile.read(bytes, options);
case "ply":
case "compressed-ply": return PlyFile.read(bytes, options);
case "spz": return SpzFile.read(bytes, options);
}
}
// A table's SH as asked: kept when `sh` is undefined, dropped with false
// (meta.ignoredShDegree records the degree), else converted to the codec
static withSh<D>(data: D, sh: ShTarget | undefined): D {
const table = data as unknown as ArrayData;
const found = Sh.find(table.bundles);
if (sh === undefined || !found) {
return data;
}
if (sh === false) {
table.removeBundle(found.name);
table.meta.ignoredShDegree = found.degree;
return data;
}
return Sh.convert(table, sh) as unknown as D;
}
// A file of `data` in `format`: PLY, SPZ and RAD take its splat bundle
// (and lodTree, and SH), RDX every serialized bundle
static async encode(data: ArrayData, format: SplatFormat, options: SplatWriteOptions & { progress?: Progress } = {}): Promise<Uint8Array<ArrayBuffer>> {
if (options.shPalette && format !== "rad" && options.sh !== false) {
data = options.shPalette.expanded(data);
}
switch (format) {
case "ply": return PlyFile.write(data, options);
case "compressed-ply": return CompressedPlyFile.write(data, options);
case "spz": return SpzFile.write(data, options);
case "rad": return RadFile.write(data, options);
case "rdx": return RdxFile.encode(data, options);
}
}
// Download (0-40% of the progress) and decode
static async fetch<L extends SplatLayout = "extSplats">(url: string, { init, progress, ...target }: SplatReadOptions<L> & {
init?: RequestInit;
} = {}): Promise<LoadedSplats<L>> {
const response = await fetch(url, init);
if (!response.ok) {
throw new Error(`Failed to load ${url}: ${response.status}`);
}
const bytes = await readResponse(response, span(progress, 0, 0.4));
return SplatFiles.decode(bytes, { ...target, name: url, progress: span(progress, 0.4, 1) });
}
}