@mkkellogg/gaussian-splats-3d
Version:
Three.js-based 3D Gaussian splat viewer
1,213 lines (1,014 loc) • 586 kB
JavaScript
import * as THREE from 'three';
import { Ray as Ray$1, Plane, MathUtils, EventDispatcher, Vector3, MOUSE, TOUCH, Quaternion, Spherical, Vector2 } from 'three';
/**
* AbortablePromise: A quick & dirty wrapper for JavaScript's Promise class that allows the underlying
* asynchronous operation to be cancelled. It is only meant for simple situations where no complex promise
* chaining or merging occurs. It needs a significant amount of work to truly replicate the full
* functionality of JavaScript's Promise class. Look at Util.fetchWithProgress() for example usage.
*
* This class was primarily added to allow splat scene downloads to be cancelled. It has not been tested
* very thoroughly and the implementation is kinda janky. If you can at all help it, please avoid using it :)
*/
class AbortablePromise {
static idGen = 0;
constructor(promiseFunc, abortHandler) {
let resolver;
let rejecter;
this.promise = new Promise((resolve, reject) => {
resolver = resolve;
rejecter = reject;
});
const promiseResolve = resolver.bind(this);
const promiseReject = rejecter.bind(this);
const resolve = (...args) => {
promiseResolve(...args);
};
const reject = (error) => {
promiseReject(error);
};
promiseFunc(resolve.bind(this), reject.bind(this));
this.abortHandler = abortHandler;
this.id = AbortablePromise.idGen++;
}
then(onResolve) {
return new AbortablePromise((resolve, reject) => {
this.promise = this.promise
.then((...args) => {
const onResolveResult = onResolve(...args);
if (onResolveResult instanceof Promise || onResolveResult instanceof AbortablePromise) {
onResolveResult.then((...args2) => {
resolve(...args2);
});
} else {
resolve(onResolveResult);
}
})
.catch((error) => {
reject(error);
});
}, this.abortHandler);
}
catch(onFail) {
return new AbortablePromise((resolve) => {
this.promise = this.promise.then((...args) => {
resolve(...args);
})
.catch(onFail);
}, this.abortHandler);
}
abort(reason) {
if (this.abortHandler) this.abortHandler(reason);
}
}
class AbortedPromiseError extends Error {
constructor(msg) {
super(msg);
}
}
const floatToHalf = function() {
const floatView = new Float32Array(1);
const int32View = new Int32Array(floatView.buffer);
return function(val) {
floatView[0] = val;
const x = int32View[0];
let bits = (x >> 16) & 0x8000;
let m = (x >> 12) & 0x07ff;
const e = (x >> 23) & 0xff;
if (e < 103) return bits;
if (e > 142) {
bits |= 0x7c00;
bits |= ((e == 255) ? 0 : 1) && (x & 0x007fffff);
return bits;
}
if (e < 113) {
m |= 0x0800;
bits |= (m >> (114 - e)) + ((m >> (113 - e)) & 1);
return bits;
}
bits |= (( e - 112) << 10) | (m >> 1);
bits += m & 1;
return bits;
};
}();
const uintEncodedFloat = function() {
const floatView = new Float32Array(1);
const int32View = new Int32Array(floatView.buffer);
return function(f) {
floatView[0] = f;
return int32View[0];
};
}();
const rgbaToInteger = function(r, g, b, a) {
return r + (g << 8) + (b << 16) + (a << 24);
};
const rgbaArrayToInteger = function(arr, offset) {
return arr[offset] + (arr[offset + 1] << 8) + (arr[offset + 2] << 16) + (arr[offset + 3] << 24);
};
const fetchWithProgress = function(path, onProgress, saveChunks = true, headers) {
const abortController = new AbortController();
const signal = abortController.signal;
let aborted = false;
const abortHandler = (reason) => {
abortController.abort(reason);
aborted = true;
};
return new AbortablePromise((resolve, reject) => {
const fetchOptions = { signal };
if (headers) fetchOptions.headers = headers;
fetch(path, fetchOptions)
.then(async (data) => {
// Handle error conditions where data is still returned
if (!data.ok) {
const errorText = await data.text();
reject(new Error(`Fetch failed: ${data.status} ${data.statusText} ${errorText}`));
return;
}
const reader = data.body.getReader();
let bytesDownloaded = 0;
let _fileSize = data.headers.get('Content-Length');
let fileSize = _fileSize ? parseInt(_fileSize) : undefined;
const chunks = [];
while (!aborted) {
try {
const { value: chunk, done } = await reader.read();
if (done) {
if (onProgress) {
onProgress(100, '100%', chunk, fileSize);
}
if (saveChunks) {
const buffer = new Blob(chunks).arrayBuffer();
resolve(buffer);
} else {
resolve();
}
break;
}
bytesDownloaded += chunk.length;
let percent;
let percentLabel;
if (fileSize !== undefined) {
percent = bytesDownloaded / fileSize * 100;
percentLabel = `${percent.toFixed(2)}%`;
}
if (saveChunks) {
chunks.push(chunk);
}
if (onProgress) {
onProgress(percent, percentLabel, chunk, fileSize);
}
} catch (error) {
reject(error);
return;
}
}
})
.catch((error) => {
reject(new AbortedPromiseError(error));
});
}, abortHandler);
};
const clamp = function(val, min, max) {
return Math.max(Math.min(val, max), min);
};
const getCurrentTime = function() {
return performance.now() / 1000;
};
const disposeAllMeshes = (object3D) => {
if (object3D.geometry) {
object3D.geometry.dispose();
object3D.geometry = null;
}
if (object3D.material) {
object3D.material.dispose();
object3D.material = null;
}
if (object3D.children) {
for (let child of object3D.children) {
disposeAllMeshes(child);
}
}
};
const delayedExecute = (func, fast) => {
return new Promise((resolve) => {
window.setTimeout(() => {
resolve(func());
}, fast ? 1 : 50);
});
};
const getSphericalHarmonicsComponentCountForDegree = (sphericalHarmonicsDegree = 0) => {
switch (sphericalHarmonicsDegree) {
case 1:
return 9;
case 2:
return 24;
}
return 0;
};
const nativePromiseWithExtractedComponents = () => {
let resolver;
let rejecter;
const promise = new Promise((resolve, reject) => {
resolver = resolve;
rejecter = reject;
});
return {
'promise': promise,
'resolve': resolver,
'reject': rejecter
};
};
const abortablePromiseWithExtractedComponents = (abortHandler) => {
let resolver;
let rejecter;
if (!abortHandler) {
abortHandler = () => {};
}
const promise = new AbortablePromise((resolve, reject) => {
resolver = resolve;
rejecter = reject;
}, abortHandler);
return {
'promise': promise,
'resolve': resolver,
'reject': rejecter
};
};
class Semver {
constructor(major, minor, patch) {
this.major = major;
this.minor = minor;
this.patch = patch;
}
toString() {
return `${this.major}_${this.minor}_${this.patch}`;
}
}
function isIOS() {
const ua = navigator.userAgent;
return ua.indexOf('iPhone') > 0 || ua.indexOf('iPad') > 0;
}
function getIOSSemever() {
if (isIOS()) {
const extract = navigator.userAgent.match(/OS (\d+)_(\d+)_?(\d+)?/);
return new Semver(
parseInt(extract[1] || 0, 10),
parseInt(extract[2] || 0, 10),
parseInt(extract[3] || 0, 10)
);
} else {
return null; // or [0,0,0]
}
}
const BASE_COMPONENT_COUNT = 14;
class UncompressedSplatArray {
static OFFSET = {
X: 0,
Y: 1,
Z: 2,
SCALE0: 3,
SCALE1: 4,
SCALE2: 5,
ROTATION0: 6,
ROTATION1: 7,
ROTATION2: 8,
ROTATION3: 9,
FDC0: 10,
FDC1: 11,
FDC2: 12,
OPACITY: 13,
FRC0: 14,
FRC1: 15,
FRC2: 16,
FRC3: 17,
FRC4: 18,
FRC5: 19,
FRC6: 20,
FRC7: 21,
FRC8: 22,
FRC9: 23,
FRC10: 24,
FRC11: 25,
FRC12: 26,
FRC13: 27,
FRC14: 28,
FRC15: 29,
FRC16: 30,
FRC17: 31,
FRC18: 32,
FRC19: 33,
FRC20: 34,
FRC21: 35,
FRC22: 36,
FRC23: 37
};
constructor(sphericalHarmonicsDegree = 0) {
this.sphericalHarmonicsDegree = sphericalHarmonicsDegree;
this.sphericalHarmonicsCount = getSphericalHarmonicsComponentCountForDegree(this.sphericalHarmonicsDegree);
this.componentCount = this.sphericalHarmonicsCount + BASE_COMPONENT_COUNT;
this.defaultSphericalHarmonics = new Array(this.sphericalHarmonicsCount).fill(0);
this.splats = [];
this.splatCount = 0;
}
static createSplat(sphericalHarmonicsDegree = 0) {
const baseSplat = [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0];
let shEntries = getSphericalHarmonicsComponentCountForDegree(sphericalHarmonicsDegree);
for (let i = 0; i < shEntries; i++) baseSplat.push(0);
return baseSplat;
}
addSplat(splat) {
this.splats.push(splat);
this.splatCount++;
}
getSplat(index) {
return this.splats[index];
}
addDefaultSplat() {
const newSplat = UncompressedSplatArray.createSplat(this.sphericalHarmonicsDegree);
this.addSplat(newSplat);
return newSplat;
}
addSplatFromComonents(x, y, z, scale0, scale1, scale2, rot0, rot1, rot2, rot3, r, g, b, opacity, ...rest) {
const newSplat = [x, y, z, scale0, scale1, scale2, rot0, rot1, rot2, rot3, r, g, b, opacity, ...this.defaultSphericalHarmonics];
for (let i = 0; i < rest.length && i < this.sphericalHarmonicsCount; i++) {
newSplat[i] = rest[i];
}
this.addSplat(newSplat);
return newSplat;
}
addSplatFromArray(src, srcIndex) {
const srcSplat = src.splats[srcIndex];
const newSplat = UncompressedSplatArray.createSplat(this.sphericalHarmonicsDegree);
for (let i = 0; i < this.componentCount && i < srcSplat.length; i++) {
newSplat[i] = srcSplat[i];
}
this.addSplat(newSplat);
}
}
class Constants {
static DefaultSplatSortDistanceMapPrecision = 16;
static MemoryPageSize = 65536;
static BytesPerFloat = 4;
static BytesPerInt = 4;
static MaxScenes = 32;
static ProgressiveLoadSectionSize = 262144;
static ProgressiveLoadSectionDelayDuration = 15;
static SphericalHarmonics8BitCompressionRange = 3;
}
const DefaultSphericalHarmonics8BitCompressionRange = Constants.SphericalHarmonics8BitCompressionRange;
const DefaultSphericalHarmonics8BitCompressionHalfRange = DefaultSphericalHarmonics8BitCompressionRange / 2.0;
const toHalfFloat = THREE.DataUtils.toHalfFloat.bind(THREE.DataUtils);
const fromHalfFloat$1 = THREE.DataUtils.fromHalfFloat.bind(THREE.DataUtils);
const toUncompressedFloat = (f, compressionLevel, isSH = false, range8BitMin, range8BitMax) => {
if (compressionLevel === 0) {
return f;
} else if (compressionLevel === 1 || compressionLevel === 2 && !isSH) {
return THREE.DataUtils.fromHalfFloat(f);
} else if (compressionLevel === 2) {
return fromUint8(f, range8BitMin, range8BitMax);
}
};
const toUint8 = (v, rangeMin, rangeMax) => {
v = clamp(v, rangeMin, rangeMax);
const range = (rangeMax - rangeMin);
return clamp(Math.floor((v - rangeMin) / range * 255), 0, 255);
};
const fromUint8 = (v, rangeMin, rangeMax) => {
const range = (rangeMax - rangeMin);
return (v / 255 * range + rangeMin);
};
const fromHalfFloatToUint8 = (v, rangeMin, rangeMax) => {
return toUint8(fromHalfFloat$1(v, rangeMin, rangeMax));
};
const fromUint8ToHalfFloat = (v, rangeMin, rangeMax) => {
return toHalfFloat(fromUint8(v, rangeMin, rangeMax));
};
const dataViewFloatForCompressionLevel = (dataView, floatIndex, compressionLevel, isSH = false) => {
if (compressionLevel === 0) {
return dataView.getFloat32(floatIndex * 4, true);
} else if (compressionLevel === 1 || compressionLevel === 2 && !isSH) {
return dataView.getUint16(floatIndex * 2, true);
} else {
return dataView.getUint8(floatIndex, true);
}
};
const convertBetweenCompressionLevels = function() {
const noop = (v) => v;
return function(val, fromLevel, toLevel, isSH = false) {
if (fromLevel === toLevel) return val;
let outputConversionFunc = noop;
if (fromLevel === 2 && isSH) {
if (toLevel === 1) outputConversionFunc = fromUint8ToHalfFloat;
else if (toLevel == 0) {
outputConversionFunc = fromUint8;
}
} else if (fromLevel === 2 || fromLevel === 1) {
if (toLevel === 0) outputConversionFunc = fromHalfFloat$1;
else if (toLevel == 2) {
if (!isSH) outputConversionFunc = noop;
else outputConversionFunc = fromHalfFloatToUint8;
}
} else if (fromLevel === 0) {
if (toLevel === 1) outputConversionFunc = toHalfFloat;
else if (toLevel == 2) {
if (!isSH) outputConversionFunc = toHalfFloat;
else outputConversionFunc = toUint8;
}
}
return outputConversionFunc(val);
};
}();
const copyBetweenBuffers = (srcBuffer, srcOffset, destBuffer, destOffset, byteCount = 0) => {
const src = new Uint8Array(srcBuffer, srcOffset);
const dest = new Uint8Array(destBuffer, destOffset);
for (let i = 0; i < byteCount; i++) {
dest[i] = src[i];
}
};
/**
* SplatBuffer: Container for splat data from a single scene/file and capable of (mediocre) compression.
*/
class SplatBuffer {
static CurrentMajorVersion = 0;
static CurrentMinorVersion = 1;
static CenterComponentCount = 3;
static ScaleComponentCount = 3;
static RotationComponentCount = 4;
static ColorComponentCount = 4;
static CovarianceComponentCount = 6;
static SplatScaleOffsetFloat = 3;
static SplatRotationOffsetFloat = 6;
static CompressionLevels = {
0: {
BytesPerCenter: 12,
BytesPerScale: 12,
BytesPerRotation: 16,
BytesPerColor: 4,
ScaleOffsetBytes: 12,
RotationffsetBytes: 24,
ColorOffsetBytes: 40,
SphericalHarmonicsOffsetBytes: 44,
ScaleRange: 1,
BytesPerSphericalHarmonicsComponent: 4,
SphericalHarmonicsOffsetFloat: 11,
SphericalHarmonicsDegrees: {
0: { BytesPerSplat: 44 },
1: { BytesPerSplat: 80 },
2: { BytesPerSplat: 140 }
},
},
1: {
BytesPerCenter: 6,
BytesPerScale: 6,
BytesPerRotation: 8,
BytesPerColor: 4,
ScaleOffsetBytes: 6,
RotationffsetBytes: 12,
ColorOffsetBytes: 20,
SphericalHarmonicsOffsetBytes: 24,
ScaleRange: 32767,
BytesPerSphericalHarmonicsComponent: 2,
SphericalHarmonicsOffsetFloat: 12,
SphericalHarmonicsDegrees: {
0: { BytesPerSplat: 24 },
1: { BytesPerSplat: 42 },
2: { BytesPerSplat: 72 }
},
},
2: {
BytesPerCenter: 6,
BytesPerScale: 6,
BytesPerRotation: 8,
BytesPerColor: 4,
ScaleOffsetBytes: 6,
RotationffsetBytes: 12,
ColorOffsetBytes: 20,
SphericalHarmonicsOffsetBytes: 24,
ScaleRange: 32767,
BytesPerSphericalHarmonicsComponent: 1,
SphericalHarmonicsOffsetFloat: 12,
SphericalHarmonicsDegrees: {
0: { BytesPerSplat: 24 },
1: { BytesPerSplat: 33 },
2: { BytesPerSplat: 48 }
},
}
};
static CovarianceSizeFloats = 6;
static HeaderSizeBytes = 4096;
static SectionHeaderSizeBytes = 1024;
static BucketStorageSizeBytes = 12;
static BucketStorageSizeFloats = 3;
static BucketBlockSize = 5.0;
static BucketSize = 256;
constructor(bufferData, secLoadedCountsToMax = true) {
this.constructFromBuffer(bufferData, secLoadedCountsToMax);
}
getSplatCount() {
return this.splatCount;
}
getMaxSplatCount() {
return this.maxSplatCount;
}
getMinSphericalHarmonicsDegree() {
let minSphericalHarmonicsDegree = 0;
for (let i = 0; i < this.sections.length; i++) {
const section = this.sections[i];
if (i === 0 || section.sphericalHarmonicsDegree < minSphericalHarmonicsDegree) {
minSphericalHarmonicsDegree = section.sphericalHarmonicsDegree;
}
}
return minSphericalHarmonicsDegree;
}
getBucketIndex(section, localSplatIndex) {
let bucketIndex;
const maxSplatIndexInFullBuckets = section.fullBucketCount * section.bucketSize;
if (localSplatIndex < maxSplatIndexInFullBuckets) {
bucketIndex = Math.floor(localSplatIndex / section.bucketSize);
} else {
let bucketSplatIndex = maxSplatIndexInFullBuckets;
bucketIndex = section.fullBucketCount;
let partiallyFullBucketIndex = 0;
while (bucketSplatIndex < section.splatCount) {
let currentPartiallyFilledBucketSize = section.partiallyFilledBucketLengths[partiallyFullBucketIndex];
if (localSplatIndex >= bucketSplatIndex && localSplatIndex < bucketSplatIndex + currentPartiallyFilledBucketSize) {
break;
}
bucketSplatIndex += currentPartiallyFilledBucketSize;
bucketIndex++;
partiallyFullBucketIndex++;
}
}
return bucketIndex;
}
getSplatCenter(globalSplatIndex, outCenter, transform) {
const sectionIndex = this.globalSplatIndexToSectionMap[globalSplatIndex];
const section = this.sections[sectionIndex];
const localSplatIndex = globalSplatIndex - section.splatCountOffset;
const srcSplatCentersBase = section.bytesPerSplat * localSplatIndex;
const dataView = new DataView(this.bufferData, section.dataBase + srcSplatCentersBase);
const x = dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel);
const y = dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel);
const z = dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel);
if (this.compressionLevel >= 1) {
const bucketIndex = this.getBucketIndex(section, localSplatIndex);
const bucketBase = bucketIndex * SplatBuffer.BucketStorageSizeFloats;
const sf = section.compressionScaleFactor;
const sr = section.compressionScaleRange;
outCenter.x = (x - sr) * sf + section.bucketArray[bucketBase];
outCenter.y = (y - sr) * sf + section.bucketArray[bucketBase + 1];
outCenter.z = (z - sr) * sf + section.bucketArray[bucketBase + 2];
} else {
outCenter.x = x;
outCenter.y = y;
outCenter.z = z;
}
if (transform) outCenter.applyMatrix4(transform);
}
getSplatScaleAndRotation = function() {
const scaleMatrix = new THREE.Matrix4();
const rotationMatrix = new THREE.Matrix4();
const tempMatrix = new THREE.Matrix4();
const tempPosition = new THREE.Vector3();
const scale = new THREE.Vector3();
const rotation = new THREE.Quaternion();
return function(index, outScale, outRotation, transform, scaleOverride) {
const sectionIndex = this.globalSplatIndexToSectionMap[index];
const section = this.sections[sectionIndex];
const localSplatIndex = index - section.splatCountOffset;
const srcSplatScalesBase = section.bytesPerSplat * localSplatIndex +
SplatBuffer.CompressionLevels[this.compressionLevel].ScaleOffsetBytes;
const dataView = new DataView(this.bufferData, section.dataBase + srcSplatScalesBase);
scale.set(toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel), this.compressionLevel));
if (scaleOverride) {
if (scaleOverride.x !== undefined) scale.x = scaleOverride.x;
if (scaleOverride.y !== undefined) scale.y = scaleOverride.y;
if (scaleOverride.z !== undefined) scale.z = scaleOverride.z;
}
rotation.set(toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 4, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 5, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 6, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 3, this.compressionLevel), this.compressionLevel));
if (transform) {
scaleMatrix.makeScale(scale.x, scale.y, scale.z);
rotationMatrix.makeRotationFromQuaternion(rotation);
tempMatrix.copy(scaleMatrix).multiply(rotationMatrix).multiply(transform);
tempMatrix.decompose(tempPosition, outRotation, outScale);
} else {
outScale.copy(scale);
outRotation.copy(rotation);
}
};
}();
getSplatColor(globalSplatIndex, outColor) {
const sectionIndex = this.globalSplatIndexToSectionMap[globalSplatIndex];
const section = this.sections[sectionIndex];
const localSplatIndex = globalSplatIndex - section.splatCountOffset;
const srcSplatColorsBase = section.bytesPerSplat * localSplatIndex +
SplatBuffer.CompressionLevels[this.compressionLevel].ColorOffsetBytes;
const splatColorsArray = new Uint8Array(this.bufferData, section.dataBase + srcSplatColorsBase, 4);
outColor.set(splatColorsArray[0], splatColorsArray[1],
splatColorsArray[2], splatColorsArray[3]);
}
fillSplatCenterArray(outCenterArray, transform, srcFrom, srcTo, destFrom) {
const splatCount = this.splatCount;
srcFrom = srcFrom || 0;
srcTo = srcTo || splatCount - 1;
if (destFrom === undefined) destFrom = srcFrom;
const center = new THREE.Vector3();
for (let i = srcFrom; i <= srcTo; i++) {
const sectionIndex = this.globalSplatIndexToSectionMap[i];
const section = this.sections[sectionIndex];
const localSplatIndex = i - section.splatCountOffset;
const centerDestBase = (i - srcFrom + destFrom) * SplatBuffer.CenterComponentCount;
const srcSplatCentersBase = section.bytesPerSplat * localSplatIndex;
const dataView = new DataView(this.bufferData, section.dataBase + srcSplatCentersBase);
const x = dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel);
const y = dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel);
const z = dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel);
if (this.compressionLevel >= 1) {
const bucketIndex = this.getBucketIndex(section, localSplatIndex);
const bucketBase = bucketIndex * SplatBuffer.BucketStorageSizeFloats;
const sf = section.compressionScaleFactor;
const sr = section.compressionScaleRange;
center.x = (x - sr) * sf + section.bucketArray[bucketBase];
center.y = (y - sr) * sf + section.bucketArray[bucketBase + 1];
center.z = (z - sr) * sf + section.bucketArray[bucketBase + 2];
} else {
center.x = x;
center.y = y;
center.z = z;
}
if (transform) {
center.applyMatrix4(transform);
}
outCenterArray[centerDestBase] = center.x;
outCenterArray[centerDestBase + 1] = center.y;
outCenterArray[centerDestBase + 2] = center.z;
}
}
fillSplatScaleRotationArray = function() {
const scaleMatrix = new THREE.Matrix4();
const rotationMatrix = new THREE.Matrix4();
const tempMatrix = new THREE.Matrix4();
const scale = new THREE.Vector3();
const rotation = new THREE.Quaternion();
const tempPosition = new THREE.Vector3();
const ensurePositiveW = (quaternion) => {
const flip = quaternion.w < 0 ? -1 : 1;
quaternion.x *= flip;
quaternion.y *= flip;
quaternion.z *= flip;
quaternion.w *= flip;
};
return function(outScaleArray, outRotationArray, transform, srcFrom, srcTo, destFrom,
desiredOutputCompressionLevel, scaleOverride) {
const splatCount = this.splatCount;
srcFrom = srcFrom || 0;
srcTo = srcTo || splatCount - 1;
if (destFrom === undefined) destFrom = srcFrom;
const outputConversion = (value, srcCompressionLevel) => {
if (srcCompressionLevel === undefined) srcCompressionLevel = this.compressionLevel;
return convertBetweenCompressionLevels(value, srcCompressionLevel, desiredOutputCompressionLevel);
};
for (let i = srcFrom; i <= srcTo; i++) {
const sectionIndex = this.globalSplatIndexToSectionMap[i];
const section = this.sections[sectionIndex];
const localSplatIndex = i - section.splatCountOffset;
const srcSplatScalesBase = section.bytesPerSplat * localSplatIndex +
SplatBuffer.CompressionLevels[this.compressionLevel].ScaleOffsetBytes;
const scaleDestBase = (i - srcFrom + destFrom) * SplatBuffer.ScaleComponentCount;
const rotationDestBase = (i - srcFrom + destFrom) * SplatBuffer.RotationComponentCount;
const dataView = new DataView(this.bufferData, section.dataBase + srcSplatScalesBase);
const srcScaleX = (scaleOverride && scaleOverride.x !== undefined) ? scaleOverride.x :
dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel);
const srcScaleY = (scaleOverride && scaleOverride.y !== undefined) ? scaleOverride.y :
dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel);
const srcScaleZ = (scaleOverride && scaleOverride.z !== undefined) ? scaleOverride.z :
dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel);
const srcRotationW = dataViewFloatForCompressionLevel(dataView, 3, this.compressionLevel);
const srcRotationX = dataViewFloatForCompressionLevel(dataView, 4, this.compressionLevel);
const srcRotationY = dataViewFloatForCompressionLevel(dataView, 5, this.compressionLevel);
const srcRotationZ = dataViewFloatForCompressionLevel(dataView, 6, this.compressionLevel);
scale.set(toUncompressedFloat(srcScaleX, this.compressionLevel),
toUncompressedFloat(srcScaleY, this.compressionLevel),
toUncompressedFloat(srcScaleZ, this.compressionLevel));
rotation.set(toUncompressedFloat(srcRotationX, this.compressionLevel),
toUncompressedFloat(srcRotationY, this.compressionLevel),
toUncompressedFloat(srcRotationZ, this.compressionLevel),
toUncompressedFloat(srcRotationW, this.compressionLevel)).normalize();
if (transform) {
tempPosition.set(0, 0, 0);
scaleMatrix.makeScale(scale.x, scale.y, scale.z);
rotationMatrix.makeRotationFromQuaternion(rotation);
tempMatrix.identity().premultiply(scaleMatrix).premultiply(rotationMatrix);
tempMatrix.premultiply(transform);
tempMatrix.decompose(tempPosition, rotation, scale);
rotation.normalize();
}
ensurePositiveW(rotation);
if (outScaleArray) {
outScaleArray[scaleDestBase] = outputConversion(scale.x, 0);
outScaleArray[scaleDestBase + 1] = outputConversion(scale.y, 0);
outScaleArray[scaleDestBase + 2] = outputConversion(scale.z, 0);
}
if (outRotationArray) {
outRotationArray[rotationDestBase] = outputConversion(rotation.x, 0);
outRotationArray[rotationDestBase + 1] = outputConversion(rotation.y, 0);
outRotationArray[rotationDestBase + 2] = outputConversion(rotation.z, 0);
outRotationArray[rotationDestBase + 3] = outputConversion(rotation.w, 0);
}
}
};
}();
static computeCovariance = function() {
const tempMatrix4 = new THREE.Matrix4();
const scaleMatrix = new THREE.Matrix3();
const rotationMatrix = new THREE.Matrix3();
const covarianceMatrix = new THREE.Matrix3();
const transformedCovariance = new THREE.Matrix3();
const transform3x3 = new THREE.Matrix3();
const transform3x3Transpose = new THREE.Matrix3();
return function(scale, rotation, transform, outCovariance, outOffset = 0, desiredOutputCompressionLevel) {
tempMatrix4.makeScale(scale.x, scale.y, scale.z);
scaleMatrix.setFromMatrix4(tempMatrix4);
tempMatrix4.makeRotationFromQuaternion(rotation);
rotationMatrix.setFromMatrix4(tempMatrix4);
covarianceMatrix.copy(rotationMatrix).multiply(scaleMatrix);
transformedCovariance.copy(covarianceMatrix).transpose().premultiply(covarianceMatrix);
if (transform) {
transform3x3.setFromMatrix4(transform);
transform3x3Transpose.copy(transform3x3).transpose();
transformedCovariance.multiply(transform3x3Transpose);
transformedCovariance.premultiply(transform3x3);
}
if (desiredOutputCompressionLevel >= 1) {
outCovariance[outOffset] = toHalfFloat(transformedCovariance.elements[0]);
outCovariance[outOffset + 1] = toHalfFloat(transformedCovariance.elements[3]);
outCovariance[outOffset + 2] = toHalfFloat(transformedCovariance.elements[6]);
outCovariance[outOffset + 3] = toHalfFloat(transformedCovariance.elements[4]);
outCovariance[outOffset + 4] = toHalfFloat(transformedCovariance.elements[7]);
outCovariance[outOffset + 5] = toHalfFloat(transformedCovariance.elements[8]);
} else {
outCovariance[outOffset] = transformedCovariance.elements[0];
outCovariance[outOffset + 1] = transformedCovariance.elements[3];
outCovariance[outOffset + 2] = transformedCovariance.elements[6];
outCovariance[outOffset + 3] = transformedCovariance.elements[4];
outCovariance[outOffset + 4] = transformedCovariance.elements[7];
outCovariance[outOffset + 5] = transformedCovariance.elements[8];
}
};
}();
fillSplatCovarianceArray(covarianceArray, transform, srcFrom, srcTo, destFrom, desiredOutputCompressionLevel) {
const splatCount = this.splatCount;
const scale = new THREE.Vector3();
const rotation = new THREE.Quaternion();
srcFrom = srcFrom || 0;
srcTo = srcTo || splatCount - 1;
if (destFrom === undefined) destFrom = srcFrom;
for (let i = srcFrom; i <= srcTo; i++) {
const sectionIndex = this.globalSplatIndexToSectionMap[i];
const section = this.sections[sectionIndex];
const localSplatIndex = i - section.splatCountOffset;
const covarianceDestBase = (i - srcFrom + destFrom) * SplatBuffer.CovarianceComponentCount;
const srcSplatScalesBase = section.bytesPerSplat * localSplatIndex +
SplatBuffer.CompressionLevels[this.compressionLevel].ScaleOffsetBytes;
const dataView = new DataView(this.bufferData, section.dataBase + srcSplatScalesBase);
scale.set(toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 0, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 1, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 2, this.compressionLevel), this.compressionLevel));
rotation.set(toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 4, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 5, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 6, this.compressionLevel), this.compressionLevel),
toUncompressedFloat(dataViewFloatForCompressionLevel(dataView, 3, this.compressionLevel), this.compressionLevel));
SplatBuffer.computeCovariance(scale, rotation, transform, covarianceArray, covarianceDestBase, desiredOutputCompressionLevel);
}
}
fillSplatColorArray(outColorArray, minimumAlpha, srcFrom, srcTo, destFrom) {
const splatCount = this.splatCount;
srcFrom = srcFrom || 0;
srcTo = srcTo || splatCount - 1;
if (destFrom === undefined) destFrom = srcFrom;
for (let i = srcFrom; i <= srcTo; i++) {
const sectionIndex = this.globalSplatIndexToSectionMap[i];
const section = this.sections[sectionIndex];
const localSplatIndex = i - section.splatCountOffset;
const colorDestBase = (i - srcFrom + destFrom) * SplatBuffer.ColorComponentCount;
const srcSplatColorsBase = section.bytesPerSplat * localSplatIndex +
SplatBuffer.CompressionLevels[this.compressionLevel].ColorOffsetBytes;
const dataView = new Uint8Array(this.bufferData, section.dataBase + srcSplatColorsBase);
let alpha = dataView[3];
alpha = (alpha >= minimumAlpha) ? alpha : 0;
outColorArray[colorDestBase] = dataView[0];
outColorArray[colorDestBase + 1] = dataView[1];
outColorArray[colorDestBase + 2] = dataView[2];
outColorArray[colorDestBase + 3] = alpha;
}
}
fillSphericalHarmonicsArray = function() {
const sphericalHarmonicVectors = [];
for (let i = 0; i < 15; i++) {
sphericalHarmonicVectors[i] = new THREE.Vector3();
}
const tempMatrix3 = new THREE.Matrix3();
const tempMatrix4 = new THREE.Matrix4();
const tempTranslation = new THREE.Vector3();
const tempScale = new THREE.Vector3();
const tempRotation = new THREE.Quaternion();
const sh11 = [];
const sh12 = [];
const sh13 = [];
const sh21 = [];
const sh22 = [];
const sh23 = [];
const sh24 = [];
const sh25 = [];
const shIn1 = [];
const shIn2 = [];
const shIn3 = [];
const shIn4 = [];
const shIn5 = [];
const shOut1 = [];
const shOut2 = [];
const shOut3 = [];
const shOut4 = [];
const shOut5 = [];
const noop = (v) => v;
const set3 = (array, val1, val2, val3) => {
array[0] = val1;
array[1] = val2;
array[2] = val3;
};
const set3FromArray = (array, srcDestView, stride, srcBase, compressionLevel) => {
array[0] = dataViewFloatForCompressionLevel(srcDestView, srcBase, compressionLevel, true);
array[1] = dataViewFloatForCompressionLevel(srcDestView, srcBase + stride, compressionLevel, true);
array[2] = dataViewFloatForCompressionLevel(srcDestView, srcBase + stride + stride, compressionLevel, true);
};
const copy3 = (srcArray, destArray) => {
destArray[0] = srcArray[0];
destArray[1] = srcArray[1];
destArray[2] = srcArray[2];
};
const setOutput3 = (srcArray, destArray, destBase, conversionFunc) => {
destArray[destBase] = conversionFunc(srcArray[0]);
destArray[destBase + 1] = conversionFunc(srcArray[1]);
destArray[destBase + 2] = conversionFunc(srcArray[2]);
};
const toUncompressedFloatArray3 = (src, dest, compressionLevel, range8BitMin, range8BitMax) => {
dest[0] = toUncompressedFloat(src[0], compressionLevel, true, range8BitMin, range8BitMax);
dest[1] = toUncompressedFloat(src[1], compressionLevel, true, range8BitMin, range8BitMax);
dest[2] = toUncompressedFloat(src[2], compressionLevel, true, range8BitMin, range8BitMax);
return dest;
};
return function(outSphericalHarmonicsArray, outSphericalHarmonicsDegree, transform,
srcFrom, srcTo, destFrom, desiredOutputCompressionLevel) {
const splatCount = this.splatCount;
srcFrom = srcFrom || 0;
srcTo = srcTo || splatCount - 1;
if (destFrom === undefined) destFrom = srcFrom;
if (transform && outSphericalHarmonicsDegree >= 1) {
tempMatrix4.copy(transform);
tempMatrix4.decompose(tempTranslation, tempRotation, tempScale);
tempRotation.normalize();
tempMatrix4.makeRotationFromQuaternion(tempRotation);
tempMatrix3.setFromMatrix4(tempMatrix4);
set3(sh11, tempMatrix3.elements[4], -tempMatrix3.elements[7], tempMatrix3.elements[1]);
set3(sh12, -tempMatrix3.elements[5], tempMatrix3.elements[8], -tempMatrix3.elements[2]);
set3(sh13, tempMatrix3.elements[3], -tempMatrix3.elements[6], tempMatrix3.elements[0]);
}
const localFromHalfFloatToUint8 = (v) => {
return fromHalfFloatToUint8(v, this.minSphericalHarmonicsCoeff, this.maxSphericalHarmonicsCoeff);
};
const localToUint8 = (v) => {
return toUint8(v, this.minSphericalHarmonicsCoeff, this.maxSphericalHarmonicsCoeff);
};
for (let i = srcFrom; i <= srcTo; i++) {
const sectionIndex = this.globalSplatIndexToSectionMap[i];
const section = this.sections[sectionIndex];
outSphericalHarmonicsDegree = Math.min(outSphericalHarmonicsDegree, section.sphericalHarmonicsDegree);
const outSphericalHarmonicsComponentsCount = getSphericalHarmonicsComponentCountForDegree(outSphericalHarmonicsDegree);
const localSplatIndex = i - section.splatCountOffset;
const srcSplatSHBase = section.bytesPerSplat * localSplatIndex +
SplatBuffer.CompressionLevels[this.compressionLevel].SphericalHarmonicsOffsetBytes;
const dataView = new DataView(this.bufferData, section.dataBase + srcSplatSHBase);
const shDestBase = (i - srcFrom + destFrom) * outSphericalHarmonicsComponentsCount;
let compressionLevelForOutputConversion = transform ? 0 : this.compressionLevel;
let outputConversionFunc = noop;
if (compressionLevelForOutputConversion !== desiredOutputCompressionLevel) {
if (compressionLevelForOutputConversion === 1) {
if (desiredOutputCompressionLevel === 0) outputConversionFunc = fromHalfFloat$1;
else if (desiredOutputCompressionLevel == 2) outputConversionFunc = localFromHalfFloatToUint8;
} else if (compressionLevelForOutputConversion === 0) {
if (desiredOutputCompressionLevel === 1) outputConversionFunc = toHalfFloat;
else if (desiredOutputCompressionLevel == 2) outputConversionFunc = localToUint8;
}
}
const minShCoeff = this.minSphericalHarmonicsCoeff;
const maxShCoeff = this.maxSphericalHarmonicsCoeff;
if (outSphericalHarmonicsDegree >= 1) {
set3FromArray(shIn1, dataView, 3, 0, this.compressionLevel);
set3FromArray(shIn2, dataView, 3, 1, this.compressionLevel);
set3FromArray(shIn3, dataView, 3, 2, this.compressionLevel);
if (transform) {
toUncompressedFloatArray3(shIn1, shIn1, this.compressionLevel, minShCoeff, maxShCoeff);
toUncompressedFloatArray3(shIn2, shIn2, this.compressionLevel, minShCoeff, maxShCoeff);
toUncompressedFloatArray3(shIn3, shIn3, this.compressionLevel, minShCoeff, maxShCoeff);
SplatBuffer.rotateSphericalHarmonics3(shIn1, shIn2, shIn3, sh11, sh12, sh13, shOut1, shOut2, shOut3);
} else {
copy3(shIn1, shOut1);
copy3(shIn2, shOut2);
copy3(shIn3, shOut3);
}
setOutput3(shOut1, outSphericalHarmonicsArray, shDestBase, outputConversionFunc);
setOutput3(shOut2, outSphericalHarmonicsArray, shDestBase + 3, outputConversionFunc);
setOutput3(shOut3, outSphericalHarmonicsArray, shDestBase + 6, outputConversionFunc);
if (outSphericalHarmonicsDegree >= 2) {
set3FromArray(shIn1, dataView, 5, 9, this.compressionLevel);
set3FromArray(shIn2, dataView, 5, 10, this.compressionLevel);
set3FromArray(shIn3, dataView, 5, 11, this.compressionLevel);
set3FromArray(shIn4, dataView, 5, 12, this.compressionLevel);
set3FromArray(shIn5, dataView, 5, 13, this.compressionLevel);
if (transform) {
toUncompressedFloatArray3(shIn1, shIn1, this.compressionLevel, minShCoeff, maxShCoeff);
toUncompressedFloatArray3(shIn2, shIn2, this.compressionLevel, minShCoeff, maxShCoeff);
toUncompressedFloatArray3(shIn3, shIn3, this.compressionLevel, minShCoeff, maxShCoeff);
toUncompressedFloatArray3(shIn4, shIn4, this.compressionLevel, minShCoeff, maxShCoeff);
toUncompressedFloatArray3(shIn5, shIn5, this.compressionLevel, minShCoeff, maxShCoeff);
SplatBuffer.rotateSphericalHarmonics5(shIn1, shIn2, shIn3, shIn4, shIn5,
sh11, sh12, sh13, sh21, sh22, sh23, sh24, sh25,
shOut1, shOut2, shOut3, shOut4, shOut5);
} else {
copy3(shIn1, shOut1);
copy3(shIn2, shOut2);
copy3(shIn3, shOut3);
copy3(shIn4, shOut4);
copy3(shIn5, shOut5);
}
setOutput3(shOut1, outSphericalHarmonicsArray, shDestBase + 9, outputConversionFunc);
setOutput3(shOut2, outSphericalHarmonicsArray, shDestBase + 12, outputConversionFunc);
setOutput3(shOut3, outSphericalHarmonicsArray, shDestBase + 15, outputConversionFunc);
setOutput3(shOut4, outSphericalHarmonicsArray, shDestBase + 18, outputConversionFunc);
setOutput3(shOut5, outSphericalHarmonicsArray, shDestBase + 21, outputConversionFunc);
}
}
}
};
}();
static dot3 = (v1, v2, v3, transformRow, outArray) => {
outArray[0] = outArray[1] = outArray[2] = 0;
const t0 = transformRow[0];
const t1 = transformRow[1];
const t2 = transformRow[2];
SplatBuffer.addInto3(v1[0] * t0, v1[1] * t0, v1[2] * t0, outArray);
SplatBuffer.addInto3(v2[0] * t1, v2[1] * t1, v2[2] * t1, outArray);
SplatBuffer.addInto3(v3[0] * t2, v3[1] * t2, v3[2] * t2, outArray);
};
static addInto3 = (val1, val2, val3, destArray) => {
destArray[0] = destArray[0] + val1;
destArray[1] = destArray[1] + val2;
destArray[2] = destArray[2] + val3;
};
static dot5 = (v1, v2, v3, v4, v5, transformRow, outArray) => {
outArray[0] = outArray[1] = outArray[2] = 0;
const t0 = transformRow[0];
const t1 = transformRow[1];
const t2 = transformRow[2];
const t3 = transformRow[3];
const t4 = transformRow[4];
SplatBuffer.addInto3(v1[0] * t0, v1[1] * t0, v1[2] * t0, outArray);
SplatBuffer.addInto3(v2[0] * t1, v2[1] * t1, v2[2] * t1, outArray);
SplatBuffer.addInto3(v3[0] * t2, v3[1] * t2, v3[2] * t2, outArray);
SplatBuffer.addInto3(v4[0] * t3, v4[1] * t3, v4[2] * t3, outArray);
SplatBuffer.addInto3(v5[0] * t4, v5[1] * t4, v5[2] * t4, outArray);
};
static rotateSphericalHarmonics3 = (in1, in2, in3, tsh11, tsh12, tsh13, out1, out2, out3) => {
SplatBuffer.dot3(in1, in2, in3, tsh11, out1);
SplatBuffer.dot3(in1, in2, in3, tsh12, out2);
SplatBuffer.dot3(in1, in2, in3, tsh13, out3);
};
static rotateSphericalHarmonics5 = (in1, in2, in3, in4, in5, tsh11, tsh12, tsh13,
tsh21, tsh22, tsh23, tsh24, tsh25, out1, out2, out3, out4, out5) => {
const kSqrt0104 = Math.sqrt(1.0 / 4.0);
const kSqrt0304 = Math.sqrt(3.0 / 4.0);
const kSqrt0103 = Math.sqrt(1.0 / 3.0);
const kSqrt0403 = Math.sqrt(4.0 / 3.0);
const kSqrt0112 = Math.sqrt(1.0 / 12.0);
tsh21[0] = kSqrt0104 * ((tsh13[2] * tsh11[0] + tsh13[0] * tsh11[2]) + (tsh11[2] * tsh13[0] + tsh11[0] * tsh13[2]));
tsh21[1] = (tsh13[1] * tsh11[0] + tsh11[1] * tsh13[0]);
tsh21[2] = kSqrt0304 * (tsh13[1] * tsh11[1] + tsh11[1] * tsh13[1]);
tsh21[3] = (tsh13[1] * tsh11[2] + tsh11[1] * tsh13[2]);
tsh21[4] = kSqrt0104 * ((tsh13[2] * tsh11[2] - tsh13[0] * tsh11[0]) + (tsh11[2] * tsh13[2] - tsh11[0] * tsh13[0]));
SplatBuffer.dot5(in1, in2, in3, in4, in5, tsh21, out1);
tsh22[0] = kSqrt0104 * ((tsh12[2] * tsh11[0] + tsh12[0] * tsh11[2]) + (tsh11[2] * tsh12[0] + tsh11[0] * tsh12[2]));
tsh22[1] = tsh12[1] * tsh11[0] + tsh11[1] * tsh12[0];
tsh22[2] = kSqrt0304 * (tsh12[1] * tsh11[1] + tsh11[1] * tsh12[1]);
tsh22[3] = tsh12[1] * tsh11[2] + tsh11[1] * tsh12[2];
tsh22[4] = kSqrt0104 * ((tsh12[2] * tsh11[2] - tsh12[0] * tsh11[0]) + (tsh11[2] * tsh12[2] - tsh11[0] * tsh12[0]));
SplatBuffer.dot5(in1, in2, in3, in4, in5, tsh22, out2);
tsh23[0] = kSqrt0103 * (tsh12[2] * tsh12[0] + tsh12[0] * tsh12[2]) + -kSqrt0112 *
((tsh13[2] * tsh13[0] + tsh13[0] * tsh13[2]) + (tsh11[2] * tsh11[0] + tsh11[0] * tsh11[2]));
tsh23[1] = kSqrt0403 * tsh12[1] * tsh12[0] + -kSqrt0103 * (tsh13[1] * tsh13[0] + tsh11[1] * tsh11[0]);
tsh23[2] = tsh12[1] * tsh12[1] + -kSqrt0104 * (tsh13[1] * tsh13[1] + tsh11[1] * tsh11[1]);
tsh23[3] = kSqrt0403 * tsh12[1] * tsh12[2] + -kSqrt0103 * (tsh13[1] * tsh13[2] + tsh11[1] * tsh11[2]);
tsh23[4] = kSqrt0103 * (tsh12[2] * tsh12[2] - tsh12[0] * tsh12[0]) + -