Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Viewer suspend rendering when not visible #15835

Merged
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();
sebavan marked this conversation as resolved.
Show resolved Hide resolved
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>