Accueil / npm / three@0.186.1 / examples / jsm / loaders / KSPLATLoader.js

KSPLATLoader.js

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 };