Use DecompressionStream for decompressing .spz files (#181)

This commit is contained in:
Noeri Huisman
2025-09-25 17:25:57 +02:00
committed by GitHub
parent 2defaf3f64
commit c6bb49d74d
3 changed files with 48 additions and 41 deletions
+30 -18
View File
@@ -18,21 +18,28 @@ export class SpzReader {
fileBytes: Uint8Array; fileBytes: Uint8Array;
reader: GunzipReader; reader: GunzipReader;
version: number; version = -1;
numSplats: number; numSplats = 0;
shDegree: number; shDegree = 0;
fractionalBits: number; fractionalBits = 0;
flags: number; flags = 0;
flagAntiAlias: boolean; flagAntiAlias = false;
reserved: number; reserved = 0;
parsed: boolean; headerParsed = false;
parsed = false;
constructor({ fileBytes }: { fileBytes: Uint8Array | ArrayBuffer }) { constructor({ fileBytes }: { fileBytes: Uint8Array | ArrayBuffer }) {
this.fileBytes = this.fileBytes =
fileBytes instanceof ArrayBuffer ? new Uint8Array(fileBytes) : fileBytes; fileBytes instanceof ArrayBuffer ? new Uint8Array(fileBytes) : fileBytes;
this.reader = new GunzipReader({ fileBytes: this.fileBytes }); this.reader = new GunzipReader({ fileBytes: this.fileBytes });
}
const header = new DataView(this.reader.read(16).buffer); async parseHeader() {
if (this.headerParsed) {
throw new Error("SPZ file header already parsed");
}
const header = new DataView((await this.reader.read(16)).buffer);
if (header.getUint32(0, true) !== 0x5053474e) { if (header.getUint32(0, true) !== 0x5053474e) {
throw new Error("Invalid SPZ file"); throw new Error("Invalid SPZ file");
} }
@@ -47,10 +54,11 @@ export class SpzReader {
this.flags = header.getUint8(14); this.flags = header.getUint8(14);
this.flagAntiAlias = (this.flags & 0x01) !== 0; this.flagAntiAlias = (this.flags & 0x01) !== 0;
this.reserved = header.getUint8(15); this.reserved = header.getUint8(15);
this.headerParsed = true;
this.parsed = false; this.parsed = false;
} }
parseSplats( async parseSplats(
centerCallback?: (index: number, x: number, y: number, z: number) => void, centerCallback?: (index: number, x: number, y: number, z: number) => void,
alphaCallback?: (index: number, alpha: number) => void, alphaCallback?: (index: number, alpha: number) => void,
rgbCallback?: (index: number, r: number, g: number, b: number) => void, rgbCallback?: (index: number, r: number, g: number, b: number) => void,
@@ -74,6 +82,9 @@ export class SpzReader {
sh3?: Float32Array, sh3?: Float32Array,
) => void, ) => void,
) { ) {
if (!this.headerParsed) {
throw new Error("SPZ file header must be parsed first");
}
if (this.parsed) { if (this.parsed) {
throw new Error("SPZ file already parsed"); throw new Error("SPZ file already parsed");
} }
@@ -81,7 +92,7 @@ export class SpzReader {
if (this.version === 1) { if (this.version === 1) {
// float16 centers // float16 centers
const centerBytes = this.reader.read(this.numSplats * 3 * 2); const centerBytes = await this.reader.read(this.numSplats * 3 * 2);
const centerUint16 = new Uint16Array(centerBytes.buffer); const centerUint16 = new Uint16Array(centerBytes.buffer);
for (let i = 0; i < this.numSplats; i++) { for (let i = 0; i < this.numSplats; i++) {
const i3 = i * 3; const i3 = i * 3;
@@ -93,7 +104,7 @@ export class SpzReader {
} else if (this.version === 2 || this.version === 3) { } else if (this.version === 2 || this.version === 3) {
// 24-bit fixed-point centers // 24-bit fixed-point centers
const fixed = 1 << this.fractionalBits; const fixed = 1 << this.fractionalBits;
const centerBytes = this.reader.read(this.numSplats * 3 * 3); const centerBytes = await this.reader.read(this.numSplats * 3 * 3);
for (let i = 0; i < this.numSplats; i++) { for (let i = 0; i < this.numSplats; i++) {
const i9 = i * 9; const i9 = i * 9;
const x = const x =
@@ -121,13 +132,13 @@ export class SpzReader {
} }
{ {
const bytes = this.reader.read(this.numSplats); const bytes = await this.reader.read(this.numSplats);
for (let i = 0; i < this.numSplats; i++) { for (let i = 0; i < this.numSplats; i++) {
alphaCallback?.(i, bytes[i] / 255); alphaCallback?.(i, bytes[i] / 255);
} }
} }
{ {
const rgbBytes = this.reader.read(this.numSplats * 3); const rgbBytes = await this.reader.read(this.numSplats * 3);
const scale = SH_C0 / 0.15; const scale = SH_C0 / 0.15;
for (let i = 0; i < this.numSplats; i++) { for (let i = 0; i < this.numSplats; i++) {
const i3 = i * 3; const i3 = i * 3;
@@ -138,7 +149,7 @@ export class SpzReader {
} }
} }
{ {
const scalesBytes = this.reader.read(this.numSplats * 3); const scalesBytes = await this.reader.read(this.numSplats * 3);
for (let i = 0; i < this.numSplats; i++) { for (let i = 0; i < this.numSplats; i++) {
const i3 = i * 3; const i3 = i * 3;
const scaleX = Math.exp(scalesBytes[i3] / 16 - 10); const scaleX = Math.exp(scalesBytes[i3] / 16 - 10);
@@ -160,7 +171,7 @@ export class SpzReader {
// v^2 + v^2 = 1 // v^2 + v^2 = 1
// v = 1 / sqrt(2); // v = 1 / sqrt(2);
const maxValue = 1 / Math.sqrt(2); // 0.7071 const maxValue = 1 / Math.sqrt(2); // 0.7071
const quatBytes = this.reader.read(this.numSplats * 4); const quatBytes = await this.reader.read(this.numSplats * 4);
for (let i = 0; i < this.numSplats; i++) { for (let i = 0; i < this.numSplats; i++) {
const i3 = i * 4; const i3 = i * 4;
const quaternion = [0, 0, 0, 0]; const quaternion = [0, 0, 0, 0];
@@ -211,7 +222,7 @@ export class SpzReader {
); );
} }
} else { } else {
const quatBytes = this.reader.read(this.numSplats * 3); const quatBytes = await this.reader.read(this.numSplats * 3);
for (let i = 0; i < this.numSplats; i++) { for (let i = 0; i < this.numSplats; i++) {
const i3 = i * 3; const i3 = i * 3;
const quatX = quatBytes[i3] / 127.5 - 1; const quatX = quatBytes[i3] / 127.5 - 1;
@@ -228,7 +239,7 @@ export class SpzReader {
const sh1 = new Float32Array(3 * 3); const sh1 = new Float32Array(3 * 3);
const sh2 = this.shDegree >= 2 ? new Float32Array(5 * 3) : undefined; const sh2 = this.shDegree >= 2 ? new Float32Array(5 * 3) : undefined;
const sh3 = this.shDegree >= 3 ? new Float32Array(7 * 3) : undefined; const sh3 = this.shDegree >= 3 ? new Float32Array(7 * 3) : undefined;
const shBytes = this.reader.read( const shBytes = await this.reader.read(
this.numSplats * SH_DEGREE_TO_VECS[this.shDegree] * 3, this.numSplats * SH_DEGREE_TO_VECS[this.shDegree] * 3,
); );
@@ -592,6 +603,7 @@ export async function transcodeSpz(input: TranscodeSpzInput) {
} }
case SplatFileType.SPZ: { case SplatFileType.SPZ: {
const spz = new SpzReader({ fileBytes: input.fileBytes }); const spz = new SpzReader({ fileBytes: input.fileBytes });
await spz.parseHeader();
const mapping = new Int32Array(spz.numSplats); const mapping = new Int32Array(spz.numSplats);
mapping.fill(-1); mapping.fill(-1);
const centers = new Float32Array(spz.numSplats * 3); const centers = new Float32Array(spz.numSplats * 3);
+12 -18
View File
@@ -1344,10 +1344,9 @@ export class GunzipReader {
fileBytes: Uint8Array; fileBytes: Uint8Array;
chunkBytes: number; chunkBytes: number;
offset: number;
chunks: Uint8Array[]; chunks: Uint8Array[];
totalBytes: number; totalBytes: number;
gunzip: Gunzip; reader: ReadableStreamDefaultReader;
constructor({ constructor({
fileBytes, fileBytes,
@@ -1355,28 +1354,23 @@ export class GunzipReader {
}: { fileBytes: Uint8Array; chunkBytes?: number }) { }: { fileBytes: Uint8Array; chunkBytes?: number }) {
this.fileBytes = fileBytes; this.fileBytes = fileBytes;
this.chunkBytes = chunkBytes; this.chunkBytes = chunkBytes;
this.offset = 0;
this.chunks = []; this.chunks = [];
this.totalBytes = 0; this.totalBytes = 0;
this.gunzip = new Gunzip((chunk, _final) => { const ds = new DecompressionStream("gzip");
this.chunks.push(chunk); const decompressionStream = new Blob([fileBytes]).stream().pipeThrough(ds);
this.totalBytes += chunk.length; this.reader = decompressionStream.getReader();
});
} }
read(numBytes: number): Uint8Array { async read(numBytes: number): Promise<Uint8Array> {
while (this.totalBytes < numBytes && this.offset < this.fileBytes.length) { while (this.totalBytes < numBytes) {
const end = Math.min( const { value: chunk, done: readerDone } = await this.reader.read();
this.offset + this.chunkBytes, if (readerDone) {
this.fileBytes.length, break;
); }
this.gunzip.push(this.fileBytes.subarray(this.offset, end), false);
this.offset = end;
}
if (this.totalBytes < numBytes && this.offset >= this.fileBytes.length) { this.chunks.push(chunk);
this.gunzip.push(new Uint8Array(0), true); this.totalBytes += chunk.length;
} }
if (this.totalBytes < numBytes) { if (this.totalBytes < numBytes) {
+6 -5
View File
@@ -61,7 +61,7 @@ async function onMessage(event: MessageEvent) {
fileBytes: Uint8Array; fileBytes: Uint8Array;
splatEncoding: SplatEncoding; splatEncoding: SplatEncoding;
}; };
const decoded = unpackSpz(fileBytes, splatEncoding); const decoded = await unpackSpz(fileBytes, splatEncoding);
result = { result = {
id, id,
numSplats: decoded.numSplats, numSplats: decoded.numSplats,
@@ -379,21 +379,22 @@ async function unpackPly({
return { packedArray, numSplats, extra }; return { packedArray, numSplats, extra };
} }
function unpackSpz( async function unpackSpz(
fileBytes: Uint8Array, fileBytes: Uint8Array,
splatEncoding: SplatEncoding, splatEncoding: SplatEncoding,
): { ): Promise<{
packedArray: Uint32Array; packedArray: Uint32Array;
numSplats: number; numSplats: number;
extra: Record<string, unknown>; extra: Record<string, unknown>;
} { }> {
const spz = new SpzReader({ fileBytes }); const spz = new SpzReader({ fileBytes });
await spz.parseHeader();
const numSplats = spz.numSplats; const numSplats = spz.numSplats;
const maxSplats = computeMaxSplats(numSplats); const maxSplats = computeMaxSplats(numSplats);
const packedArray = new Uint32Array(maxSplats * 4); const packedArray = new Uint32Array(maxSplats * 4);
const extra: Record<string, unknown> = {}; const extra: Record<string, unknown> = {};
spz.parseSplats( await spz.parseSplats(
(index, x, y, z) => { (index, x, y, z) => {
setPackedSplatCenter(packedArray, index, x, y, z); setPackedSplatCenter(packedArray, index, x, y, z);
}, },