mirror of
https://github.com/storytold/spark.git
synced 2026-10-09 00:09:53 +00:00
Add SPZ v3 smallest 3 quaternion encoding support (#171)
Co-authored-by: George Ash <george@nianticspatial.com>
This commit is contained in:
+42
-38
@@ -6,7 +6,7 @@ import {
|
|||||||
getSplatFileType,
|
getSplatFileType,
|
||||||
getSplatFileTypeFromPath,
|
getSplatFileTypeFromPath,
|
||||||
} from "./SplatLoader";
|
} from "./SplatLoader";
|
||||||
import { GunzipReader, fromHalf, unpackSplat } from "./utils";
|
import { GunzipReader, fromHalf, normalize, unpackSplat } from "./utils";
|
||||||
|
|
||||||
import { decodeAntiSplat } from "./antisplat";
|
import { decodeAntiSplat } from "./antisplat";
|
||||||
import { decodeKsplat } from "./ksplat";
|
import { decodeKsplat } from "./ksplat";
|
||||||
@@ -149,9 +149,7 @@ export class SpzReader {
|
|||||||
}
|
}
|
||||||
if (this.version === 3) {
|
if (this.version === 3) {
|
||||||
// Version 3 uses a trick called "smallest three" to compress the rotation quaternions
|
// Version 3 uses a trick called "smallest three" to compress the rotation quaternions
|
||||||
// achieving better precision.
|
// 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
|
||||||
// "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
|
|
||||||
// We can drop one component and reconstruct it with the identity above.
|
// We can drop one component and reconstruct it with the identity above.
|
||||||
// Largest component is dropped for best numerical precision.
|
// Largest component is dropped for best numerical precision.
|
||||||
// Quaternion stored in 32 bits
|
// Quaternion stored in 32 bits
|
||||||
@@ -262,7 +260,7 @@ const SH_DEGREE_TO_VECS: Record<number, number> = { 1: 3, 2: 8, 3: 15 };
|
|||||||
const SH_C0 = 0.28209479177387814;
|
const SH_C0 = 0.28209479177387814;
|
||||||
|
|
||||||
export const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
|
export const SPZ_MAGIC = 0x5053474e; // NGSP = Niantic gaussian splat
|
||||||
export const SPZ_VERSION = 2;
|
export const SPZ_VERSION = 3;
|
||||||
export const FLAG_ANTIALIASED = 0x1;
|
export const FLAG_ANTIALIASED = 0x1;
|
||||||
|
|
||||||
export class SpzWriter {
|
export class SpzWriter {
|
||||||
@@ -287,11 +285,11 @@ export class SpzWriter {
|
|||||||
flagAntiAlias?: boolean;
|
flagAntiAlias?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const splatSize =
|
const splatSize =
|
||||||
9 +
|
9 + // Position
|
||||||
1 +
|
1 + // Opacity
|
||||||
3 +
|
3 + // Scale
|
||||||
3 +
|
3 + // DC-rgb
|
||||||
3 +
|
4 + // Rotation
|
||||||
(shDegree >= 1 ? 9 : 0) +
|
(shDegree >= 1 ? 9 : 0) +
|
||||||
(shDegree >= 2 ? 15 : 0) +
|
(shDegree >= 2 ? 15 : 0) +
|
||||||
(shDegree >= 3 ? 21 : 0);
|
(shDegree >= 3 ? 21 : 0);
|
||||||
@@ -382,34 +380,40 @@ export class SpzWriter {
|
|||||||
|
|
||||||
setQuat(
|
setQuat(
|
||||||
index: number,
|
index: number,
|
||||||
quatX: number,
|
...q: [number, number, number, number] // x, y, z, w
|
||||||
quatY: number,
|
|
||||||
quatZ: number,
|
|
||||||
quatW: number,
|
|
||||||
) {
|
) {
|
||||||
const base = 16 + this.numSplats * 16 + index * 3;
|
const base = 16 + this.numSplats * 16 + index * 4;
|
||||||
const quatNeg = quatW < 0;
|
|
||||||
this.view.setUint8(
|
const quat = normalize(q);
|
||||||
base,
|
|
||||||
Math.max(
|
// Find largest component
|
||||||
0,
|
let iLargest = 0;
|
||||||
Math.min(255, Math.round(((quatNeg ? -quatX : quatX) + 1) * 127.5)),
|
for (let i = 1; i < 4; ++i) {
|
||||||
),
|
if (Math.abs(quat[i]) > Math.abs(quat[iLargest])) {
|
||||||
);
|
iLargest = i;
|
||||||
this.view.setUint8(
|
}
|
||||||
base + 1,
|
}
|
||||||
Math.max(
|
|
||||||
0,
|
// Since -quat represents the same rotation as quat, transform the quaternion so the largest element
|
||||||
Math.min(255, Math.round(((quatNeg ? -quatY : quatY) + 1) * 127.5)),
|
// is positive. This avoids having to send its sign bit.
|
||||||
),
|
const negate = quat[iLargest] < 0 ? 1 : 0;
|
||||||
);
|
|
||||||
this.view.setUint8(
|
// Do compression using sign bit and 9-bit precision per element.
|
||||||
base + 2,
|
let comp = iLargest;
|
||||||
Math.max(
|
for (let i = 0; i < 4; ++i) {
|
||||||
0,
|
if (i !== iLargest) {
|
||||||
Math.min(255, Math.round(((quatNeg ? -quatZ : quatZ) + 1) * 127.5)),
|
const negbit = (quat[i] < 0 ? 1 : 0) ^ negate;
|
||||||
),
|
const mag = Math.floor(
|
||||||
);
|
((1 << 9) - 1) * (Math.abs(quat[i]) / Math.SQRT1_2) + 0.5,
|
||||||
|
);
|
||||||
|
comp = (comp << 10) | (negbit << 9) | mag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.view.setUint8(base, comp & 0xff);
|
||||||
|
this.view.setUint8(base + 1, (comp >> 8) & 0xff);
|
||||||
|
this.view.setUint8(base + 2, (comp >> 16) & 0xff);
|
||||||
|
this.view.setUint8(base + 3, (comp >>> 24) & 0xff);
|
||||||
}
|
}
|
||||||
|
|
||||||
static quantizeSh(sh: number, bits: number) {
|
static quantizeSh(sh: number, bits: number) {
|
||||||
@@ -427,7 +431,7 @@ export class SpzWriter {
|
|||||||
sh3?: Float32Array,
|
sh3?: Float32Array,
|
||||||
) {
|
) {
|
||||||
const shVecs = SH_DEGREE_TO_VECS[this.shDegree] || 0;
|
const shVecs = SH_DEGREE_TO_VECS[this.shDegree] || 0;
|
||||||
const base1 = 16 + this.numSplats * 19 + index * shVecs * 3;
|
const base1 = 16 + this.numSplats * 20 + index * shVecs * 3;
|
||||||
for (let j = 0; j < 9; ++j) {
|
for (let j = 0; j < 9; ++j) {
|
||||||
this.view.setUint8(base1 + j, SpzWriter.quantizeSh(sh1[j], 5));
|
this.view.setUint8(base1 + j, SpzWriter.quantizeSh(sh1[j], 5));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ const f16buffer = supportsFloat16Array
|
|||||||
: null;
|
: null;
|
||||||
const u16buffer = new Uint16Array(f16buffer?.buffer);
|
const u16buffer = new Uint16Array(f16buffer?.buffer);
|
||||||
|
|
||||||
|
// Returns a normalized array of numbers
|
||||||
|
export function normalize(vec: number[]) {
|
||||||
|
const norm = Math.sqrt(vec.reduce((acc, v) => acc + v * v, 0));
|
||||||
|
return vec.map((v) => v / norm);
|
||||||
|
}
|
||||||
|
|
||||||
// Reinterpret the bits of a float32 as a uint32
|
// Reinterpret the bits of a float32 as a uint32
|
||||||
export function floatBitsToUint(f: number): number {
|
export function floatBitsToUint(f: number): number {
|
||||||
f32buffer[0] = f;
|
f32buffer[0] = f;
|
||||||
|
|||||||
Reference in New Issue
Block a user