Skip to content

Commit d0aed2a

Browse files
author
George Ash
committed
Add SPZ v3, largest 3 quaternion encoding/decoding support
1 parent b454b36 commit d0aed2a

2 files changed

Lines changed: 83 additions & 38 deletions

File tree

src/spz.ts

Lines changed: 77 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
getSplatFileType,
77
getSplatFileTypeFromPath,
88
} from "./SplatLoader";
9-
import { GunzipReader, fromHalf, unpackSplat } from "./utils";
9+
import { GunzipReader, fromHalf, normalize, unpackSplat } from "./utils";
1010

1111
import { decodeAntiSplat } from "./antisplat";
1212
import { decodeKsplat } from "./ksplat";
@@ -37,7 +37,7 @@ export class SpzReader {
3737
throw new Error("Invalid SPZ file");
3838
}
3939
this.version = header.getUint32(4, true);
40-
if (this.version < 1 || this.version > 2) {
40+
if (this.version < 1 || this.version > 3) {
4141
throw new Error(`Unsupported SPZ version: ${this.version}`);
4242
}
4343

@@ -90,7 +90,7 @@ export class SpzReader {
9090
const z = fromHalf(centerUint16[i3 + 2]);
9191
centerCallback?.(i, x, y, z);
9292
}
93-
} else if (this.version === 2) {
93+
} else if (this.version >= 2) {
9494
// 24-bit fixed-point centers
9595
const fixed = 1 << this.fractionalBits;
9696
const centerBytes = this.reader.read(this.numSplats * 3 * 3);
@@ -147,7 +147,7 @@ export class SpzReader {
147147
scalesCallback?.(i, scaleX, scaleY, scaleZ);
148148
}
149149
}
150-
{
150+
if (this.version < 3) {
151151
const quatBytes = this.reader.read(this.numSplats * 3);
152152
for (let i = 0; i < this.numSplats; i++) {
153153
const i3 = i * 3;
@@ -159,6 +159,39 @@ export class SpzReader {
159159
);
160160
quatCallback?.(i, quatX, quatY, quatZ, quatW);
161161
}
162+
} else {
163+
const quatBytes = this.reader.read(this.numSplats * 4);
164+
165+
//largest three decode
166+
for (let i = 0; i < this.numSplats; i++) {
167+
const rotation: [number, number, number, number] = [0, 0, 0, 0];
168+
const i4 = i * 4;
169+
let comp =
170+
quatBytes[i4] +
171+
(quatBytes[i4 + 1] << 8) +
172+
(quatBytes[i4 + 2] << 16) +
173+
(quatBytes[i4 + 3] << 24);
174+
const c_mask = (1 << 9) - 1;
175+
const i_largest = comp >>> 30;
176+
let sum_squares = 0;
177+
178+
for (let i = 3; i >= 0; --i) {
179+
if (i !== i_largest) {
180+
const mag = comp & c_mask;
181+
const negbit = (comp >>> 9) & 0x1;
182+
comp = comp >>> 10;
183+
184+
rotation[i] = (Math.SQRT1_2 * mag) / c_mask;
185+
186+
if (negbit === 1) {
187+
rotation[i] = -rotation[i];
188+
}
189+
sum_squares += rotation[i] * rotation[i];
190+
}
191+
}
192+
rotation[i_largest] = Math.sqrt(1.0 - sum_squares);
193+
quatCallback?.(i, ...rotation);
194+
}
162195
}
163196

164197
if (shCallback && this.shDegree >= 1) {
@@ -197,7 +230,7 @@ const SH_DEGREE_TO_VECS: Record<number, number> = { 1: 3, 2: 8, 3: 15 };
197230
const SH_C0 = 0.28209479177387814;
198231

199232
export const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
200-
export const SPZ_VERSION = 2;
233+
export const SPZ_VERSION = 3;
201234
export const FLAG_ANTIALIASED = 0x1;
202235

203236
export class SpzWriter {
@@ -222,11 +255,11 @@ export class SpzWriter {
222255
flagAntiAlias?: boolean;
223256
}) {
224257
const splatSize =
225-
9 +
226-
1 +
227-
3 +
228-
3 +
229-
3 +
258+
9 + // Position
259+
1 + // Opacity
260+
3 + // Scale
261+
3 + // DC-rgb
262+
4 + // Rotation
230263
(shDegree >= 1 ? 9 : 0) +
231264
(shDegree >= 2 ? 15 : 0) +
232265
(shDegree >= 3 ? 21 : 0);
@@ -317,34 +350,40 @@ export class SpzWriter {
317350

318351
setQuat(
319352
index: number,
320-
quatX: number,
321-
quatY: number,
322-
quatZ: number,
323-
quatW: number,
353+
...q: [number, number, number, number] // x, y, z, w
324354
) {
325-
const base = 16 + this.numSplats * 16 + index * 3;
326-
const quatNeg = quatW < 0;
327-
this.view.setUint8(
328-
base,
329-
Math.max(
330-
0,
331-
Math.min(255, Math.round(((quatNeg ? -quatX : quatX) + 1) * 127.5)),
332-
),
333-
);
334-
this.view.setUint8(
335-
base + 1,
336-
Math.max(
337-
0,
338-
Math.min(255, Math.round(((quatNeg ? -quatY : quatY) + 1) * 127.5)),
339-
),
340-
);
341-
this.view.setUint8(
342-
base + 2,
343-
Math.max(
344-
0,
345-
Math.min(255, Math.round(((quatNeg ? -quatZ : quatZ) + 1) * 127.5)),
346-
),
347-
);
355+
const base = 16 + this.numSplats * 16 + index * 4;
356+
357+
const quat = normalize(q);
358+
359+
// Find largest component
360+
let iLargest = 0;
361+
for (let i = 1; i < 4; ++i) {
362+
if (Math.abs(quat[i]) > Math.abs(quat[iLargest])) {
363+
iLargest = i;
364+
}
365+
}
366+
367+
// Since -quat represents the same rotation as quat, transform the quaternion so the largest element
368+
// is positive. This avoids having to send its sign bit.
369+
const negate = quat[iLargest] < 0 ? 1 : 0;
370+
371+
// Do compression using sign bit and 9-bit precision per element.
372+
let comp = iLargest;
373+
for (let i = 0; i < 4; ++i) {
374+
if (i !== iLargest) {
375+
const negbit = (quat[i] < 0 ? 1 : 0) ^ negate;
376+
const mag = Math.floor(
377+
((1 << 9) - 1) * (Math.abs(quat[i]) / Math.SQRT1_2) + 0.5,
378+
);
379+
comp = (comp << 10) | (negbit << 9) | mag;
380+
}
381+
}
382+
383+
this.view.setUint8(base, comp & 0xff);
384+
this.view.setUint8(base + 1, (comp >> 8) & 0xff);
385+
this.view.setUint8(base + 2, (comp >> 16) & 0xff);
386+
this.view.setUint8(base + 3, (comp >>> 24) & 0xff);
348387
}
349388

350389
static quantizeSh(sh: number, bits: number) {
@@ -362,7 +401,7 @@ export class SpzWriter {
362401
sh3?: Float32Array,
363402
) {
364403
const shVecs = SH_DEGREE_TO_VECS[this.shDegree] || 0;
365-
const base1 = 16 + this.numSplats * 19 + index * shVecs * 3;
404+
const base1 = 16 + this.numSplats * 20 + index * shVecs * 3;
366405
for (let j = 0; j < 9; ++j) {
367406
this.view.setUint8(base1 + j, SpzWriter.quantizeSh(sh1[j], 5));
368407
}

src/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ import { unindent } from "./dyno/base.js";
1616
const f32buffer = new Float32Array(1);
1717
const u32buffer = new Uint32Array(f32buffer.buffer);
1818

19+
// Returns a normalized array of numbers
20+
export function normalize(vec: number[]) {
21+
const norm = Math.sqrt(vec.reduce((acc, v) => acc + v * v, 0));
22+
return vec.map((v) => v / norm);
23+
}
24+
1925
// Reinterpret the bits of a float32 as a uint32
2026
export function floatBitsToUint(f: number): number {
2127
f32buffer[0] = f;

0 commit comments

Comments
 (0)