Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions examples/jsm/exporters/GLTFExporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import {
RGBAFormat,
RepeatWrapping,
Scene,
Source,
SRGBColorSpace,
TextureSource,
CompressedTexture,
Vector3,
Quaternion,
Expand Down Expand Up @@ -1033,7 +1033,7 @@ class GLTFWriter {

const texture = reference.clone();

texture.source = new Source( canvas );
texture.source = new TextureSource( canvas );
texture.colorSpace = NoColorSpace;
texture.channel = ( metalnessMap || roughnessMap ).channel;

Expand Down Expand Up @@ -1095,7 +1095,7 @@ class GLTFWriter {
context.putImageData( imageData, 0, 0 );

const texture = normalMap.clone();
texture.source = new Source( canvas );
texture.source = new TextureSource( canvas );

return texture;

Expand Down
101 changes: 88 additions & 13 deletions examples/jsm/loaders/GLTFGaussianSplatLoaderExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
} from 'three';

import { GaussianSplatMesh } from '../objects/GaussianSplatMesh.js';
import { createGaussianSplatGeometry, writeColorBytesFromSH0, writeCovariance } from '../utils/GaussianSplatUtils.js';
import { SH_BAND_WORDS, createGaussianSplatGeometry, createPackedSphericalHarmonicsBand, writeColorBytesFromSH0, writeCovariance } from '../utils/GaussianSplatUtils.js';

const EXTENSION_NAME = 'KHR_gaussian_splatting';
const POINTS = 0;
Expand Down Expand Up @@ -158,20 +158,10 @@ function createGaussianSplatMesh( geometry, primitiveDef ) {

}

for ( const semantic in primitiveDef.attributes ) {

if ( /^KHR_gaussian_splatting:SH_DEGREE_[1-3]_COEF_/.test( semantic ) ) {

console.warn( 'THREE.GLTFGaussianSplatLoaderExtension: KHR_gaussian_splatting spherical harmonics above degree 0 are ignored.' );
break;

}

}

const centers = new Float32Array( count * 3 );
const covariances = new Float32Array( count * 6 );
const colors = new Uint8ClampedArray( count * 4 );
const sphericalHarmonics = createGLTFSphericalHarmonicsAttributes( geometry, primitiveDef, count );

for ( let i = 0; i < count; i ++ ) {

Expand Down Expand Up @@ -204,7 +194,7 @@ function createGaussianSplatMesh( geometry, primitiveDef ) {

}

const mesh = new GaussianSplatMesh( createGaussianSplatGeometry( centers, covariances, colors ) );
const mesh = new GaussianSplatMesh( createGaussianSplatGeometry( centers, covariances, colors, sphericalHarmonics ) );

mesh.userData.gltfExtensions = mesh.userData.gltfExtensions || {};
mesh.userData.gltfExtensions[ EXTENSION_NAME ] = Object.assign( {}, extensionDef );
Expand All @@ -213,6 +203,81 @@ function createGaussianSplatMesh( geometry, primitiveDef ) {

}

function createGLTFSphericalHarmonicsAttributes( geometry, primitiveDef, count ) {

const sphericalHarmonics = {};

for ( let degree = 1; degree <= 3; degree ++ ) {

const coefficientCount = degree * 2 + 1;
const attributes = [];

for ( let coefficient = 0; coefficient < coefficientCount; coefficient ++ ) {

const semantic = `KHR_gaussian_splatting:SH_DEGREE_${ degree }_COEF_${ coefficient }`;
const attribute = getOptionalGaussianSplatAttribute( geometry, primitiveDef, semantic );

if ( attribute !== undefined ) {

if ( attribute.count !== count || attribute.itemSize !== 3 ) {

throw new Error( `THREE.GLTFGaussianSplatLoaderExtension: Invalid ${ semantic } attribute.` );

}

}

attributes.push( attribute );

}

if ( attributes.every( attribute => attribute === undefined ) ) break;

if ( attributes.some( attribute => attribute === undefined ) ) {

throw new Error( `THREE.GLTFGaussianSplatLoaderExtension: Incomplete KHR_gaussian_splatting SH degree ${ degree } coefficients.` );

}

const band = createPackedSphericalHarmonicsBand( count, degree );
const target = band.bytes;
const byteStride = SH_BAND_WORDS[ degree ] * 4;

for ( let i = 0; i < count; i ++ ) {

for ( let coefficient = 0; coefficient < coefficientCount; coefficient ++ ) {

const attribute = attributes[ coefficient ];
const targetOffset = i * byteStride + coefficient * 3;

target[ targetOffset ] = attribute.getX( i ) * 128 + 128;
target[ targetOffset + 1 ] = attribute.getY( i ) * 128 + 128;
target[ targetOffset + 2 ] = attribute.getZ( i ) * 128 + 128;

}

}

sphericalHarmonics[ `sh${ degree }` ] = band.packed;

}

for ( const semantic in primitiveDef.attributes ) {

const match = semantic.match( /^KHR_gaussian_splatting:SH_DEGREE_([1-3])_COEF_/ );

if ( match !== null && sphericalHarmonics[ `sh${ match[ 1 ] }` ] === undefined ) {

throw new Error( 'THREE.GLTFGaussianSplatLoaderExtension: KHR_gaussian_splatting spherical harmonics attributes must be contiguous.' );

}

}

return sphericalHarmonics;

}

function getGaussianSplatAttribute( geometry, primitiveDef, semantic ) {

if ( primitiveDef.attributes[ semantic ] === undefined ) {
Expand All @@ -234,6 +299,16 @@ function getGaussianSplatAttribute( geometry, primitiveDef, semantic ) {

}

function getOptionalGaussianSplatAttribute( geometry, primitiveDef, semantic ) {

if ( primitiveDef.attributes[ semantic ] === undefined ) return undefined;

const attributeName = ATTRIBUTES[ semantic ] || semantic.toLowerCase();

return geometry.getAttribute( attributeName );

}

function assignExtrasToUserData( object, gltfDef ) {

if ( gltfDef.extras !== undefined ) {
Expand Down
101 changes: 94 additions & 7 deletions examples/jsm/loaders/KSPLATLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@ import {
Loader
} from 'three';

import { createGaussianSplatGeometry, writeColorBytes, writeCovariance } from '../utils/GaussianSplatUtils.js';
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,
Expand Down Expand Up @@ -52,8 +58,10 @@ const COMPRESSION_LEVELS = {
* A loader for GaussianSplats3D `.ksplat` files.
*
* This loader decodes the format into `BufferGeometry` for use with
* `GaussianSplatMesh`. Spherical harmonics payloads are skipped because the
* current renderer uses the stored degree-0 color.
* `GaussianSplatMesh`. 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();
Expand Down Expand Up @@ -170,6 +178,8 @@ class KSPLATLoader extends Loader {
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;

Expand Down Expand Up @@ -212,7 +222,10 @@ class KSPLATLoader extends Loader {
splatOffset,
centers,
covariances,
colors
colors,
sphericalHarmonics,
sphericalHarmonicsBytes,
header
);

splatOffset += section.splatCount;
Expand All @@ -229,7 +242,7 @@ class KSPLATLoader extends Loader {

}

return createGaussianSplatGeometry( centers, covariances, colors );
return createGaussianSplatGeometry( centers, covariances, colors, sphericalHarmonics );

}

Expand All @@ -244,7 +257,9 @@ function parseHeader( view ) {
sectionCount: view.getUint32( 8, true ),
maxSplatCount: view.getUint32( 12, true ),
splatCount: view.getUint32( 16, true ),
compressionLevel: view.getUint16( 20, true )
compressionLevel: view.getUint16( 20, true ),
minSphericalHarmonicsCoeff: view.getFloat32( 36, true ) || - 1.5,
maxSphericalHarmonicsCoeff: view.getFloat32( 40, true ) || 1.5
};

}
Expand All @@ -266,15 +281,18 @@ function parseSectionHeader( view, offset, compression ) {

}

function readSection( view, bytes, section, compression, sectionBase, bucketsMetaDataSizeBytes, bucketsStorageSizeBytes, bytesPerSplat, splatOffset, centers, covariances, colors ) {
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 );
Expand Down Expand Up @@ -323,6 +341,55 @@ function readSection( view, bytes, section, compression, sectionBase, bucketsMet
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;

}

}
Expand Down Expand Up @@ -373,4 +440,24 @@ function readCompressedFloat( view, offset, bytesPerVector ) {

}

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