diff --git a/README.md b/README.md index 1e71bdf..0cf314d 100644 --- a/README.md +++ b/README.md @@ -259,7 +259,8 @@ const viewer = new GaussianSplats3D.Viewer({ 'sharedMemoryForWorkers': true, 'integerBasedSort': true, 'dynamicScene': false, - 'webXRMode': GaussianSplats3D.WebXRMode.None + 'webXRMode': GaussianSplats3D.WebXRMode.None, + 'renderMode': GaussianSplats3D.RenderMode.OnChange }); viewer.addSplatScene('') .then(() => { @@ -289,6 +290,7 @@ Advanced `Viewer` parameters | `integerBasedSort` | Tells the sorting web worker to use the integer versions of relevant data to compute the distance of splats from the camera. Since integer arithmetic is faster than floating point, this reduces sort time. However it can result in integer overflows in larger scenes so it should only be used for small scenes. Defaults to `true`. | `dynamicScene` | Tells the viewer to not make any optimizations that depend on the scene being static. Additionally all splat data retrieved from the viewer's splat mesh will not have their respective scene transform applied to them by default. | `webXRMode` | Tells the viewer whether or not to enable built-in Web VR or Web AR. Valid values are defined in the `WebXRMode` enum: `None`, `VR`, and `AR`. Defaults to `None`. +| `renderMode` | Controls when the viewer renders the scene. Valid values are defined in the `RenderMode` enum: `Always`, `OnChange`, and `Never`. Defaults to `Always`.
### Creating KSPLAT files diff --git a/src/RenderMode.js b/src/RenderMode.js new file mode 100644 index 0000000..ac0b0a9 --- /dev/null +++ b/src/RenderMode.js @@ -0,0 +1,5 @@ +export const RenderMode = { + Always: 0, + OnChange: 1, + Never: 2 +}; diff --git a/src/SceneHelper.js b/src/SceneHelper.js index 986acb5..c9a4f0c 100644 --- a/src/SceneHelper.js +++ b/src/SceneHelper.js @@ -129,6 +129,10 @@ export class SceneHelper { this.meshCursor.visible = visible; } + getMeschCursorVisibility() { + return this.meshCursor.visible; + } + setMeshCursorPosition(position) { this.meshCursor.position.copy(position); } diff --git a/src/SplatMesh.js b/src/SplatMesh.js index 6306673..0d071ea 100644 --- a/src/SplatMesh.js +++ b/src/SplatMesh.js @@ -72,6 +72,7 @@ export class SplatMesh extends THREE.Mesh { this.maxRadius = 0; this.visibleRegionRadius = 0; this.visibleRegionFadeStartRadius = 0; + this.visibleRegionChanging = false; this.disposed = false; } @@ -921,6 +922,7 @@ export class SplatMesh extends THREE.Mesh { this.material.uniforms.currentTime.value = performance.now(); this.material.uniforms.fadeInComplete.value = fadeInComplete; this.material.uniformsNeedUpdate = true; + this.visibleRegionChanging = !fadeInComplete; } /** diff --git a/src/Viewer.js b/src/Viewer.js index b7e608d..ae2fbfa 100644 --- a/src/Viewer.js +++ b/src/Viewer.js @@ -20,13 +20,14 @@ import { VRButton } from './webxr/VRButton.js'; import { ARButton } from './webxr/ARButton.js'; import { delayedExecute } from './Util.js'; import { LoaderStatus } from './loaders/LoaderStatus.js'; - +import { RenderMode } from './RenderMode.js'; const THREE_CAMERA_FOV = 50; const MINIMUM_DISTANCE_TO_NEW_FOCAL_POINT = .75; const MIN_SPLAT_COUNT_TO_SHOW_SPLAT_TREE_LOADING_SPINNER = 1500000; const FOCUS_MARKER_FADE_IN_SPEED = 10.0; const FOCUS_MARKER_FADE_OUT_SPEED = 2.5; +const CONSECUTIVE_RENDERED_FRAMES_FOR_FPS_CALCULATION = 60; /** * Viewer: Manages the rendering of splat scenes. Manages an instance of SplatMesh as well as a web worker @@ -121,6 +122,8 @@ export class Viewer { this.gpuAcceleratedSort = false; } + this.renderMode = options.renderMode || RenderMode.Always; + this.controls = null; this.showMeshCursor = false; @@ -147,6 +150,7 @@ export class Viewer { this.currentFPS = 0; this.lastSortTime = 0; + this.consecutiveRenderFrames = 0; this.previousCameraTarget = new THREE.Vector3(); this.nextCameraTarget = new THREE.Vector3(); @@ -283,6 +287,10 @@ export class Viewer { } } + setRenderMode(renderMode) { + this.renderMode = renderMode; + } + onKeyDown = function() { const forward = new THREE.Vector3(); @@ -941,6 +949,7 @@ export class Viewer { this.sortPromiseResolver(); this.sortPromise = null; this.sortPromiseResolver = null; + this.forceRenderNextFrame(); if (sortCount === 0) { this.runAfterFirstSort.forEach((func) => { func(); @@ -1088,13 +1097,60 @@ export class Viewer { this.requestFrameId = requestAnimationFrame(this.selfDrivenUpdateFunc); } this.update(); - this.render(); + if (this.shouldRender()) { + this.render(); + this.consecutiveRenderFrames++; + } else { + this.consecutiveRenderFrames = 0; + } + this.renderNextFrame = false; } + forceRenderNextFrame() { + this.renderNextFrame = true; + } + + shouldRender = function() { + + let renderCount = 0; + const lastCameraPosition = new THREE.Vector3(); + const lastCameraOrientation = new THREE.Quaternion(); + const changeEpsilon = 0.0001; + + return function() { + let shouldRender = false; + let cameraChanged = false; + if (this.camera) { + const cp = this.camera.position; + const co = this.camera.quaternion; + cameraChanged = Math.abs(cp.x - lastCameraPosition.x) > changeEpsilon || + Math.abs(cp.y - lastCameraPosition.y) > changeEpsilon || + Math.abs(cp.z - lastCameraPosition.z) > changeEpsilon || + Math.abs(co.x - lastCameraOrientation.x) > changeEpsilon || + Math.abs(co.y - lastCameraOrientation.y) > changeEpsilon || + Math.abs(co.z - lastCameraOrientation.z) > changeEpsilon || + Math.abs(co.w - lastCameraOrientation.w) > changeEpsilon; + } + + shouldRender = this.renderMode !== RenderMode.Never && (renderCount === 0 || this.splatMesh.visibleRegionChanging || + cameraChanged || this.renderMode === RenderMode.Always || this.dynamicMode === true || this.renderNextFrame); + + if (this.camera) { + lastCameraPosition.copy(this.camera.position); + lastCameraOrientation.copy(this.camera.quaternion); + } + + renderCount++; + return shouldRender; + }; + + }(); + render = function() { return function() { if (!this.initialized || !this.splatRenderingInitialized) return; + const hasRenderables = (threeScene) => { for (let child of threeScene.children) { if (child.visible) return true; @@ -1141,14 +1197,18 @@ export class Viewer { let frameCount = 0; return function() { - const currentTime = getCurrentTime(); - const calcDelta = currentTime - lastCalcTime; - if (calcDelta >= 1.0) { - this.currentFPS = frameCount; - frameCount = 0; - lastCalcTime = currentTime; + if (this.consecutiveRenderFrames > CONSECUTIVE_RENDERED_FRAMES_FOR_FPS_CALCULATION) { + const currentTime = getCurrentTime(); + const calcDelta = currentTime - lastCalcTime; + if (calcDelta >= 1.0) { + this.currentFPS = frameCount; + frameCount = 0; + lastCalcTime = currentTime; + } else { + frameCount++; + } } else { - frameCount++; + this.currentFPS = null; } }; @@ -1227,6 +1287,7 @@ export class Viewer { this.sceneHelper.setFocusMarkerOpacity(newFocusMarkerOpacity); this.sceneHelper.updateFocusMarker(this.nextCameraTarget, this.camera, renderDimensions); wasTransitioning = true; + this.forceRenderNextFrame(); } else { let currentFocusMarkerOpacity; if (wasTransitioning) currentFocusMarkerOpacity = 1.0; @@ -1237,6 +1298,7 @@ export class Viewer { this.sceneHelper.setFocusMarkerOpacity(newFocusMarkerOpacity); if (newFocusMarkerOpacity === 0.0) this.sceneHelper.setFocusMarkerVisibility(false); } + if (currentFocusMarkerOpacity > 0.0) this.forceRenderNextFrame(); wasTransitioning = false; } }; @@ -1250,6 +1312,7 @@ export class Viewer { return function() { if (this.showMeshCursor) { + this.forceRenderNextFrame(); this.getRenderDimensions(renderDimensions); outHits.length = 0; this.raycaster.setFromCameraAndScreenPosition(this.camera, this.mousePosition, renderDimensions); @@ -1261,6 +1324,7 @@ export class Viewer { this.sceneHelper.setMeshCursorVisibility(false); } } else { + if (this.sceneHelper.getMeschCursorVisibility()) this.forceRenderNextFrame(); this.sceneHelper.setMeshCursorVisibility(false); } }; @@ -1279,7 +1343,7 @@ export class Viewer { const meshCursorPosition = this.showMeshCursor ? this.sceneHelper.meshCursor.position : null; const splatRenderCountPct = this.splatRenderCount / splatCount * 100; this.infoPanel.update(renderDimensions, this.camera.position, cameraLookAtPosition, - this.camera.up, meshCursorPosition, this.currentFPS, splatCount, + this.camera.up, meshCursorPosition, this.currentFPS || 'N/A', splatCount, this.splatRenderCount, splatRenderCountPct, this.lastSortTime); }; diff --git a/src/index.js b/src/index.js index 0dc576d..64b2935 100644 --- a/src/index.js +++ b/src/index.js @@ -13,6 +13,7 @@ import { OrbitControls } from './OrbitControls.js'; import { AbortablePromise } from './AbortablePromise.js'; import { SceneFormat } from './loaders/SceneFormat.js'; import { WebXRMode } from './webxr/WebXRMode.js'; +import { RenderMode } from './RenderMode.js'; export { PlyParser, @@ -29,5 +30,6 @@ export { OrbitControls, AbortablePromise, SceneFormat, - WebXRMode + WebXRMode, + RenderMode };