UNPKG

three

Version:

JavaScript 3D library

464 lines (329 loc) 14.3 kB
import { DataUtils, FileLoader, Loader } from 'three'; import { SH_BAND_COMPONENTS, SH_BAND_WORDS, createGaussianSplatGeometry, createPackedSphericalHarmonicsBand, writeColorBytes, writeCovariance } from '../utils/GaussianSplatUtils.js'; const HEADER_SIZE_BYTES = 4096; const SECTION_HEADER_SIZE_BYTES = 1024; const CURRENT_VERSION_MAJOR = 0; const CURRENT_VERSION_MINOR = 1; const MAX_SPLATS = 10000000; const SH_DEGREE_TO_COMPONENTS = [ 0, 9, 24, 45 ]; const SH_BAND_INDEX = [ null, [ 0, 3, 6, 1, 4, 7, 2, 5, 8 ], [ 9, 14, 19, 10, 15, 20, 11, 16, 21, 12, 17, 22, 13, 18, 23 ], [ 24, 31, 38, 25, 32, 39, 26, 33, 40, 27, 34, 41, 28, 35, 42, 29, 36, 43, 30, 37, 44 ] ]; const COMPRESSION_LEVELS = { 0: { bytesPerCenter: 12, bytesPerScale: 12, bytesPerRotation: 16, bytesPerColor: 4, bytesPerSphericalHarmonicsComponent: 4, scaleOffsetBytes: 12, rotationOffsetBytes: 24, colorOffsetBytes: 40, scaleRange: 1 }, 1: { bytesPerCenter: 6, bytesPerScale: 6, bytesPerRotation: 8, bytesPerColor: 4, bytesPerSphericalHarmonicsComponent: 2, scaleOffsetBytes: 6, rotationOffsetBytes: 12, colorOffsetBytes: 20, scaleRange: 32767 }, 2: { bytesPerCenter: 6, bytesPerScale: 6, bytesPerRotation: 8, bytesPerColor: 4, bytesPerSphericalHarmonicsComponent: 1, scaleOffsetBytes: 6, rotationOffsetBytes: 12, colorOffsetBytes: 20, scaleRange: 32767 } }; /** * A loader for GaussianSplats3D `.ksplat` files. * * This loader decodes the format into `BufferGeometry` for use with * `GaussianSplat`. Higher-order spherical harmonics are exposed as optional * `sphericalHarmonics1` through `sphericalHarmonics3` packed uint32 geometry * attributes (`SH_BAND_WORDS[ degree ]` words per splat). Coefficients use the * clamped-byte encoding `( value - 128 ) / 128`, four bytes per word. * * ```js * const loader = new KSPLATLoader(); * const data = await loader.loadAsync( './models/gsplat/example.ksplat' ); * scene.add( new GaussianSplat( data ) ); * ``` * * @augments Loader * @three_import import { KSPLATLoader } from 'three/addons/loaders/KSPLATLoader.js'; */ class KSPLATLoader extends Loader { /** * Constructs a new Gaussian splat KSPLAT loader. * * @param {LoadingManager} [manager] - The loading manager. */ constructor( manager ) { super( manager ); } /** * Starts loading from the given URL and passes the loaded splat data to * the `onLoad()` callback. * * @param {string} url - The path/URL of the file to be loaded. This can also be a data URI. * @param {function(BufferGeometry)} onLoad - Executed when the loading process has been finished. * @param {onProgressCallback} onProgress - Executed while the loading is in progress. * @param {onErrorCallback} onError - Executed when errors occur. */ load( url, onLoad, onProgress, onError ) { const scope = this; const loader = new FileLoader( this.manager ); loader.setPath( this.path ); loader.setResponseType( 'arraybuffer' ); loader.setRequestHeader( this.requestHeader ); loader.setWithCredentials( this.withCredentials ); loader.load( url, function ( buffer ) { try { onLoad( scope.parse( buffer ) ); } catch ( e ) { if ( onError ) { onError( e ); } else { console.error( e ); } scope.manager.itemError( url ); } }, onProgress, onError ); } /** * Parses the given `.ksplat` data. * * @param {ArrayBuffer} buffer - The raw KSPLAT file as an array buffer. * @return {BufferGeometry} The parsed splat geometry. */ parse( buffer ) { if ( buffer.byteLength < HEADER_SIZE_BYTES ) { throw new Error( 'THREE.KSPLATLoader: Invalid KSPLAT header.' ); } const bytes = new Uint8Array( buffer ); const view = new DataView( buffer ); const header = parseHeader( view ); if ( header.versionMajor !== CURRENT_VERSION_MAJOR || header.versionMinor < CURRENT_VERSION_MINOR ) { throw new Error( `THREE.KSPLATLoader: Unsupported KSPLAT version ${ header.versionMajor }.${ header.versionMinor }.` ); } if ( header.compressionLevel < 0 || header.compressionLevel > 2 ) { throw new Error( `THREE.KSPLATLoader: Unsupported KSPLAT compression level ${ header.compressionLevel }.` ); } if ( header.splatCount > MAX_SPLATS ) { throw new Error( `THREE.KSPLATLoader: KSPLAT file contains too many splats (${ header.splatCount }).` ); } const sectionHeadersOffset = HEADER_SIZE_BYTES; const sectionDataOffset = HEADER_SIZE_BYTES + header.maxSectionCount * SECTION_HEADER_SIZE_BYTES; if ( bytes.byteLength < sectionDataOffset ) { throw new Error( 'THREE.KSPLATLoader: Invalid KSPLAT section headers.' ); } const compression = COMPRESSION_LEVELS[ header.compressionLevel ]; const centers = new Float32Array( header.splatCount * 3 ); const covariances = new Float32Array( header.splatCount * 6 ); const colors = new Uint8ClampedArray( header.splatCount * 4 ); const sphericalHarmonics = {}; const sphericalHarmonicsBytes = {}; let splatOffset = 0; let sectionBase = sectionDataOffset; for ( let sectionIndex = 0; sectionIndex < header.maxSectionCount; sectionIndex ++ ) { const sectionHeaderOffset = sectionHeadersOffset + sectionIndex * SECTION_HEADER_SIZE_BYTES; const section = parseSectionHeader( view, sectionHeaderOffset, compression ); const shComponents = SH_DEGREE_TO_COMPONENTS[ section.sphericalHarmonicsDegree ]; if ( shComponents === undefined ) { throw new Error( `THREE.KSPLATLoader: Unsupported KSPLAT spherical harmonics degree ${ section.sphericalHarmonicsDegree }.` ); } const bytesPerSplat = compression.bytesPerCenter + compression.bytesPerScale + compression.bytesPerRotation + compression.bytesPerColor + shComponents * compression.bytesPerSphericalHarmonicsComponent; const bucketsMetaDataSizeBytes = section.partiallyFilledBucketCount * 4; const bucketsStorageSizeBytes = section.bucketStorageSizeBytes * section.bucketCount + bucketsMetaDataSizeBytes; const splatDataStorageSizeBytes = bytesPerSplat * section.maxSplatCount; const storageSizeBytes = bucketsStorageSizeBytes + splatDataStorageSizeBytes; if ( sectionBase + storageSizeBytes > bytes.byteLength ) { throw new Error( 'THREE.KSPLATLoader: Invalid KSPLAT byte length.' ); } if ( section.splatCount > 0 ) { readSection( view, bytes, section, compression, sectionBase, bucketsMetaDataSizeBytes, bucketsStorageSizeBytes, bytesPerSplat, splatOffset, centers, covariances, colors, sphericalHarmonics, sphericalHarmonicsBytes, header ); splatOffset += section.splatCount; } sectionBase += storageSizeBytes; } if ( splatOffset !== header.splatCount ) { throw new Error( 'THREE.KSPLATLoader: KSPLAT splat count mismatch.' ); } return createGaussianSplatGeometry( centers, covariances, colors, sphericalHarmonics ); } } function parseHeader( view ) { return { versionMajor: view.getUint8( 0 ), versionMinor: view.getUint8( 1 ), maxSectionCount: view.getUint32( 4, true ), sectionCount: view.getUint32( 8, true ), maxSplatCount: view.getUint32( 12, true ), splatCount: view.getUint32( 16, true ), compressionLevel: view.getUint16( 20, true ), minSphericalHarmonicsCoeff: view.getFloat32( 36, true ) || - 1.5, maxSphericalHarmonicsCoeff: view.getFloat32( 40, true ) || 1.5 }; } function parseSectionHeader( view, offset, compression ) { return { splatCount: view.getUint32( offset, true ), maxSplatCount: view.getUint32( offset + 4, true ), bucketSize: view.getUint32( offset + 8, true ), bucketCount: view.getUint32( offset + 12, true ), bucketBlockSize: view.getFloat32( offset + 16, true ), bucketStorageSizeBytes: view.getUint16( offset + 20, true ), compressionScaleRange: view.getUint32( offset + 24, true ) || compression.scaleRange, fullBucketCount: view.getUint32( offset + 32, true ), partiallyFilledBucketCount: view.getUint32( offset + 36, true ), sphericalHarmonicsDegree: view.getUint16( offset + 40, true ) }; } function readSection( view, bytes, section, compression, sectionBase, bucketsMetaDataSizeBytes, bucketsStorageSizeBytes, bytesPerSplat, splatOffset, centers, covariances, colors, sphericalHarmonics, sphericalHarmonicsBytes, header ) { const bucketsBase = sectionBase + bucketsMetaDataSizeBytes; const dataBase = sectionBase + bucketsStorageSizeBytes; const fullBucketSplats = section.fullBucketCount * section.bucketSize; const compressionScaleFactor = section.bucketBlockSize / 2 / section.compressionScaleRange; const sphericalHarmonicsOffset = compression.colorOffsetBytes + compression.bytesPerColor; let partialBucketIndex = section.fullBucketCount; let partialBucketBase = fullBucketSplats; ensureSphericalHarmonics( sphericalHarmonics, sphericalHarmonicsBytes, header.splatCount, section.sphericalHarmonicsDegree ); for ( let i = 0; i < section.splatCount; i ++ ) { const bucketIndex = getBucketIndex( view, section, sectionBase, i, fullBucketSplats, partialBucketIndex, partialBucketBase ); if ( bucketIndex.partialBucketIndex !== undefined ) { partialBucketIndex = bucketIndex.partialBucketIndex; partialBucketBase = bucketIndex.partialBucketBase; } const rowOffset = dataBase + i * bytesPerSplat; const outIndex = splatOffset + i; const i3 = outIndex * 3; if ( compression.bytesPerCenter === 12 ) { centers[ i3 ] = view.getFloat32( rowOffset, true ); centers[ i3 + 1 ] = view.getFloat32( rowOffset + 4, true ); centers[ i3 + 2 ] = view.getFloat32( rowOffset + 8, true ); } else { const bucketBase = bucketsBase + bucketIndex.value * section.bucketStorageSizeBytes; centers[ i3 ] = ( view.getUint16( rowOffset, true ) - section.compressionScaleRange ) * compressionScaleFactor + view.getFloat32( bucketBase, true ); centers[ i3 + 1 ] = ( view.getUint16( rowOffset + 2, true ) - section.compressionScaleRange ) * compressionScaleFactor + view.getFloat32( bucketBase + 4, true ); centers[ i3 + 2 ] = ( view.getUint16( rowOffset + 4, true ) - section.compressionScaleRange ) * compressionScaleFactor + view.getFloat32( bucketBase + 8, true ); } const sx = readCompressedFloat( view, rowOffset + compression.scaleOffsetBytes, compression.bytesPerScale ); const sy = readCompressedFloat( view, rowOffset + compression.scaleOffsetBytes + compression.bytesPerScale / 3, compression.bytesPerScale ); const sz = readCompressedFloat( view, rowOffset + compression.scaleOffsetBytes + compression.bytesPerScale / 3 * 2, compression.bytesPerScale ); const qw = readCompressedFloat( view, rowOffset + compression.rotationOffsetBytes, compression.bytesPerRotation ); const qx = readCompressedFloat( view, rowOffset + compression.rotationOffsetBytes + compression.bytesPerRotation / 4, compression.bytesPerRotation ); const qy = readCompressedFloat( view, rowOffset + compression.rotationOffsetBytes + compression.bytesPerRotation / 4 * 2, compression.bytesPerRotation ); const qz = readCompressedFloat( view, rowOffset + compression.rotationOffsetBytes + compression.bytesPerRotation / 4 * 3, compression.bytesPerRotation ); writeCovariance( covariances, outIndex * 6, sx, sy, sz, qx, qy, qz, qw ); writeColorBytes( colors, outIndex * 4, bytes[ rowOffset + compression.colorOffsetBytes ], bytes[ rowOffset + compression.colorOffsetBytes + 1 ], bytes[ rowOffset + compression.colorOffsetBytes + 2 ], bytes[ rowOffset + compression.colorOffsetBytes + 3 ] ); for ( let degree = 1; degree <= section.sphericalHarmonicsDegree; degree ++ ) { writeKSPLATSphericalHarmonicsBand( sphericalHarmonicsBytes[ `sh${ degree }` ], outIndex, SH_BAND_COMPONENTS[ degree ], SH_BAND_WORDS[ degree ] * 4, SH_BAND_INDEX[ degree ], view, rowOffset + sphericalHarmonicsOffset, compression.bytesPerSphericalHarmonicsComponent, header ); } } } function ensureSphericalHarmonics( sphericalHarmonics, sphericalHarmonicsBytes, count, degree ) { for ( let i = 1; i <= degree; i ++ ) { if ( sphericalHarmonics[ `sh${ i }` ] === undefined ) { const band = createPackedSphericalHarmonicsBand( count, i ); sphericalHarmonics[ `sh${ i }` ] = band.packed; sphericalHarmonicsBytes[ `sh${ i }` ] = band.bytes; } } } function writeKSPLATSphericalHarmonicsBand( target, index, bandComponents, byteStride, componentIndexes, view, rowOffset, bytesPerComponent, header ) { const targetOffset = index * byteStride; for ( let i = 0; i < bandComponents; i ++ ) { target[ targetOffset + i ] = readCompressedSphericalHarmonic( view, rowOffset + componentIndexes[ i ] * bytesPerComponent, bytesPerComponent, header ) * 128 + 128; } } function getBucketIndex( view, section, sectionBase, splatIndex, fullBucketSplats, partialBucketIndex, partialBucketBase ) { if ( section.bucketCount === 0 ) { return { value: 0 }; } if ( splatIndex < fullBucketSplats ) { return { value: Math.floor( splatIndex / section.bucketSize ) }; } while ( partialBucketIndex < section.bucketCount ) { const partialIndex = partialBucketIndex - section.fullBucketCount; const bucketLength = view.getUint32( sectionBase + partialIndex * 4, true ); if ( splatIndex < partialBucketBase + bucketLength ) { return { value: partialBucketIndex, partialBucketIndex, partialBucketBase }; } partialBucketIndex ++; partialBucketBase += bucketLength; } throw new Error( 'THREE.KSPLATLoader: Invalid KSPLAT bucket data.' ); } function readCompressedFloat( view, offset, bytesPerVector ) { if ( bytesPerVector === 12 || bytesPerVector === 16 ) { return view.getFloat32( offset, true ); } return DataUtils.fromHalfFloat( view.getUint16( offset, true ) ); } function readCompressedSphericalHarmonic( view, offset, bytesPerComponent, header ) { if ( bytesPerComponent === 4 ) { return view.getFloat32( offset, true ); } if ( bytesPerComponent === 2 ) { return DataUtils.fromHalfFloat( view.getUint16( offset, true ) ); } const t = view.getUint8( offset ) / 255; return header.minSphericalHarmonicsCoeff + t * ( header.maxSphericalHarmonicsCoeff - header.minSphericalHarmonicsCoeff ); } export { KSPLATLoader };