Skip to content

Commit

Permalink
Merge pull request #413 from mkkellogg/dev
Browse files Browse the repository at this point in the history
Release 0.4.7
  • Loading branch information
mkkellogg authored Jan 25, 2025
2 parents 408943e + c3cd322 commit 2dfc83e
Show file tree
Hide file tree
Showing 21 changed files with 989 additions and 269 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ Advanced `Viewer` parameters
| `logLevel` | Verbosity of the console logging. Defaults to `GaussianSplats3D.LogLevel.None`.
| `sphericalHarmonicsDegree` | Degree of spherical harmonics to utilize in rendering splats (assuming the data is present in the splat scene). Valid values are 0, 1, or 2. Default value is 0.
| `enableOptionalEffects` | When true, allows for usage of extra properties and attributes during rendering for effects such as opacity adjustment. Default is `false` for performance reasons. These properties are separate from transform properties (scale, rotation, position) that are enabled by the `dynamicScene` parameter.
| `inMemoryCompressionLevel` | Level to compress `.ply` or `.ksplat` files when loading them for direct rendering (not exporting to `.ksplat`). Valid values are the same as `.ksplat` compression levels (0, 1, or 2). Default is 0.
| `optimizeSplatData` | Reorder splat data in memory after loading is complete to optimize cache utilization. Default is `true`. Does not apply if splat scene is progressively loaded.
| `optimizeSplatData` | After loading is complete, reorder splat data in memory to optimize cache utilization as well as apply in-memory compression to reduce memory usage. Default is `true`. This option is automatically disabled if the scene is progressively loaded.
| `inMemoryCompressionLevel` | Level to compress scenes when loading them for direct rendering (not exporting to `.ksplat`). Valid values are the same as `.ksplat` compression levels (0, 1, or 2). Default is 0 (uncompressed). If a scene is loaded progressively, or `optimizeSplatData` is set to `false`, `inMemoryCompressionLevel` will be 0.
| `freeIntermediateSplatData` | When true, the intermediate splat data that is the result of decompressing splat bufffer(s) and used to populate data textures will be freed. This will reduces memory usage, but if that data needs to be modified it will need to be re-populated from the splat buffer(s). Defaults to `false`.
| `splatRenderMode` | Determine which splat rendering mode to enable. Valid values are defined in the `SplatRenderMode` enum: `ThreeD` and `TwoD`. `ThreeD` is the original/traditional mode and `TwoD` is the new mode described here: https://surfsplatting.github.io/
| `sceneFadeInRateMultiplier` | Customize the speed at which the scene is revealed. Default is 1.0.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"type": "git",
"url": "https://github.com/mkkellogg/GaussianSplats3D"
},
"version": "0.4.6",
"version": "0.4.7",
"description": "Three.js-based 3D Gaussian splat viewer",
"module": "build/gaussian-splats-3d.module.js",
"main": "build/gaussian-splats-3d.umd.cjs",
Expand Down
36 changes: 23 additions & 13 deletions src/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
aborted = true;
};

let onProgressCalledAtComplete = false;
const localOnProgress = (percent, percentLabel, chunk, fileSize) => {
if (onProgress && !onProgressCalledAtComplete) {
onProgress(percent, percentLabel, chunk, fileSize);
if (percent === 100) {
onProgressCalledAtComplete = true;
}
}
};

return new AbortablePromise((resolve, reject) => {
const fetchOptions = { signal };
if (headers) fetchOptions.headers = headers;
Expand All @@ -87,9 +97,7 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
try {
const { value: chunk, done } = await reader.read();
if (done) {
if (onProgress) {
onProgress(100, '100%', chunk, fileSize);
}
localOnProgress(100, '100%', chunk, fileSize);
if (saveChunks) {
const buffer = new Blob(chunks).arrayBuffer();
resolve(buffer);
Expand All @@ -108,9 +116,7 @@ export const fetchWithProgress = function(path, onProgress, saveChunks = true, h
if (saveChunks) {
chunks.push(chunk);
}
if (onProgress) {
onProgress(percent, percentLabel, chunk, fileSize);
}
localOnProgress(percent, percentLabel, chunk, fileSize);
} catch (error) {
reject(error);
return;
Expand Down Expand Up @@ -151,20 +157,24 @@ export const disposeAllMeshes = (object3D) => {
export const delayedExecute = (func, fast) => {
return new Promise((resolve) => {
window.setTimeout(() => {
resolve(func());
resolve(func ? func() : undefined);
}, fast ? 1 : 50);
});
};


export const getSphericalHarmonicsComponentCountForDegree = (sphericalHarmonicsDegree = 0) => {
switch (sphericalHarmonicsDegree) {
case 1:
return 9;
case 2:
return 24;
let shCoeffPerSplat = 0;
if (sphericalHarmonicsDegree === 1) {
shCoeffPerSplat = 9;
} else if (sphericalHarmonicsDegree === 2) {
shCoeffPerSplat = 24;
} else if (sphericalHarmonicsDegree === 3) {
shCoeffPerSplat = 45;
} else if (sphericalHarmonicsDegree > 3) {
throw new Error('getSphericalHarmonicsComponentCountForDegree() -> Invalid spherical harmonics degree');
}
return 0;
return shCoeffPerSplat;
};

export const nativePromiseWithExtractedComponents = () => {
Expand Down
48 changes: 27 additions & 21 deletions src/Viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { OrbitControls } from './OrbitControls.js';
import { PlyLoader } from './loaders/ply/PlyLoader.js';
import { SplatLoader } from './loaders/splat/SplatLoader.js';
import { KSplatLoader } from './loaders/ksplat/KSplatLoader.js';
import { SpzLoader } from './loaders/spz/SpzLoader.js';
import { sceneFormatFromPath } from './loaders/Utils.js';
import { LoadingSpinner } from './ui/LoadingSpinner.js';
import { LoadingProgressBar } from './ui/LoadingProgressBar.js';
Expand Down Expand Up @@ -857,8 +858,7 @@ export class Viewer {
if (onException) onException();
this.clearSplatSceneDownloadAndBuildPromise();
this.removeSplatSceneDownloadPromise(downloadPromise);
const error = (e instanceof AbortedPromiseError) ? e : new Error(`Viewer::addSplatScene -> Could not load file ${path}`);
downloadAndBuildPromise.reject(error);
downloadAndBuildPromise.reject(this.updateError(e, `Viewer::addSplatScene -> Could not load file ${path}`));
});

this.addSplatSceneDownloadPromise(downloadPromise);
Expand Down Expand Up @@ -938,7 +938,7 @@ export class Viewer {
.catch((e) => {
this.clearSplatSceneDownloadAndBuildPromise();
this.removeSplatSceneDownloadPromise(splatSceneDownloadPromise);
const error = (e instanceof AbortedPromiseError) ? e : new Error(`Viewer::addSplatScene -> Could not load one or more scenes`);
const error = this.updateError(e, `Viewer::addSplatScene -> Could not load one or more scenes`);
progressiveLoadFirstSectionBuildPromise.reject(error);
if (onDownloadException) onDownloadException(error);
});
Expand Down Expand Up @@ -1030,9 +1030,7 @@ export class Viewer {
.catch((e) => {
if (showLoadingUI) this.loadingSpinner.removeTask(loadingUITaskId);
this.clearSplatSceneDownloadAndBuildPromise();
const error = (e instanceof AbortedPromiseError) ? e :
new Error(`Viewer::addSplatScenes -> Could not load one or more splat scenes.`);
reject(error);
reject(this.updateError(e, `Viewer::addSplatScenes -> Could not load one or more splat scenes.`));
})
.finally(() => {
this.removeSplatSceneDownloadPromise(downloadAndBuildPromise);
Expand Down Expand Up @@ -1062,24 +1060,24 @@ export class Viewer {
*/
downloadSplatSceneToSplatBuffer(path, splatAlphaRemovalThreshold = 1, onProgress = undefined,
progressiveBuild = false, onSectionBuilt = undefined, format, headers) {

const optimizeSplatData = progressiveBuild ? false : this.optimizeSplatData;
try {
if (format === SceneFormat.Splat) {
return SplatLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, splatAlphaRemovalThreshold,
this.inMemoryCompressionLevel, optimizeSplatData, headers);
} else if (format === SceneFormat.KSplat) {
return KSplatLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, headers);
} else if (format === SceneFormat.Ply) {
return PlyLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, splatAlphaRemovalThreshold,
this.inMemoryCompressionLevel, optimizeSplatData, this.sphericalHarmonicsDegree, headers);
if (format === SceneFormat.Splat || format === SceneFormat.KSplat || format === SceneFormat.Ply) {
const optimizeSplatData = progressiveBuild ? false : this.optimizeSplatData;
if (format === SceneFormat.Splat) {
return SplatLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, splatAlphaRemovalThreshold,
this.inMemoryCompressionLevel, optimizeSplatData, headers);
} else if (format === SceneFormat.KSplat) {
return KSplatLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, headers);
} else if (format === SceneFormat.Ply) {
return PlyLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, splatAlphaRemovalThreshold,
this.inMemoryCompressionLevel, optimizeSplatData, this.sphericalHarmonicsDegree, headers);
}
} else if (format === SceneFormat.Spz) {
return SpzLoader.loadFromURL(path, onProgress, splatAlphaRemovalThreshold, this.inMemoryCompressionLevel,
this.optimizeSplatData, this.sphericalHarmonicsDegree, headers);
}
} catch (e) {
if (e instanceof DirectLoadError) {
throw new Error('File type or server does not support progressive loading.');
} else {
throw e;
}
throw this.updateError(e, null);
}

throw new Error(`Viewer::downloadSplatSceneToSplatBuffer -> File format not supported: ${path}`);
Expand Down Expand Up @@ -1301,6 +1299,14 @@ export class Viewer {
});
}

updateError(error, defaultMessage) {
if (error instanceof AbortedPromiseError) return error;
if (error instanceof DirectLoadError) {
return new Error('File type or server does not support progressive loading.');
}
return defaultMessage ? new Error(defaultMessage) : error;
}

disposeSortWorker() {
if (this.sortWorker) this.sortWorker.terminate();
this.sortWorker = null;
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { PlyParser } from './loaders/ply/PlyParser.js';
import { PlayCanvasCompressedPlyParser } from './loaders/ply/PlayCanvasCompressedPlyParser.js';
import { PlyLoader } from './loaders/ply/PlyLoader.js';
import { SpzLoader } from './loaders/spz/SpzLoader.js';
import { SplatLoader } from './loaders/splat/SplatLoader.js';
import { KSplatLoader } from './loaders/ksplat/KSplatLoader.js';
import * as LoaderUtils from './loaders/Utils.js';
Expand All @@ -23,6 +24,7 @@ export {
PlyParser,
PlayCanvasCompressedPlyParser,
PlyLoader,
SpzLoader,
SplatLoader,
KSplatLoader,
LoaderUtils,
Expand Down
42 changes: 42 additions & 0 deletions src/loaders/Compression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const createStream = (data)=> {
return new ReadableStream({
async start(controller) {
controller.enqueue(data);
controller.close();
},
});
};

export async function decompressGzipped(data) {
try {
const stream = createStream(data);
if (!stream) throw new Error('Failed to create stream from data');

return await decompressGzipStream(stream);
} catch (error) {
console.error('Error decompressing gzipped data:', error);
throw error;
}
}

export async function decompressGzipStream(stream) {
const decompressedStream = stream.pipeThrough(new DecompressionStream('gzip'));
const response = new Response(decompressedStream);
const buffer = await response.arrayBuffer();

return new Uint8Array(buffer);
}

export async function compressGzipped(data) {
try {
const stream = createStream(data);
const compressedStream = stream.pipeThrough(new CompressionStream('gzip'));
const response = new Response(compressedStream);
const buffer = await response.arrayBuffer();

return new Uint8Array(buffer);
} catch (error) {
console.error('Error compressing gzipped data:', error);
throw error;
}
}
4 changes: 2 additions & 2 deletions src/loaders/InternalLoadType.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const InternalLoadType = {
DirectToSplatBuffer: 0,
DirectToSplatArray: 1,
ProgressiveToSplatBuffer: 0,
ProgressiveToSplatArray: 1,
DownloadBeforeProcessing: 2
};
3 changes: 2 additions & 1 deletion src/loaders/SceneFormat.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export const SceneFormat = {
'Splat': 0,
'KSplat': 1,
'Ply': 2
'Ply': 2,
'Spz': 3
};
34 changes: 34 additions & 0 deletions src/loaders/SplatBuffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -1398,4 +1398,38 @@ export class SplatBuffer {
};
}

static preallocateUncompressed(splatCount, sphericalHarmonicsDegrees) {
const shDescriptor = SplatBuffer.CompressionLevels[0].SphericalHarmonicsDegrees[sphericalHarmonicsDegrees];
const splatBufferDataOffsetBytes = SplatBuffer.HeaderSizeBytes + SplatBuffer.SectionHeaderSizeBytes;
const splatBufferSizeBytes = splatBufferDataOffsetBytes + shDescriptor.BytesPerSplat * splatCount;
const outBuffer = new ArrayBuffer(splatBufferSizeBytes);
SplatBuffer.writeHeaderToBuffer({
versionMajor: SplatBuffer.CurrentMajorVersion,
versionMinor: SplatBuffer.CurrentMinorVersion,
maxSectionCount: 1,
sectionCount: 1,
maxSplatCount: splatCount,
splatCount: splatCount,
compressionLevel: 0,
sceneCenter: new THREE.Vector3()
}, outBuffer);

SplatBuffer.writeSectionHeaderToBuffer({
maxSplatCount: splatCount,
splatCount: splatCount,
bucketSize: 0,
bucketCount: 0,
bucketBlockSize: 0,
compressionScaleRange: 0,
storageSizeBytes: 0,
fullBucketCount: 0,
partiallyFilledBucketCount: 0,
sphericalHarmonicsDegree: sphericalHarmonicsDegrees
}, 0, outBuffer, SplatBuffer.HeaderSizeBytes);

return {
splatBuffer: new SplatBuffer(outBuffer, true),
splatBufferDataOffsetBytes
};
}
}
1 change: 1 addition & 0 deletions src/loaders/Utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ export const sceneFormatFromPath = (path) => {
if (path.endsWith('.ply')) return SceneFormat.Ply;
else if (path.endsWith('.splat')) return SceneFormat.Splat;
else if (path.endsWith('.ksplat')) return SceneFormat.KSplat;
else if (path.endsWith('.spz')) return SceneFormat.Spz;
return null;
};
8 changes: 4 additions & 4 deletions src/loaders/ksplat/KSplatLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class KSplatLoader {
}
};

static loadFromURL(fileName, externalOnProgress, loadDirectoToSplatBuffer, onSectionBuilt, headers) {
static loadFromURL(fileName, externalOnProgress, progressiveLoadToSplatBuffer, onSectionBuilt, headers) {
let directLoadBuffer;
let directLoadSplatBuffer;

Expand Down Expand Up @@ -187,7 +187,7 @@ export class KSplatLoader {
}
numBytesLoaded += chunk.byteLength;
}
if (loadDirectoToSplatBuffer) {
if (progressiveLoadToSplatBuffer) {
checkAndLoadHeader();
checkAndLoadSectionHeaders();
checkAndLoadSections();
Expand All @@ -196,9 +196,9 @@ export class KSplatLoader {
}
};

return fetchWithProgress(fileName, localOnProgress, !loadDirectoToSplatBuffer, headers).then((fullBuffer) => {
return fetchWithProgress(fileName, localOnProgress, !progressiveLoadToSplatBuffer, headers).then((fullBuffer) => {
if (externalOnProgress) externalOnProgress(0, '0%', LoaderStatus.Processing);
const loadPromise = loadDirectoToSplatBuffer ? directLoadPromise.promise : KSplatLoader.loadFromFileData(fullBuffer);
const loadPromise = progressiveLoadToSplatBuffer ? directLoadPromise.promise : KSplatLoader.loadFromFileData(fullBuffer);
return loadPromise.then((splatBuffer) => {
if (externalOnProgress) externalOnProgress(100, '100%', LoaderStatus.Done);
return splatBuffer;
Expand Down
Loading

0 comments on commit 2dfc83e

Please sign in to comment.