From 58343c93b851a98064dff48e8f130469e257be9a Mon Sep 17 00:00:00 2001 From: Ryan Tremblay Date: Mon, 18 Nov 2024 06:22:02 -0800 Subject: [PATCH] Viewer suspend rendering when not visible (#15835) --- packages/tools/viewer-alpha/src/viewer.ts | 68 +++++-- .../tools/viewer-alpha/src/viewerFactory.ts | 17 +- .../viewer-alpha/test/apps/web/index.html | 184 +++++++++--------- 3 files changed, 164 insertions(+), 105 deletions(-) diff --git a/packages/tools/viewer-alpha/src/viewer.ts b/packages/tools/viewer-alpha/src/viewer.ts index 2a751d6198b..d097a1d58b3 100644 --- a/packages/tools/viewer-alpha/src/viewer.ts +++ b/packages/tools/viewer-alpha/src/viewer.ts @@ -140,6 +140,12 @@ export type ViewerDetails = { * Provides access to the currently loaded model. */ model: Nullable; + + /** + * Suspends the render loop. + * @returns A token that should be disposed when the request for suspending rendering is no longer needed. + */ + suspendRendering(): IDisposable; }; export type ViewerOptions = Partial< @@ -284,8 +290,8 @@ export class Viewer implements IDisposable { private readonly _details: ViewerDetails; private readonly _snapshotHelper: SnapshotRenderingHelper; private readonly _autoRotationBehavior: AutoRotationBehavior; - private readonly _renderLoopController: IDisposable; private readonly _imageProcessingConfigurationObserver: Observer; + private _renderLoopController: Nullable = null; private _skybox: Nullable = null; private _skyboxBlur: number = 0.3; private _light: Nullable = null; @@ -294,6 +300,7 @@ export class Viewer implements IDisposable { private _contrast: number; private _exposure: number; + private _suspendRenderCount = 0; private _isDisposed = false; private readonly _loadModelLock = new AsyncLock(); @@ -358,6 +365,7 @@ export class Viewer implements IDisposable { scene, camera, model: null, + suspendRendering: this._suspendRendering.bind(this), }; } this._details.scene.skipFrustumClipping = true; @@ -375,19 +383,7 @@ export class Viewer implements IDisposable { // Load a default light, but ignore errors as the user might be immediately loading their own environment. this.resetEnvironment().catch(() => {}); - // TODO: render at least back ground. Maybe we can only run renderloop when a mesh is loaded. What to render until then? - const render = () => { - this._details.scene.render(); - if (this.isAnimationPlaying) { - this.onAnimationProgressChanged.notifyObservers(); - this._autoRotationBehavior.resetLastInteractionTime(); - } - }; - - this._engine.runRenderLoop(render); - this._renderLoopController = { - dispose: () => this._engine.stopRenderLoop(render), - }; + this._beginRendering(); options?.onInitialized?.(this._details); } @@ -823,7 +819,7 @@ export class Viewer implements IDisposable { this._loadEnvironmentAbortController?.abort("Thew viewer is being disposed."); this._loadModelAbortController?.abort("Thew viewer is being disposed."); - this._renderLoopController.dispose(); + this._renderLoopController?.dispose(); this._details.scene.dispose(); this.onEnvironmentChanged.clear(); @@ -894,6 +890,48 @@ export class Viewer implements IDisposable { return true; } + private _suspendRendering(): IDisposable { + this._renderLoopController?.dispose(); + this._suspendRenderCount++; + let disposed = false; + return { + dispose: () => { + if (!disposed) { + disposed = true; + this._suspendRenderCount--; + if (this._suspendRenderCount === 0) { + this._beginRendering(); + } + } + }, + }; + } + + private _beginRendering(): void { + if (!this._renderLoopController) { + const render = () => { + this._details.scene.render(); + if (this.isAnimationPlaying) { + this.onAnimationProgressChanged.notifyObservers(); + this._autoRotationBehavior.resetLastInteractionTime(); + } + }; + + this._engine.runRenderLoop(render); + + let disposed = false; + this._renderLoopController = { + dispose: () => { + if (!disposed) { + disposed = true; + this._engine.stopRenderLoop(render); + this._renderLoopController = null; + } + }, + }; + } + } + private _updateCamera(interpolate = false): void { // Enable camera's behaviors this._details.camera.useFramingBehavior = true; diff --git a/packages/tools/viewer-alpha/src/viewerFactory.ts b/packages/tools/viewer-alpha/src/viewerFactory.ts index 0a1d528c330..4ce3d795962 100644 --- a/packages/tools/viewer-alpha/src/viewerFactory.ts +++ b/packages/tools/viewer-alpha/src/viewerFactory.ts @@ -1,5 +1,5 @@ // eslint-disable-next-line import/no-internal-modules -import type { AbstractEngine, AbstractEngineOptions, EngineOptions, WebGPUEngineOptions } from "core/index"; +import type { AbstractEngine, AbstractEngineOptions, EngineOptions, IDisposable, Nullable, WebGPUEngineOptions } from "core/index"; import type { ViewerOptions } from "./viewer"; import { Viewer } from "./viewer"; @@ -69,6 +69,21 @@ export async function createViewerForCanvas(canvas: HTMLCanvasElement, options?: }); disposeActions.push(() => beforeRenderObserver.remove()); + // If the canvas is not visible, suspend rendering. + let offscreenRenderingSuspension: Nullable = null; + const interactionObserver = new IntersectionObserver((entries) => { + if (entries.length > 0) { + if (entries[entries.length - 1].isIntersecting) { + offscreenRenderingSuspension?.dispose(); + offscreenRenderingSuspension = null; + } else { + offscreenRenderingSuspension = details.suspendRendering(); + } + } + }); + interactionObserver.observe(canvas); + disposeActions.push(() => interactionObserver.disconnect()); + // Call the original onInitialized callback, if one was provided. onInitialized?.(details); }; diff --git a/packages/tools/viewer-alpha/test/apps/web/index.html b/packages/tools/viewer-alpha/test/apps/web/index.html index bdc3c1a06d0..34eccd90ea0 100644 --- a/packages/tools/viewer-alpha/test/apps/web/index.html +++ b/packages/tools/viewer-alpha/test/apps/web/index.html @@ -12,31 +12,15 @@ height: 100%; padding: 0; margin: 0; - overflow: hidden; } - .toggle-dom-button { + .button-container { position: absolute; - top: 10px; - right: 10px; - } - - .toggle-engine-button { - position: absolute; - top: 40px; - right: 10px; - } - - .toggle-hotspot-button { - position: absolute; - top: 70px; - right: 10px; - } - - .toggle-inspector-button { - position: absolute; - top: 100px; + top: 50px; right: 10px; + display: flex; + flex-direction: column; + gap: 10px; } .lineContainer { @@ -85,68 +69,75 @@ - - - - - - -
- - - -
-
- -
Thruster
-
- -
World Space POI
-
-
- - - - - + +
+ + + + + + +
+ + + +
+
+ +
Thruster
+
+ +
World Space POI
+
+
+
+ +
+ + + + + + +