@@ -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