Skip to content

Commit

Permalink
Viewer suspend rendering when not visible (#15835)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryantrem authored Nov 18, 2024
1 parent a5b4476 commit 58343c9
Show file tree
Hide file tree
Showing 3 changed files with 164 additions and 105 deletions.
68 changes: 53 additions & 15 deletions packages/tools/viewer-alpha/src/viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,12 @@ export type ViewerDetails = {
* Provides access to the currently loaded model.
*/
model: Nullable<AssetContainer>;

/**
* 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<
Expand Down Expand Up @@ -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<ImageProcessingConfiguration>;
private _renderLoopController: Nullable<IDisposable> = null;
private _skybox: Nullable<Mesh> = null;
private _skyboxBlur: number = 0.3;
private _light: Nullable<HemisphericLight> = null;
Expand All @@ -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();
Expand Down Expand Up @@ -358,6 +365,7 @@ export class Viewer implements IDisposable {
scene,
camera,
model: null,
suspendRendering: this._suspendRendering.bind(this),
};
}
this._details.scene.skipFrustumClipping = true;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
17 changes: 16 additions & 1 deletion packages/tools/viewer-alpha/src/viewerFactory.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<IDisposable> = 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);
};
Expand Down
184 changes: 95 additions & 89 deletions packages/tools/viewer-alpha/test/apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -85,68 +69,75 @@
</head>

<body ondragover="event.preventDefault()" ondrop="onDrop(event)">
<babylon-viewer
engine="WebGPU"
source="https://raw.githubusercontent.com/BabylonJS/Assets/master/meshes/ufo.glb"
environment="../../../../../public/@babylonjs/viewer-alpha/assets/photoStudio.env"
animation-speed="1.5"
selected-animation="1"
camera-orbit="auto auto auto"
camera-target="auto auto auto"
hotspots='{
"antenna": {
"type": "surface",
"meshIndex": 1,
"pointIndex": [94, 93, 76],
"barycentric": [0.391, 0.387, 0.223],
"cameraOrbit": [1.270, 1.424, 0.514]
},
"thruster": {
"type": "surface",
"meshIndex": 1,
"pointIndex": [8339, 8338, 8337],
"barycentric": [0.213, 0.220, 0.567],
"cameraOrbit": [-5.595, 2.398, 0.787]
},
"hotspot 3": {
"type": "surface",
"meshIndex": 1,
"pointIndex": [228, 113, 111],
"barycentric": [0.217, 0.341, 0.442]
},
"poi": {
"type": "world",
"position": [0.37435858930052035, 0.5999379610676736, 0.021725763097220963],
"normal": [0.6903377419741442, 0.44314472456113957, -0.5718885862645555],
"cameraOrbit": [-32.108, 1.112, 1.714]
}
}'
>
<!-- <div slot="tool-bar" style="position: absolute; top: 12px; left: 12px; width: 100px; height: 36px">
<button onclick="document.querySelector('babylon-viewer').toggleAnimation()">Toggle Animation</button>
</div> -->
<svg id="lines" style="position: absolute; width: 100%; height: 100%" xmlns="http://www.w3.org/2000/svg" class="lineContainer">
<line class="line"></line>
</svg>
<babylon-viewer-annotation hotspot="antenna">
<div>
<svg style="width: 20px; height: 20px; transform: translate(-50%, -50%)">
<ellipse cx="10" cy="10" rx="8" ry="8" fill="red" stroke="white" stroke-width="3" />
</svg>
</div>
</babylon-viewer-annotation>
<babylon-viewer-annotation hotspot="thruster">
<div style="background-color: aliceblue; border-radius: 8px; padding: 3px 6px;">Thruster</div>
</babylon-viewer-annotation>
<babylon-viewer-annotation hotspot="poi">
<div style="background-color: aliceblue; border-radius: 8px; padding: 3px 6px;">World Space POI</div>
</babylon-viewer-annotation>
</babylon-viewer>
<button class="toggle-dom-button" onclick="onToggleDOM()">Toggle DOM</button>
<button class="toggle-engine-button" onclick="onToggleEngine()">Toggle Engine</button>

<button id="anchor-button" class="toggle-hotspot-button" onclick="onToggleHotSpot()">Toggle Hot Spot</button>
<button id="inspector-button" class="toggle-inspector-button" onclick="onToggleInspector()">Toggle Inspector</button>
<div id="moreUpperPageContent" style="width: 100%; height: 200vh; display: none"></div>
<div id="viewerContainer" style="width: 100vw; height: 100vh">
<babylon-viewer
engine="WebGPU"
source="https://raw.githubusercontent.com/BabylonJS/Assets/master/meshes/ufo.glb"
environment="../../../../../public/@babylonjs/viewer-alpha/assets/photoStudio.env"
animation-speed="1.5"
selected-animation="1"
camera-orbit="auto auto auto"
camera-target="auto auto auto"
hotspots='{
"antenna": {
"type": "surface",
"meshIndex": 1,
"pointIndex": [94, 93, 76],
"barycentric": [0.391, 0.387, 0.223],
"cameraOrbit": [1.270, 1.424, 0.514]
},
"thruster": {
"type": "surface",
"meshIndex": 1,
"pointIndex": [8339, 8338, 8337],
"barycentric": [0.213, 0.220, 0.567],
"cameraOrbit": [-5.595, 2.398, 0.787]
},
"hotspot 3": {
"type": "surface",
"meshIndex": 1,
"pointIndex": [228, 113, 111],
"barycentric": [0.217, 0.341, 0.442]
},
"poi": {
"type": "world",
"position": [0.37435858930052035, 0.5999379610676736, 0.021725763097220963],
"normal": [0.6903377419741442, 0.44314472456113957, -0.5718885862645555],
"cameraOrbit": [-32.108, 1.112, 1.714]
}
}'
>
<!-- <div slot="tool-bar" style="position: absolute; top: 12px; left: 12px; width: 100px; height: 36px">
<button onclick="document.querySelector('babylon-viewer').toggleAnimation()">Toggle Animation</button>
</div> -->
<svg id="lines" style="position: absolute; width: 100%; height: 100%" xmlns="http://www.w3.org/2000/svg" class="lineContainer">
<line class="line"></line>
</svg>
<babylon-viewer-annotation hotspot="antenna">
<div>
<svg style="width: 20px; height: 20px; transform: translate(-50%, -50%)">
<ellipse cx="10" cy="10" rx="8" ry="8" fill="red" stroke="white" stroke-width="3" />
</svg>
</div>
</babylon-viewer-annotation>
<babylon-viewer-annotation hotspot="thruster">
<div style="background-color: aliceblue; border-radius: 8px; padding: 3px 6px;">Thruster</div>
</babylon-viewer-annotation>
<babylon-viewer-annotation hotspot="poi">
<div style="background-color: aliceblue; border-radius: 8px; padding: 3px 6px;">World Space POI</div>
</babylon-viewer-annotation>
</babylon-viewer>
</div>
<div id="moreLowerPageContent" style="width: 100%; height: 200vh; display: none"></div>
<div class="button-container">
<button onclick="onToggleDOM()">Toggle DOM</button>
<button onclick="onToggleEngine()">Toggle Engine</button>
<button id="anchor-button" onclick="onToggleHotSpot()">Toggle Hot Spot</button>
<button id="inspector-button" onclick="onToggleInspector()">Toggle Inspector</button>
<button onClick="onToggleTopContent()">Toggle Top Content ⬆️</button>
<button onClick="onToggleBottomContent()">Toggle Bottom Content ⬇️</button>
</div>
<script type="module" src="/packages/tools/viewer-alpha/src/index.ts"></script>
<script type="module">
import { Inspector } from "inspector/inspector";
Expand Down Expand Up @@ -182,11 +173,11 @@
// Alternatively, we could just await import("/packages/tools/viewer-alpha/src/index.ts") here instead.
await customElements.whenDefined("babylon-viewer");

setInterval(() => {
//console.log(viewerElement.skyboxBlur);
//viewerElement.skyboxBlur = (Number(viewerElement.skyboxBlur) + 0.01) % 1;
//console.log(viewerElement.skyboxBlur);
}, 16);
// setInterval(() => {
// console.log(viewerElement.skyboxBlur);
// viewerElement.skyboxBlur = (Number(viewerElement.skyboxBlur) + 0.01) % 1;
// console.log(viewerElement.skyboxBlur);
// }, 16);

await new Promise((resolve) => setTimeout(resolve, 2000));
//viewerElement.source = "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Models/main/2.0/BrainStem/glTF-Binary/BrainStem.glb";
Expand Down Expand Up @@ -222,7 +213,8 @@
}

globalThis.onToggleDOM = () => {
isViewerConnected ? document.body.removeChild(viewerElement) : document.body.appendChild(viewerElement);
const viewerContainer = document.getElementById("viewerContainer");
isViewerConnected ? viewerContainer.removeChild(viewerElement) : viewerContainer.appendChild(viewerElement);
isViewerConnected = !isViewerConnected;
}

Expand Down Expand Up @@ -252,6 +244,20 @@
}
isInspectorVisible = !isInspectorVisible;
}

let isTopContentVisible = false;
globalThis.onToggleTopContent = () => {
const topContent = document.getElementById("moreUpperPageContent");
topContent.style.display = isTopContentVisible ? "none" : "block";
isTopContentVisible = !isTopContentVisible;
}

let isBottomContentVisible = false;
globalThis.onToggleBottomContent = () => {
const bottomContent = document.getElementById("moreLowerPageContent");
bottomContent.style.display = isBottomContentVisible ? "none" : "block";
isBottomContentVisible = !isBottomContentVisible;
}
</script>
</body>
</html>

0 comments on commit 58343c9

Please sign in to comment.