Reference Source

src/plugins/FastNavPlugin/FastNavPlugin.js

import {Plugin} from "../../viewer/Plugin.js";

/**
 * {@link Viewer} plugin that makes interaction smoother with large models, by temporarily switching
 * the Viewer to faster, lower-quality rendering modes whenever we interact.
 *
 * [<img src="https://xeokit.io/img/docs/FastNavPlugin/FastNavPlugin.gif">](https://xeokit.github.io/xeokit-sdk/examples/index.html#performance_FastNavPlugin)
 *
 * FastNavPlugin works by hiding specified Viewer rendering features, and optionally scaling the Viewer's canvas
 * resolution, whenever we interact with the Viewer. Then, once we've finished interacting, FastNavPlugin restores those
 * rendering features and the original canvas scale, after a configured delay.
 *
 * Depending on how we configure FastNavPlugin, we essentially switch to a smooth-rendering low-quality view while
 * interacting, then return to the normal higher-quality view after we stop, following an optional delay.
 *
 * Down-scaling the canvas resolution gives particularly good results. For example, scaling by ````0.5```` means that
 * we're rendering a quarter of the pixels while interacting, which can make the Viewer noticeably smoother with big models.
 *
 * The screen capture above shows FastNavPlugin in action. In this example, whenever we move the Camera or resize the Canvas,
 * FastNavPlugin switches off enhanced edges and ambient shadows (SAO), and down-scales the canvas, making it slightly
 * blurry. When ````0.5```` seconds passes with no interaction, the plugin shows edges and SAO again, and restores the
 * original canvas scale.
 *
 * # Usage
 *
 * In the example below, we'll create a {@link Viewer}, add a {@link FastNavPlugin}, then use an {@link XKTLoaderPlugin} to load a model.
 *
 * Whenever we interact with the Viewer, our FastNavPlugin will:
 *
 * * hide edges,
 * * hide ambient shadows (SAO),
 * * hide physically-based materials (switching to non-PBR),
 * * hide transparent objects, and
 * * scale the canvas resolution by 0.5, causing the GPU to render 75% less pixels.
 * <br>
 *
 * We'll also configure a 0.5 second delay before we transition back to high-quality each time we stop ineracting, so that we're
 * not continually flipping between low and high quality as we interact. Since we're only rendering ambient shadows when not interacting, we'll also treat ourselves
 * to expensive, high-quality SAO settings, that we wouldn't normally configure for an interactive SAO effect.
 *
 * * [[Run this example](https://xeokit.github.io/xeokit-sdk/examples/index.html#performance_FastNavPlugin)]
 *
 * ````javascript
 * import {Viewer, XKTLoaderPlugin, FastNavPlugin} from "xeokit-sdk.es.js";
 *
 * // Create a Viewer with PBR and SAO enabled
 *
 * const viewer = new Viewer({
 *      canvasId: "myCanvas",
 *      transparent: true,
 *      pbr: true,                          // Enable physically-based rendering for Viewer
 *      sao: true                           // Enable ambient shadows for Viewer
 *  });
 *
 * viewer.scene.camera.eye = [-66.26, 105.84, -281.92];
 * viewer.scene.camera.look = [42.45, 49.62, -43.59];
 * viewer.scene.camera.up = [0.05, 0.95, 0.15];
 *
 * // Higher-quality SAO settings
 *
 * viewer.scene.sao.enabled = true;
 * viewer.scene.sao.numSamples = 60;
 * viewer.scene.sao.kernelRadius = 170;
 *
 * // Install a FastNavPlugin
 *
 * new FastNavPlugin(viewer, {
 *      hideEdges: true,                // Don't show edges while we interact (default is true)
 *      hideSAO: true,                  // Don't show ambient shadows while we interact (default is true)
 *      hideColorTexture: true,        // No color textures while we interact (default is true)
 *      hidePBR: true,                  // No physically-based rendering while we interact (default is true)
 *      hideTransparentObjects: true,   // Hide transparent objects while we interact (default is false)
 *      scaleCanvasResolution: true,    // Scale canvas resolution while we interact (default is false)
 *      defaultScaleCanvasResolutionFactor: 1.0, // Factor by which we scale canvas resolution when we stop interacting (default is 1.0)
 *      scaleCanvasResolutionFactor: 0.5,  // Factor by which we scale canvas resolution when we interact (default is 0.6)
 *      delayBeforeRestore: true,       // When we stop interacting, delay before restoring normal render (default is true)
 *      delayBeforeRestoreSeconds: 0.5  // The delay duration, in seconds (default is 0.5)
 * });
 *
 * // Load a BIM model from XKT
 *
 * const xktLoader = new XKTLoaderPlugin(viewer);
 *
 * const model = xktLoader.load({
 *      id: "myModel",
 *      src: "./models/xkt/HolterTower.xkt",
 *      sao: true,                          // Enable ambient shadows for this model
 *      pbr: true                           // Enable physically-based rendering for this model
 * });
 * ````
 *
 * @class FastNavPlugin
 */
class FastNavPlugin extends Plugin {

    /**
     * @constructor
     * @param {Viewer} viewer The Viewer.
     * @param {Object} cfg FastNavPlugin configuration.
     * @param {String} [cfg.id="FastNav"] Optional ID for this plugin, so that we can find it within {@link Viewer#plugins}.
     * @param {Boolean} [cfg.hideColorTexture=true] Whether to temporarily hide color textures whenever we interact with the Viewer.
     * @param {Boolean} [cfg.hidePBR=true] Whether to temporarily hide physically-based rendering (PBR) whenever we interact with the Viewer.
     * @param {Boolean} [cfg.hideSAO=true] Whether to temporarily hide scalable ambient occlusion (SAO) whenever we interact with the Viewer.
     * @param {Boolean} [cfg.hideEdges=true] Whether to temporarily hide edges whenever we interact with the Viewer.
     * @param {Boolean} [cfg.hideTransparentObjects=false] Whether to temporarily hide transparent objects whenever we interact with the Viewer.
     * @param {Number} [cfg.scaleCanvasResolution=false] Whether to temporarily down-scale the canvas resolution whenever we interact with the Viewer.
     * @param {Number} [cfg.defaultScaleCanvasResolutionFactor=0.6] The factor by which we downscale the canvas resolution whenever we stop interacting with the Viewer.
     * @param {Number} [cfg.scaleCanvasResolutionFactor=0.6] The factor by which we downscale the canvas resolution whenever we interact with the Viewer.
     * @param {Boolean} [cfg.delayBeforeRestore=true] Whether to temporarily have a delay before restoring normal rendering after we stop interacting with the Viewer.
     * @param {Number} [cfg.delayBeforeRestoreSeconds=0.5] Delay in seconds before restoring normal rendering after we stop interacting with the Viewer.
     */
    constructor(viewer, cfg = {}) {

        super("FastNav", viewer);

        this._hideColorTexture = cfg.hideColorTexture !== false;
        this._hidePBR = cfg.hidePBR !== false;
        this._hideSAO = cfg.hideSAO !== false;
        this._hideEdges = cfg.hideEdges !== false;
        this._hideTransparentObjects = !!cfg.hideTransparentObjects;
        this._scaleCanvasResolution = !!cfg.scaleCanvasResolution;
        this._defaultScaleCanvasResolutionFactor = cfg.defaultScaleCanvasResolutionFactor || 1.0;
        this._scaleCanvasResolutionFactor = cfg.scaleCanvasResolutionFactor || 0.6;
        this._delayBeforeRestore = (cfg.delayBeforeRestore !== false);
        this._delayBeforeRestoreSeconds = cfg.delayBeforeRestoreSeconds || 0.5;

        let timer = this._delayBeforeRestoreSeconds * 1000;
        let fastMode = false;

        const switchToLowQuality = () => {
            timer = (this._delayBeforeRestoreSeconds * 1000);
            if (!fastMode) {
                viewer.scene._renderer.setColorTextureEnabled(!this._hideColorTexture);
                viewer.scene._renderer.setPBREnabled(!this._hidePBR);
                viewer.scene._renderer.setSAOEnabled(!this._hideSAO);
                viewer.scene._renderer.setTransparentEnabled(!this._hideTransparentObjects);
                viewer.scene._renderer.setEdgesEnabled(!this._hideEdges);
                if (this._scaleCanvasResolution) {
                    viewer.scene.canvas.resolutionScale = this._scaleCanvasResolutionFactor;
                } else {
                    viewer.scene.canvas.resolutionScale = this._defaultScaleCanvasResolutionFactor;
                }
                fastMode = true;
            }
        };

        const switchToHighQuality = () => {
            viewer.scene.canvas.resolutionScale = this._defaultScaleCanvasResolutionFactor;
            viewer.scene._renderer.setEdgesEnabled(true);
            viewer.scene._renderer.setColorTextureEnabled(true);
            viewer.scene._renderer.setPBREnabled(true);
            viewer.scene._renderer.setSAOEnabled(true);
            viewer.scene._renderer.setTransparentEnabled(true);
            fastMode = false;
        };

        this._onCanvasBoundary = viewer.scene.canvas.on("boundary", switchToLowQuality);
        this._onCameraMatrix = viewer.scene.camera.on("matrix", switchToLowQuality);

        this._onSceneTick = viewer.scene.on("tick", (tickEvent) => {
            if (!fastMode) {
                return;
            }
            timer -= tickEvent.deltaTime;
            if ((!this._delayBeforeRestore) || timer <= 0) {
                switchToHighQuality();
            }
        });

        let down = false;

        this._onSceneMouseDown = viewer.scene.input.on("mousedown", () => {
            down = true;
        });

        this._onSceneMouseUp = viewer.scene.input.on("mouseup", () => {
            down = false;
        });

        this._onSceneMouseMove = viewer.scene.input.on("mousemove", () => {
            if (!down) {
                return;
            }
            switchToLowQuality();
        });
    }

    /**
     * Gets whether to temporarily hide color textures whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @return {Boolean} ````true```` if hiding color textures.
     */
    get hideColorTexture() {
        return this._hideColorTexture;
    }

    /**
     * Sets whether to temporarily hide color textures whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @param {Boolean} hideColorTexture ````true```` to hide color textures.
     */
    set hideColorTexture(hideColorTexture) {
        this._hideColorTexture = hideColorTexture;
    }
    
    /**
     * Gets whether to temporarily hide physically-based rendering (PBR) whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @return {Boolean} ````true```` if hiding PBR.
     */
    get hidePBR() {
        return this._hidePBR;
    }

    /**
     * Sets whether to temporarily hide physically-based rendering (PBR) whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @param {Boolean} hidePBR ````true```` to hide PBR.
     */
    set hidePBR(hidePBR) {
        this._hidePBR = hidePBR;
    }

    /**
     * Gets whether to temporarily hide scalable ambient shadows (SAO) whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @return {Boolean} ````true```` if hiding SAO.
     */
    get hideSAO() {
        return this._hideSAO;
    }

    /**
     * Sets whether to temporarily hide scalable ambient shadows (SAO) whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @param {Boolean} hideSAO ````true```` to hide SAO.
     */
    set hideSAO(hideSAO) {
        this._hideSAO = hideSAO;
    }

    /**
     * Gets whether to temporarily hide edges whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @return {Boolean}  ````true```` if hiding edges.
     */
    get hideEdges() {
        return this._hideEdges;
    }

    /**
     * Sets whether to temporarily hide edges whenever we interact with the Viewer.
     *
     * Default is ````true````.
     *
     * @param {Boolean} hideEdges ````true```` to hide edges.
     */
    set hideEdges(hideEdges) {
        this._hideEdges = hideEdges;
    }

    /**
     * Gets whether to temporarily hide transparent objects whenever we interact with the Viewer.
     *
     * Does not hide X-rayed, selected, highlighted objects.
     *
     * Default is ````false````.
     *
     * @return {Boolean} ````true```` if hiding transparent objects.
     */
    get hideTransparentObjects() {
        return this._hideTransparentObjects
    }

    /**
     * Sets whether to temporarily hide transparent objects whenever we interact with the Viewer.
     *
     * Does not hide X-rayed, selected, highlighted objects.
     *
     * Default is ````false````.
     *
     * @param {Boolean} hideTransparentObjects ````true```` to hide transparent objects.
     */
    set hideTransparentObjects(hideTransparentObjects) {
        this._hideTransparentObjects = (hideTransparentObjects !== false);
    }

    /**
     * Gets whether to temporarily scale the canvas resolution whenever we interact with the Viewer.
     *
     * Default is ````false````.
     *
     * The scaling factor is configured via {@link FastNavPlugin#scaleCanvasResolutionFactor}.
     *
     * @return {Boolean} ````true```` if scaling the canvas resolution.
     */
    get scaleCanvasResolution() {
        return this._scaleCanvasResolution;
    }

    /**
     * Sets the factor to which we restore the canvas resolution scale when we stop interacting with the viewer.
     *
     * Default is ````false````.
     *
     * The scaling factor is configured via {@link FastNavPlugin#scaleCanvasResolutionFactor}.
     *
     * @param {Boolean} scaleCanvasResolution ````true```` to scale the canvas resolution.
     */
    set scaleCanvasResolution(scaleCanvasResolution) {
        this._scaleCanvasResolution = scaleCanvasResolution;
    }

    /**
     * Gets the factor to which we restore the canvas resolution scale when we stop interacting with the viewer.
     *
     * Default is ````1.0````.
     *
     * Enable canvas resolution scaling by setting {@link FastNavPlugin#scaleCanvasResolution} ````true````.
     *
     * @return {Number} Factor by scale canvas resolution when we stop interacting with the viewer.
     */
    get defaultScaleCanvasResolutionFactor() {
        return this._defaultScaleCanvasResolutionFactor;
    }

    /**
     * Sets the factor to which we restore the canvas resolution scale when we stop interacting with the viewer.
     *
     * Accepted range is ````[0.0 .. 1.0]````.
     *
     * Default is ````1.0````.
     *
     * Enable canvas resolution scaling by setting {@link FastNavPlugin#scaleCanvasResolution} ````true````.
     *
     * @param {Number} defaultScaleCanvasResolutionFactor Factor by scale canvas resolution when we stop interacting with the viewer.
     */
    set defaultScaleCanvasResolutionFactor(defaultScaleCanvasResolutionFactor) {
        this._defaultScaleCanvasResolutionFactor = defaultScaleCanvasResolutionFactor || 1.0;
    }

    /**
     * Gets the factor by which we temporarily scale the canvas resolution when we interact with the viewer.
     *
     * Default is ````0.6````.
     *
     * Enable canvas resolution scaling by setting {@link FastNavPlugin#scaleCanvasResolution} ````true````.
     *
     * @return {Number} Factor by which we scale the canvas resolution.
     */
    get scaleCanvasResolutionFactor() {
        return this._scaleCanvasResolutionFactor;
    }

    /**
     * Sets the factor by which we temporarily scale the canvas resolution when we interact with the viewer.
     *
     * Accepted range is ````[0.0 .. 1.0]````.
     *
     * Default is ````0.6````.
     *
     * Enable canvas resolution scaling by setting {@link FastNavPlugin#scaleCanvasResolution} ````true````.
     *
     * @param {Number} scaleCanvasResolutionFactor Factor by which we scale the canvas resolution.
     */
    set scaleCanvasResolutionFactor(scaleCanvasResolutionFactor) {
        this._scaleCanvasResolutionFactor = scaleCanvasResolutionFactor || 0.6;
    }

    /**
     * Gets whether to have a delay before restoring normal rendering after we stop interacting with the Viewer.
     *
     * The delay duration is configured via {@link FastNavPlugin#delayBeforeRestoreSeconds}.
     *
     * Default is ````true````.
     *
     * @return {Boolean} Whether to have a delay.
     */
    get delayBeforeRestore() {
        return this._delayBeforeRestore;
    }

    /**
     * Sets whether to have a delay before restoring normal rendering after we stop interacting with the Viewer.
     *
     * The delay duration is configured via {@link FastNavPlugin#delayBeforeRestoreSeconds}.
     *
     * Default is ````true````.
     *
     * @param {Boolean} delayBeforeRestore Whether to have a delay.
     */
    set delayBeforeRestore(delayBeforeRestore) {
        this._delayBeforeRestore = delayBeforeRestore;
    }

    /**
     * Gets the delay before restoring normal rendering after we stop interacting with the Viewer.
     *
     * The delay is enabled when {@link FastNavPlugin#delayBeforeRestore} is ````true````.
     *
     * Default is ````0.5```` seconds.
     *
     * @return {Number} Delay in seconds.
     */
    get delayBeforeRestoreSeconds() {
        return this._delayBeforeRestoreSeconds;
    }

    /**
     * Sets the delay before restoring normal rendering after we stop interacting with the Viewer.
     *
     * The delay is enabled when {@link FastNavPlugin#delayBeforeRestore} is ````true````.
     *
     * Default is ````0.5```` seconds.
     *
     * @param {Number} delayBeforeRestoreSeconds Delay in seconds.
     */
    set delayBeforeRestoreSeconds(delayBeforeRestoreSeconds) {
        this._delayBeforeRestoreSeconds = delayBeforeRestoreSeconds !== null && delayBeforeRestoreSeconds !== undefined ? delayBeforeRestoreSeconds : 0.5;
    }

    /**
     * @private
     */
    send(name, value) {
        switch (name) {
            case "clear":
                break;
        }
    }

    /**
     * Destroys this plugin.
     */
    destroy() {
        this.viewer.scene.camera.off(this._onCameraMatrix);
        this.viewer.scene.canvas.off(this._onCanvasBoundary);
        this.viewer.scene.input.off(this._onSceneMouseDown);
        this.viewer.scene.input.off(this._onSceneMouseUp);
        this.viewer.scene.input.off(this._onSceneMouseMove);
        this.viewer.scene.off(this._onSceneTick);
        super.destroy();
    }
}

export {FastNavPlugin};