66 getSplatFileType ,
77 getSplatFileTypeFromPath ,
88} from "./SplatLoader" ;
9- import { GunzipReader , fromHalf , unpackSplat } from "./utils" ;
9+ import { GunzipReader , fromHalf , normalize , unpackSplat } from "./utils" ;
1010
1111import { decodeAntiSplat } from "./antisplat" ;
1212import { 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 };
197230const SH_C0 = 0.28209479177387814 ;
198231
199232export const SPZ_MAGIC = 0x5053474e ; // NGSP = Niantic gaussian splat
200- export const SPZ_VERSION = 2 ;
233+ export const SPZ_VERSION = 3 ;
201234export const FLAG_ANTIALIASED = 0x1 ;
202235
203236export 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 }
0 commit comments