diff --git a/src/Viewer.js b/src/Viewer.js index 79a1f462..b51f69c5 100644 --- a/src/Viewer.js +++ b/src/Viewer.js @@ -307,12 +307,12 @@ export class Viewer { this.rootElement.style.position = 'absolute'; document.body.appendChild(this.rootElement); } else { - this.rootElement = this.renderer.domElement || document.body; + this.rootElement = this.renderer.domElement.parentElement || document.body; } } - this.setupCamera(); this.setupRenderer(); + this.setupCamera(); this.setupWebXR(this.webXRSessionInit); this.setupControls(); this.setupEventHandlers(); @@ -333,7 +333,7 @@ export class Viewer { setupCamera() { if (!this.usingExternalCamera) { const renderDimensions = new THREE.Vector2(); - this.getRenderDimensions(renderDimensions); + this.renderer.getSize(renderDimensions); this.perspectiveCamera = new THREE.PerspectiveCamera(THREE_CAMERA_FOV, renderDimensions.x / renderDimensions.y, 0.1, 1000); this.orthographicCamera = new THREE.OrthographicCamera(renderDimensions.x / -2, renderDimensions.x / 2, @@ -348,7 +348,7 @@ export class Viewer { setupRenderer() { if (!this.usingExternalRenderer) { const renderDimensions = new THREE.Vector2(); - this.getRenderDimensions(renderDimensions); + this.getRendererContainerDimensions(renderDimensions); this.renderer = new THREE.WebGLRenderer({ antialias: false, @@ -360,7 +360,7 @@ export class Viewer { this.renderer.setSize(renderDimensions.x, renderDimensions.y); this.resizeObserver = new ResizeObserver(() => { - this.getRenderDimensions(renderDimensions); + this.getRendererContainerDimensions(renderDimensions); this.renderer.setSize(renderDimensions.x, renderDimensions.y); this.forceRenderNextFrame(); }); @@ -560,7 +560,7 @@ export class Viewer { return function() { if (!this.transitioningCameraTarget) { - this.getRenderDimensions(renderDimensions); + this.renderer.getSize(renderDimensions); outHits.length = 0; this.raycaster.setFromCameraAndScreenPosition(this.camera, this.mousePosition, renderDimensions); this.raycaster.intersectSplatMesh(this.splatMesh, outHits); @@ -580,7 +580,7 @@ export class Viewer { }(); - getRenderDimensions(outDimensions) { + getRendererContainerDimensions(outDimensions) { if (this.rootElement) { outDimensions.x = this.rootElement.offsetWidth; outDimensions.y = this.rootElement.offsetHeight; @@ -658,7 +658,7 @@ export class Viewer { if (splatCount > 0) { this.splatMesh.updateVisibleRegionFadeDistance(this.sceneRevealMode); this.splatMesh.updateTransforms(); - this.getRenderDimensions(renderDimensions); + this.renderer.getSize(renderDimensions); const focalLengthX = this.camera.projectionMatrix.elements[0] * 0.5 * this.devicePixelRatio * renderDimensions.x; const focalLengthY = this.camera.projectionMatrix.elements[5] * 0.5 * @@ -828,6 +828,141 @@ export class Viewer { onProgress, hideLoadingUI.bind(this), options.headers); } + /** + * Add a splat scene to the viewer from a file and display any loading UI if appropriate. + * @param {File} file File containing the splats + * @param {object} options { + * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified + * value (valid range: 0 - 255), defaults to 1 + * + * showLoadingUI: Display a loading spinner while the scene is loading, defaults to true + * + * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] + * + * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] + * + * scale (Array): Scene's scale, defaults to [1, 1, 1] + * + * onProgress: Function to be called as file data are received, or other processing occurs + * + * } + * @return {AbortablePromise} + **/ + + addSplatSceneFromFile(file, options = {}) { + const loadFileDataPromise = new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (_loadEvent) => { + if (reader.readyState == FileReader.DONE) { + resolve(reader.result); + } + } + reader.onerror = reject; + reader.readAsArrayBuffer(file); + }) + return this.addSplatSceneFromPromise(file.name, loadFileDataPromise, options); + } + + /** + * Add a splat scene to the viewer from an arraybuffer promise and display any loading UI if appropriate. + * @param {Promise} arrayBufferPromise Promise the splats + * @param {object} options { + * splatAlphaRemovalThreshold: Ignore any splats with an alpha less than the specified + * value (valid range: 0 - 255), defaults to 1 + * + * showLoadingUI: Display a loading spinner while the scene is loading, defaults to true + * + * position (Array): Position of the scene, acts as an offset from its default position, defaults to [0, 0, 0] + * + * rotation (Array): Rotation of the scene represented as a quaternion, defaults to [0, 0, 0, 1] + * + * scale (Array): Scene's scale, defaults to [1, 1, 1] + * + * onProgress: Function to be called as file data are received, or other processing occurs + * + * } + * @return {AbortablePromise} + **/ + + addSplatSceneFromPromise(filename, arrayBufferPromise, options = {}) { + if (this.isLoadingOrUnloading()) { + throw new Error('Cannot add splat scene while another load or unload is already in progress.'); + } + + if (this.isDisposingOrDisposed()) { + throw new Error('Cannot add splat scene after dispose() is called.'); + } + + if (options.progressiveLoad && this.splatMesh.scenes && this.splatMesh.scenes.length > 0) { + console.log('addSplatScene(): "progressiveLoad" option ignore because there are multiple splat scenes'); + options.progressiveLoad = false; + } + + const format = (options.format !== undefined && options.format !== null) ? options.format : sceneFormatFromPath(filename); + const progressiveLoad = Viewer.isProgressivelyLoadable(format) && options.progressiveLoad; + const showLoadingUI = (options.showLoadingUI !== undefined && options.showLoadingUI !== null) ? options.showLoadingUI : true; + + let loadingUITaskId = null; + if (showLoadingUI) { + this.loadingSpinner.removeAllTasks(); + loadingUITaskId = this.loadingSpinner.addTask(`Loading ${filename}...`); + } + const hideLoadingUI = () => { + this.loadingProgressBar.hide(); + this.loadingSpinner.removeAllTasks(); + }; + + const onProgressUIUpdate = (percentComplete, percentCompleteLabel, loaderStatus) => { + if (showLoadingUI && loaderStatus === LoaderStatus.Processing) { + this.loadingSpinner.setMessageForTask(loadingUITaskId, `Processing splats : ${percentCompleteLabel}`); + } + }; + + let downloadDone = false; + let downloadedPercentage = 0; + const splatBuffersAddedUIUpdate = (firstBuild, finalBuild) => { + if (showLoadingUI) { + if (firstBuild && progressiveLoad || finalBuild && !progressiveLoad) { + this.loadingSpinner.removeTask(loadingUITaskId); + if (!finalBuild && !downloadDone) this.loadingProgressBar.show(); + } + if (progressiveLoad) { + if (finalBuild) { + downloadDone = true; + this.loadingProgressBar.hide(); + } else { + this.loadingProgressBar.setProgress(downloadedPercentage); + } + } + } + }; + + const onProgress = (percentComplete, percentCompleteLabel, loaderStatus) => { + downloadedPercentage = percentComplete; + onProgressUIUpdate(percentComplete, percentCompleteLabel, loaderStatus); + if (options.onProgress) options.onProgress(percentComplete, percentCompleteLabel, loaderStatus); + }; + + const buildSection = (splatBuffer, firstBuild, finalBuild) => { + if (!progressiveLoad && options.onProgress) options.onProgress(0, '0%', LoaderStatus.Processing); + const addSplatBufferOptions = { + 'rotation': options.rotation || options.orientation, + 'position': options.position, + 'scale': options.scale, + 'splatAlphaRemovalThreshold': options.splatAlphaRemovalThreshold, + }; + return this.addSplatBuffers([splatBuffer], [addSplatBufferOptions], + finalBuild, firstBuild && showLoadingUI, showLoadingUI, + progressiveLoad, progressiveLoad).then(() => { + if (!progressiveLoad && options.onProgress) options.onProgress(100, '100%', LoaderStatus.Processing); + splatBuffersAddedUIUpdate(firstBuild, finalBuild); + }); + + }; + + return this.buildSingleSplatSceneFromArrayBufferPromise(arrayBufferPromise, format, options.splatAlphaRemovalThreshold, buildSection.bind(this), onProgress, hideLoadingUI.bind(this)); + } + /** * Download a single splat scene, convert to splat buffer and then rebuild the viewer's splat mesh * by calling 'buildFunc' -- all before displaying the scene. Also sets/clears relevant instance synchronization objects, @@ -867,6 +1002,40 @@ export class Viewer { return downloadAndBuildPromise.promise; } + /** + * Load a single splat scene from a file, convert to splat buffer and then rebuild the viewer's splat mesh + * by calling 'buildFunc' -- all before displaying the scene. Also sets/clears relevant instance synchronization objects, + * and calls appropriate functions on success or failure. + * @param {Promise} arrayBufferPromise Promise the splats + * @param {SceneFormat} format Format of the splat scene file + * @param {number} splatAlphaRemovalThreshold Ignore any splats with an alpha less than the specified value (valid range: 0 - 255) + * @param {function} buildFunc Function to build the viewer's splat mesh with the downloaded splat buffer + * @param {function} onProgress Function to be called as file data are received, or other processing occurs + * @param {function} onException Function to be called when exception occurs + * @return {AbortablePromise} + */ + buildSingleSplatSceneFromArrayBufferPromise(arrayBufferPromise, format, splatAlphaRemovalThreshold, buildFunc, onProgress, onException) { + // Build a promise that read the file data, and call onException on error + const loadAndBuildPromise = arrayBufferPromise + .then((arrayBuffer) => { + // Read the data from the file + let buildScenePromise = this.loadSplatSceneFromFileToSplatBuffer(arrayBuffer, splatAlphaRemovalThreshold, onProgress, false, undefined, format); + return buildScenePromise; + }) + .then((splatBuffer) => { + // Construct a scene from the splatBuffer + return buildFunc(splatBuffer, true, true) + }) + .catch((e) => { + console.error(e) + if (onException) onException(); + this.clearSplatSceneDownloadAndBuildPromise(); + const error = (e instanceof AbortedPromiseError) ? e : new Error(`Viewer::addSplatScene -> Could not load file`); + }); + + return loadAndBuildPromise; + } + /** * Download a single splat scene and convert to splat buffer in a progressive manner, allowing rendering as the file downloads. * As each section is downloaded, the viewer's splat mesh is rebuilt by calling 'buildFunc' @@ -1059,22 +1228,20 @@ export class Viewer { * @return {AbortablePromise} */ downloadSplatSceneToSplatBuffer(path, splatAlphaRemovalThreshold = 1, onProgress = undefined, - progressiveBuild = false, onSectionBuilt = undefined, format, headers) { + progressiveBuild = false, onSectionBuilt = undefined, format) { + + const optimizeSplatData = progressiveBuild ? false : this.optimizeSplatData; try { - 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); + if (format === SceneFormat.Splat) { + return SplatLoader.loadFromURL(path, onProgress, progressiveBuild, + onSectionBuilt, splatAlphaRemovalThreshold, + this.inMemoryCompressionLevel, optimizeSplatData); + } else if (format === SceneFormat.KSplat) { + return KSplatLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt); + } else if (format === SceneFormat.Ply) { + return PlyLoader.loadFromURL(path, onProgress, progressiveBuild, onSectionBuilt, + splatAlphaRemovalThreshold, this.inMemoryCompressionLevel, + optimizeSplatData, this.sphericalHarmonicsDegree); } } catch (e) { throw this.updateError(e, null); @@ -1083,6 +1250,36 @@ export class Viewer { throw new Error(`Viewer::downloadSplatSceneToSplatBuffer -> File format not supported: ${path}`); } + /** + * Load a splat scene from an ArrayBuffer and convert to SplatBuffer instance. + * @param {ArrayBuffer} fileData The data of the file + * @param {number} splatAlphaRemovalThreshold Ignore any splats with an alpha less than the specified + * value (valid range: 0 - 255), defaults to 1 + * + * @param {function} onProgress Function to be called as file data are received + * @param {boolean} progressiveBuild Construct file sections into splat buffers as they are downloaded + * @param {function} onSectionBuilt Function to be called when new section is added to the file + * @param {string} format File format of the scene + * @return {AbortablePromise} + */ + loadSplatSceneFromFileToSplatBuffer(fileData, splatAlphaRemovalThreshold = 1, onProgress = undefined, + progressiveBuild = false, onSectionBuilt = undefined, format) { + const optimizeSplatData = progressiveBuild ? false : this.optimizeSplatData; + try { + if (format === SceneFormat.Splat) { + return SplatLoader.loadFromFileData(fileData, splatAlphaRemovalThreshold, this.inMemoryCompressionLevel, optimizeSplatData); + } else if (format === SceneFormat.KSplat) { + return KSplatLoader.loadFromFileData(fileData); + } else if (format === SceneFormat.Ply) { + return PlyLoader.loadFromFileData(fileData, splatAlphaRemovalThreshold, this.inMemoryCompressionLevel, this.sphericalHarmonicsDegree); + } + throw new Error(`Viewer::LoadSplatSceneFromFileToSplatBuffer -> File format not supported`); + } catch (e) { + throw this.updateError(e, null); + } + + } + static isProgressivelyLoadable(format) { return format === SceneFormat.Splat || format === SceneFormat.KSplat || format === SceneFormat.Ply; } @@ -1749,7 +1946,7 @@ export class Viewer { let wasTransitioning = false; return function(timeDelta) { - this.getRenderDimensions(renderDimensions); + this.renderer.getSize(renderDimensions); if (this.transitioningCameraTarget) { this.sceneHelper.setFocusMarkerVisibility(true); const currentFocusMarkerOpacity = Math.max(this.sceneHelper.getFocusMarkerOpacity(), 0.0); @@ -1783,7 +1980,7 @@ export class Viewer { return function() { if (this.showMeshCursor) { this.forceRenderNextFrame(); - this.getRenderDimensions(renderDimensions); + this.renderer.getSize(renderDimensions); outHits.length = 0; this.raycaster.setFromCameraAndScreenPosition(this.camera, this.mousePosition, renderDimensions); this.raycaster.intersectSplatMesh(this.splatMesh, outHits); @@ -1808,7 +2005,7 @@ export class Viewer { return function() { if (!this.showInfo) return; const splatCount = this.splatMesh.getSplatCount(); - this.getRenderDimensions(renderDimensions); + this.renderer.getSize(renderDimensions); const cameraLookAtPosition = this.controls ? this.controls.target : null; const meshCursorPosition = this.showMeshCursor ? this.sceneHelper.meshCursor.position : null; const splatRenderCountPct = splatCount > 0 ? this.splatRenderCount / splatCount * 100 : 0; @@ -1986,7 +2183,7 @@ export class Viewer { return function(gatherAllNodes = false) { - this.getRenderDimensions(renderDimensions); + this.renderer.getSize(renderDimensions); const cameraFocalLength = (renderDimensions.y / 2.0) / Math.tan(this.camera.fov / 2.0 * THREE.MathUtils.DEG2RAD); const fovXOver2 = Math.atan(renderDimensions.x / 2.0 / cameraFocalLength); const fovYOver2 = Math.atan(renderDimensions.y / 2.0 / cameraFocalLength); diff --git a/src/loaders/spz/SpzLoader.js b/src/loaders/spz/SpzLoader.js index abcd25b9..120c2efd 100644 --- a/src/loaders/spz/SpzLoader.js +++ b/src/loaders/spz/SpzLoader.js @@ -355,8 +355,11 @@ export class SpzLoader { static loadFromURL(fileName, onProgress, minimumAlpha, compressionLevel, optimizeSplatData = true, outSphericalHarmonicsDegree = 0, headers, sectionSize, sceneCenter, blockSize, bucketSize) { + const localOnProgress = (percent, percentLabel) => { + onProgress(percent, percentLabel, LoaderStatus.Downloading); + }; if (onProgress) onProgress(0, '0%', LoaderStatus.Downloading); - return fetchWithProgress(fileName, onProgress, true, headers).then((fileData) => { + return fetchWithProgress(fileName, localOnProgress, true, headers).then((fileData) => { if (onProgress) onProgress(0, '0%', LoaderStatus.Processing); return SpzLoader.loadFromFileData(fileData, minimumAlpha, compressionLevel, optimizeSplatData, outSphericalHarmonicsDegree, sectionSize, sceneCenter, blockSize, bucketSize); diff --git a/src/worker/compile_wasm_no_simd.sh b/src/worker/compile_wasm_no_simd.sh index e83c2b96..0bb55386 100755 --- a/src/worker/compile_wasm_no_simd.sh +++ b/src/worker/compile_wasm_no_simd.sh @@ -1 +1 @@ -em++ -std=c++11 sorter_no_simd.cpp -Os -s WASM=1 -s SIDE_MODULE=2 -o sorter_no_simd.wasm -s IMPORTED_MEMORY=1 -s SHARED_MEMORY=1 \ No newline at end of file +em++ -std=c++11 sorter_no_simd.cpp -Os -s WASM=1 -s SIDE_MODULE=2 -o sorter_no_simd.wasm -s IMPORTED_MEMORY=1 -s USE_PTHREADS=1 diff --git a/src/worker/sorter.cpp b/src/worker/sorter.cpp index 4c3e7d6a..c908df4c 100644 --- a/src/worker/sorter.cpp +++ b/src/worker/sorter.cpp @@ -24,6 +24,8 @@ EXTERN EMSCRIPTEN_KEEPALIVE void sortIndexes(unsigned int* indexes, void* center int maxDistance = -2147483640; int minDistance = 2147483640; + if (distanceMapRange > sortCount) distanceMapRange = sortCount; + float fMVPTRow3[4]; unsigned int sortStart = renderCount - sortCount; if (useIntegerSort) { diff --git a/src/worker/sorter.wasm b/src/worker/sorter.wasm index 83f44191..160d825e 100755 Binary files a/src/worker/sorter.wasm and b/src/worker/sorter.wasm differ diff --git a/src/worker/sorter_no_simd.cpp b/src/worker/sorter_no_simd.cpp index c07fc71a..63a5cf5e 100644 --- a/src/worker/sorter_no_simd.cpp +++ b/src/worker/sorter_no_simd.cpp @@ -23,6 +23,8 @@ EXTERN EMSCRIPTEN_KEEPALIVE void sortIndexes(unsigned int* indexes, void* center int maxDistance = -2147483640; int minDistance = 2147483640; + if (distanceMapRange > sortCount) distanceMapRange = sortCount; + float fMVPTRow3[4]; int iMVPTRow3[4]; unsigned int sortStart = renderCount - sortCount; diff --git a/src/worker/sorter_no_simd.wasm b/src/worker/sorter_no_simd.wasm index 2cfa1a1a..b336d025 100755 Binary files a/src/worker/sorter_no_simd.wasm and b/src/worker/sorter_no_simd.wasm differ diff --git a/src/worker/sorter_no_simd_non_shared.wasm b/src/worker/sorter_no_simd_non_shared.wasm index e96cbdf0..d49c9a4c 100755 Binary files a/src/worker/sorter_no_simd_non_shared.wasm and b/src/worker/sorter_no_simd_non_shared.wasm differ diff --git a/src/worker/sorter_non_shared.wasm b/src/worker/sorter_non_shared.wasm index d902c27c..febaafb0 100755 Binary files a/src/worker/sorter_non_shared.wasm and b/src/worker/sorter_non_shared.wasm differ