From 369425ec8096f1ad85bc5662614a172649950de7 Mon Sep 17 00:00:00 2001 From: Mark Kellogg Date: Sat, 9 Mar 2024 18:27:44 -0800 Subject: [PATCH] Added mode to render only when needed --- README.md | 4 ++- src/RenderMode.js | 5 +++ src/SceneHelper.js | 4 +++ src/SplatMesh.js | 2 ++ src/Viewer.js | 84 ++++++++++++++++++++++++++++++++++++++++------ src/index.js | 4 ++- 6 files changed, 91 insertions(+), 12 deletions(-) create mode 100644 src/RenderMode.js diff --git a/README.md b/README.md index 1e71bdfe..0cf314d0 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 00000000..ac0b0a94 --- /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 986acb58..c9a4f0cf 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 63066730..0d071ea9 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 b7e608d8..ae2fbfa2 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 0dc576dc..64b29353 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 };