Skip to content

Commit ccd2dd1

Browse files
gwegashGeorge Ash
andauthored
Add SPZ v3 smallest 3 quaternion encoding support (#171)
Co-authored-by: George Ash <george@nianticspatial.com>
1 parent bd4ac36 commit ccd2dd1

2 files changed

Lines changed: 48 additions & 38 deletions

File tree

src/spz.ts

Lines changed: 42 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";
@@ -149,9 +149,7 @@ export class SpzReader {
149149
}
150150
if (this.version === 3) {
151151
// Version 3 uses a trick called "smallest three" to compress the rotation quaternions
152-
// achieving better precision.
153-
// "Optimizing orientation" section at https://gafferongames.com/post/snapshot_compression/
154-
// A quaternion length must be 1: x^2+y^2+z^2+w^2 = 1
152+
// achieving better precision. "Optimizing orientation" section at https://gafferongames.com/post/snapshot_compression/ A quaternion length must be 1: x^2+y^2+z^2+w^2 = 1
155153
// We can drop one component and reconstruct it with the identity above.
156154
// Largest component is dropped for best numerical precision.
157155
// Quaternion stored in 32 bits
@@ -262,7 +260,7 @@ const SH_DEGREE_TO_VECS: Record<number, number> = { 1: 3, 2: 8, 3: 15 };
262260
const SH_C0 = 0.28209479177387814;
263261

264262
export const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
265-
export const SPZ_VERSION = 2;
263+
export const SPZ_VERSION = 3;
266264
export const FLAG_ANTIALIASED = 0x1;
267265

268266
export class SpzWriter {
@@ -287,11 +285,11 @@ export class SpzWriter {
287285
flagAntiAlias?: boolean;
288286
}) {
289287
const splatSize =
290-
9 +
291-
1 +
292-
3 +
293-
3 +
294-
3 +
288+
9 + // Position
289+
1 + // Opacity
290+
3 + // Scale
291+
3 + // DC-rgb
292+
4 + // Rotation
295293
(shDegree >= 1 ? 9 : 0) +
296294
(shDegree >= 2 ? 15 : 0) +
297295
(shDegree >= 3 ? 21 : 0);
@@ -382,34 +380,40 @@ export class SpzWriter {
382380

383381
setQuat(
384382
index: number,
385-
quatX: number,
386-
quatY: number,
387-
quatZ: number,
388-
quatW: number,
383+
...q: [number, number, number, number] // x, y, z, w
389384
) {
390-
const base = 16 + this.numSplats * 16 + index * 3;
391-
const quatNeg = quatW < 0;
392-
this.view.setUint8(
393-
base,
394-
Math.max(
395-
0,
396-
Math.min(255, Math.round(((quatNeg ? -quatX : quatX) + 1) * 127.5)),
397-
),
398-
);
399-
this.view.setUint8(
400-
base + 1,
401-
Math.max(
402-
0,
403-
Math.min(255, Math.round(((quatNeg ? -quatY : quatY) + 1) * 127.5)),
404-
),
405-
);
406-
this.view.setUint8(
407-
base + 2,
408-
Math.max(
409-
0,
410-
Math.min(255, Math.round(((quatNeg ? -quatZ : quatZ) + 1) * 127.5)),
411-
),
412-
);
385+
const base = 16 + this.numSplats * 16 + index * 4;
386+
387+
const quat = normalize(q);
388+
389+
// Find largest component
390+
let iLargest = 0;
391+
for (let i = 1; i < 4; ++i) {
392+
if (Math.abs(quat[i]) > Math.abs(quat[iLargest])) {
393+
iLargest = i;
394+
}
395+
}
396+
397+
// Since -quat represents the same rotation as quat, transform the quaternion so the largest element
398+
// is positive. This avoids having to send its sign bit.
399+
const negate = quat[iLargest] < 0 ? 1 : 0;
400+
401+
// Do compression using sign bit and 9-bit precision per element.
402+
let comp = iLargest;
403+
for (let i = 0; i < 4; ++i) {
404+
if (i !== iLargest) {
405+
const negbit = (quat[i] < 0 ? 1 : 0) ^ negate;
406+
const mag = Math.floor(
407+
((1 << 9) - 1) * (Math.abs(quat[i]) / Math.SQRT1_2) + 0.5,
408+
);
409+
comp = (comp << 10) | (negbit << 9) | mag;
410+
}
411+
}
412+
413+
this.view.setUint8(base, comp & 0xff);
414+
this.view.setUint8(base + 1, (comp >> 8) & 0xff);
415+
this.view.setUint8(base + 2, (comp >> 16) & 0xff);
416+
this.view.setUint8(base + 3, (comp >>> 24) & 0xff);
413417
}
414418

415419
static quantizeSh(sh: number, bits: number) {
@@ -427,7 +431,7 @@ export class SpzWriter {
427431
sh3?: Float32Array,
428432
) {
429433
const shVecs = SH_DEGREE_TO_VECS[this.shDegree] || 0;
430-
const base1 = 16 + this.numSplats * 19 + index * shVecs * 3;
434+
const base1 = 16 + this.numSplats * 20 + index * shVecs * 3;
431435
for (let j = 0; j < 9; ++j) {
432436
this.view.setUint8(base1 + j, SpzWriter.quantizeSh(sh1[j], 5));
433437
}

src/utils.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ const f16buffer = supportsFloat16Array
2121
: null;
2222
const u16buffer = new Uint16Array(f16buffer?.buffer);
2323

24+
// Returns a normalized array of numbers
25+
export function normalize(vec: number[]) {
26+
const norm = Math.sqrt(vec.reduce((acc, v) => acc + v * v, 0));
27+
return vec.map((v) => v / norm);
28+
}
29+
2430
// Reinterpret the bits of a float32 as a uint32
2531
export function floatBitsToUint(f: number): number {
2632
f32buffer[0] = f;

0 commit comments

Comments
 (0)