three
Version:
JavaScript 3D library
387 lines (264 loc) • 11.2 kB
JavaScript
import {
FileLoader,
Loader
} from 'three';
import { PLYLoader } from './PLYLoader.js';
import {
SH_BAND_COMPONENTS,
SH_BAND_WORDS,
createGaussianSplatGeometry,
createPackedSphericalHarmonicsBand,
sigmoid,
writeColorBytesFromSH0,
writeCovariance
} from '../utils/GaussianSplatUtils.js';
// f_rest component count, indexed by spherical harmonics degree.
const SH_DEGREE_TO_COMPONENTS = [ 0, 9, 24, 45 ];
const GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING = {
scale: [ 'scale_0', 'scale_1', 'scale_2' ],
rotation: [ 'rot_0', 'rot_1', 'rot_2', 'rot_3' ],
f_dc: [ 'f_dc_0', 'f_dc_1', 'f_dc_2' ],
opacity: [ 'opacity' ]
};
// Property names a Gaussian splat PLY must declare in its header. Checked
// up front against the header text rather than the parsed geometry, since
// PLYLoader still creates a (garbage-filled) attribute for a custom property
// name that's missing from the file instead of omitting it.
const REQUIRED_PLY_PROPERTIES = [
'x', 'y', 'z',
...GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING.scale,
...GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING.rotation,
...GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING.f_dc,
...GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING.opacity
];
const _headerPattern = /^ply([\s\S]*?)end_header/;
const _propertyPattern = /^property\s+\S+\s+(\S+)\s*$/;
const _restPropertyPattern = /^f_rest_\d+$/;
/**
* A loader for Gaussian splat PLY files, e.g. as exported by the original
* GraphDECO/INRIA 3D Gaussian Splatting implementation.
*
* PLY itself is a generic format, so the caller would normally have to know
* the file's spherical harmonics (SH) degree ahead of time to configure
* `PLYLoader` with the right custom property mapping before parsing. This
* loader avoids that by scanning the plain-text PLY header for `f_rest_N`
* properties first, since SH degree maps to a fixed, closed table of
* `f_rest` counts (0/9/24/45 -> degree 0/1/2/3), and configuring an
* internal `PLYLoader` accordingly before converting the result into
* Gaussian splat geometry.
*
* ```js
* const loader = new GaussianSplatPLYLoader();
* const geometry = await loader.loadAsync( './models/gsplat/point_cloud.ply' );
* scene.add( new GaussianSplat( geometry ) );
* ```
*
* @augments Loader
* @three_import import { GaussianSplatPLYLoader } from 'three/addons/loaders/GaussianSplatPLYLoader.js';
*/
class GaussianSplatPLYLoader extends Loader {
/**
* Constructs a new Gaussian splat PLY loader.
*
* @param {LoadingManager} [manager] - The loading manager.
*/
constructor( manager ) {
super( manager );
}
/**
* Starts loading from the given URL and passes the loaded Gaussian splat
* geometry 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 ( data ) {
try {
onLoad( scope.parse( data ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
console.error( e );
}
scope.manager.itemError( url );
}
}, onProgress, onError );
}
/**
* Parses the given Gaussian splat PLY data and returns the resulting
* Gaussian splat geometry.
*
* This scans the PLY header for the file's spherical harmonics degree,
* so unlike a plain `PLYLoader`, no prior setup is required.
*
* @param {ArrayBuffer|string} data - The raw PLY data, as an array buffer or string.
* @return {BufferGeometry} The parsed Gaussian splat geometry.
*/
parse( data ) {
const degree = detectSphericalHarmonicsDegree( data );
const plyLoader = new PLYLoader( this.manager );
plyLoader.setCustomPropertyNameMapping( getPropertyMapping( degree ) );
return convertPLYGeometry( plyLoader.parse( data ) );
}
}
// Scans the PLY header text for its vertex properties, verifying the
// required Gaussian splat properties are present and mapping the number of
// "f_rest_N" properties found to a spherical harmonics degree. PLY headers
// are always plain ASCII text that fully precedes the vertex data, so this
// can run before the file is parsed by the generic PLYLoader.
function detectSphericalHarmonicsDegree( data ) {
const headerText = typeof data === 'string' ? data : decodeHeaderText( new Uint8Array( data ) );
const headerMatch = _headerPattern.exec( headerText );
if ( headerMatch === null ) {
throw new Error( 'THREE.GaussianSplatPLYLoader: Missing PLY header.' );
}
const propertyNames = new Set();
let restComponentCount = 0;
for ( const line of headerMatch[ 1 ].split( /\r\n|\r|\n/ ) ) {
const propertyMatch = _propertyPattern.exec( line.trim() );
if ( propertyMatch === null ) continue;
propertyNames.add( propertyMatch[ 1 ] );
if ( _restPropertyPattern.test( propertyMatch[ 1 ] ) ) restComponentCount ++;
}
if ( REQUIRED_PLY_PROPERTIES.some( name => ! propertyNames.has( name ) ) ) {
throw new Error( 'THREE.GaussianSplatPLYLoader: PLY file requires position, scale, rotation, f_dc and opacity properties.' );
}
const degree = SH_DEGREE_TO_COMPONENTS.indexOf( restComponentCount );
if ( degree === - 1 ) {
throw new Error( `THREE.GaussianSplatPLYLoader: Unsupported number of f_rest spherical harmonics coefficients (${ restComponentCount }).` );
}
return degree;
}
// Decodes only the bytes up to and including "end_header" as text, falling
// back to the full buffer if that marker isn't found, so this doesn't pay
// the cost of decoding the (potentially large) binary vertex data as text.
function decodeHeaderText( bytes ) {
const marker = 'end_header';
const scanLength = Math.min( bytes.length, 1024 * 1024 );
let headerEnd = - 1;
for ( let i = 0; i <= scanLength - marker.length; i ++ ) {
let matches = true;
for ( let j = 0; j < marker.length; j ++ ) {
if ( bytes[ i + j ] !== marker.charCodeAt( j ) ) {
matches = false;
break;
}
}
if ( matches ) {
headerEnd = i + marker.length;
break;
}
}
const end = headerEnd === - 1 ? bytes.length : headerEnd;
return new TextDecoder().decode( bytes.subarray( 0, end ) );
}
// Builds the PLYLoader custom-property mapping for a given SH degree,
// grouping the raw "f_rest_N" scalar properties into one combined
// "f_rest" attribute when the degree calls for it.
function getPropertyMapping( sphericalHarmonicsDegree ) {
const restComponentCount = SH_DEGREE_TO_COMPONENTS[ sphericalHarmonicsDegree ];
const mapping = {
scale: GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING.scale,
rotation: GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING.rotation,
f_dc: GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING.f_dc,
opacity: GAUSSIAN_SPLAT_PLY_PROPERTY_MAPPING.opacity
};
if ( restComponentCount > 0 ) {
mapping.f_rest = Array.from( { length: restComponentCount }, ( _, i ) => `f_rest_${ i }` );
}
return mapping;
}
// Converts the generic PLYLoader output - using the Gaussian splat custom
// property mapping above - into Gaussian splat geometry.
function convertPLYGeometry( geometry ) {
const position = geometry.getAttribute( 'position' );
const scale = geometry.getAttribute( 'scale' );
const rotation = geometry.getAttribute( 'rotation' );
const sh0 = geometry.getAttribute( 'f_dc' );
const shRest = geometry.getAttribute( 'f_rest' );
const opacity = geometry.getAttribute( 'opacity' );
if ( position === undefined || scale === undefined || rotation === undefined || sh0 === undefined || opacity === undefined ) {
throw new Error( 'THREE.GaussianSplatPLYLoader: PLY file requires position, scale, rotation, f_dc and opacity properties.' );
}
const count = position.count;
if ( position.itemSize !== 3 || scale.itemSize !== 3 || rotation.itemSize !== 4 || sh0.itemSize !== 3 || opacity.itemSize !== 1 ) {
throw new Error( 'THREE.GaussianSplatPLYLoader: Invalid Gaussian splat PLY property itemSize.' );
}
if ( scale.count !== count || rotation.count !== count || sh0.count !== count || opacity.count !== count ) {
throw new Error( 'THREE.GaussianSplatPLYLoader: Gaussian splat PLY property counts must match position.' );
}
const centers = new Float32Array( count * 3 );
const covariances = new Float32Array( count * 6 );
const colors = new Uint8ClampedArray( count * 4 );
const sphericalHarmonicsDegree = getRestSphericalHarmonicsDegree( shRest );
const sphericalHarmonics = {};
const sphericalHarmonicsBytes = {};
for ( let degree = 1; degree <= sphericalHarmonicsDegree; degree ++ ) {
const band = createPackedSphericalHarmonicsBand( count, degree );
sphericalHarmonics[ `sh${ degree }` ] = band.packed;
sphericalHarmonicsBytes[ `sh${ degree }` ] = band.bytes;
}
for ( let i = 0; i < count; i ++ ) {
const i3 = i * 3;
centers[ i3 ] = position.getX( i );
centers[ i3 + 1 ] = position.getY( i );
centers[ i3 + 2 ] = position.getZ( i );
const sx = Math.exp( scale.getX( i ) );
const sy = Math.exp( scale.getY( i ) );
const sz = Math.exp( scale.getZ( i ) );
// GraphDECO/INRIA PLY stores quaternions as rot_0=w, rot_1=x, rot_2=y, rot_3=z.
const qw = rotation.getX( i );
const qx = rotation.getY( i );
const qy = rotation.getZ( i );
const qz = rotation.getW( i );
writeCovariance( covariances, i * 6, sx, sy, sz, qx, qy, qz, qw );
writeColorBytesFromSH0(
colors,
i * 4,
sh0.getX( i ),
sh0.getY( i ),
sh0.getZ( i ),
sigmoid( opacity.getX( i ) )
);
if ( sphericalHarmonicsDegree > 0 ) {
writeSphericalHarmonicsFromRest( sphericalHarmonicsBytes, i, shRest );
}
}
return createGaussianSplatGeometry( centers, covariances, colors, sphericalHarmonics );
}
function getRestSphericalHarmonicsDegree( shRest ) {
if ( shRest === undefined ) return 0;
const degree = SH_DEGREE_TO_COMPONENTS.indexOf( shRest.itemSize );
if ( degree === - 1 ) {
throw new Error( 'THREE.GaussianSplatPLYLoader: Unsupported number of f_rest spherical harmonics coefficients.' );
}
return degree;
}
function writeSphericalHarmonicsFromRest( sphericalHarmonicsBytes, index, shRest ) {
const stride = shRest.itemSize / 3;
const source = shRest.array;
const sourceOffset = index * shRest.itemSize;
for ( let degree = 1; degree <= 3; degree ++ ) {
const target = sphericalHarmonicsBytes[ `sh${ degree }` ];
if ( target === undefined ) break;
const bandOffset = degree === 1 ? 0 : degree === 2 ? 3 : 8;
const byteStride = SH_BAND_WORDS[ degree ] * 4;
const targetOffset = index * byteStride;
for ( let j = 0; j < SH_BAND_COMPONENTS[ degree ]; j ++ ) {
const coefficient = Math.floor( j / 3 );
const channel = j % 3;
target[ targetOffset + j ] = source[ sourceOffset + bandOffset + coefficient + channel * stride ] * 128 + 128;
}
}
}
export { GaussianSplatPLYLoader };