Skip to content

Commit de9388d

Browse files
committed
Add spz v3 loading support
1 parent eba41ee commit de9388d

1 file changed

Lines changed: 68 additions & 3 deletions

File tree

src/spz.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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 || this.version === 3) {
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,72 @@ export class SpzReader {
147147
scalesCallback?.(i, scaleX, scaleY, scaleZ);
148148
}
149149
}
150-
{
150+
if (this.version === 3) {
151+
// 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
155+
// We can drop one component and reconstruct it with the identity above.
156+
// Largest component is dropped for best numerical precision.
157+
// Quaternion stored in 32 bits
158+
// 10 bits singed integer for each of the 3 components + 2 bits indicating the index of dropped component.
159+
// vs 8 bits for each component uncompressed (spz version < 3)
160+
// Max Value after extracting largest component v is another component v
161+
// (v,v,0,0)
162+
// v^2 + v^2 = 1
163+
// v = 1 / sqrt(2);
164+
const maxValue = 1 / Math.sqrt(2); // 0.7071
165+
const quatBytes = this.reader.read(this.numSplats * 4);
166+
for (let i = 0; i < this.numSplats; i++) {
167+
const i3 = i * 4;
168+
const quaternion = [0, 0, 0, 0];
169+
const values = [
170+
quatBytes[i3],
171+
quatBytes[i3 + 1],
172+
quatBytes[i3 + 2],
173+
quatBytes[i3 + 3],
174+
];
175+
// all values are packed in 32 bits (10 per each of 3 components + 2 bits of index of larged value)
176+
const combinedValues =
177+
values[0] + (values[1] << 8) + (values[2] << 16) + (values[3] << 24);
178+
// each component value is 9 bits + sign (1 bit)
179+
const valueMask = (1 << 9) - 1;
180+
// extract index of the largest element. 2 top bits.
181+
const largestIndex = combinedValues >>> 30;
182+
let remainingValues = combinedValues;
183+
let sumSquares = 0;
184+
185+
for (let i = 3; i >= 0; --i) {
186+
if (i !== largestIndex) {
187+
// extract current value and sign.
188+
const value = remainingValues & valueMask;
189+
const sign = (remainingValues >>> 9) & 0x1;
190+
// each value is represented as 10 bits. Shift to next one.
191+
remainingValues = remainingValues >>> 10;
192+
// convert to range [0,1] and then to [0, 0.7071]
193+
quaternion[i] = maxValue * (value / valueMask);
194+
// apply sign.
195+
quaternion[i] = sign === 0 ? quaternion[i] : -quaternion[i];
196+
// accumulate the sum of squares
197+
sumSquares += quaternion[i] * quaternion[i];
198+
}
199+
}
200+
201+
// quartenion length must be 1 (x^2+y^2+z^2+w^2 = 1)
202+
// so can reconstruct largest component from the other 3.
203+
// w = sqrt(1 - x^2 - y^2 - z^2);
204+
const square = 1 - sumSquares;
205+
quaternion[largestIndex] = Math.sqrt(Math.max(square, 0));
206+
207+
quatCallback?.(
208+
i,
209+
quaternion[0],
210+
quaternion[1],
211+
quaternion[2],
212+
quaternion[3],
213+
);
214+
}
215+
} else {
151216
const quatBytes = this.reader.read(this.numSplats * 3);
152217
for (let i = 0; i < this.numSplats; i++) {
153218
const i3 = i * 3;

0 commit comments

Comments
 (0)