Jumping between scenes while in WebXR

You can say I did that. I just stole yours via sub-classing, so it is going to look a little similar. 3 classes needed changes (WebXRDefaultExperience, WebXRExperienceHelper, & WebXRSessionManager).

Default Experience Sub

/**
 * WebXRDefaultExperience Notes:
 *     - Made a seperate Create Call, so if integrated nothing need change,
 *       unless code reduction for working in functions from here.
 *     - probably should add an engine disposer observer for here
 */
export class DefExperience extends BABYLON.WebXRDefaultExperience {
    public options : BABYLON.WebXRDefaultExperienceOptions;
    public persistent = false; // only really needed, if changes end up directly in framework

    /**
     * Creates the default xr experience for multi scene
     * @param engine
     * @param options options for basic configuration
     * @returns resulting WebXRDefaultExperience
     */
    public static CreatePersistentAsync(engine : BABYLON.Engine, options: BABYLON.WebXRDefaultExperienceOptions = {}) {
        const result = new DefExperience();
        result.options = options;
        result.persistent = true;

        const throwAwayScene = new BABYLON.Scene(engine);

        // Create base experience.  Must pass a valid scene, since it does not check
        return ExpHelper.CreateAsync(throwAwayScene)
            .then((xrHelper) => {
                (<ExpHelper> xrHelper).persistent = true;
                result.baseExperience = xrHelper;

                // Clears observer of the important scene disposers (baseExperience & session manager).
                // They are littered everywhere, but in those classes they are just going to be
                // disposed themselves before the scene is, if using moveXRToScene() correctly.
                throwAwayScene.onDisposeObservable.clear();
                throwAwayScene.dispose();
                return result;
            })
            .catch((error) => {
                BABYLON.Logger.Error("Error initializing XR");
                BABYLON.Logger.Error(error);
                return result;
            });
        }

    /**
     * broken out from CreateAsync(); could possibly reduce replication by calling
     * this from there too
     */
    private _addUI(scene: BABYLON.Scene) : void {
        // init the UI right after construction
        if (!this.options.disableDefaultUI) {
            const uiOptions: BABYLON.WebXREnterExitUIOptions = {
                renderTarget: this.renderTarget,
                ...(this.options.uiOptions || {}),
            };
            if (this.options.optionalFeatures) {
                if (typeof this.options.optionalFeatures === "boolean") {
                    uiOptions.optionalFeatures = ["hit-test", "anchors", "plane-detection", "hand-tracking"];
                } else {
                    uiOptions.optionalFeatures = this.options.optionalFeatures;
                }
            }
            this.enterExitUI = new BABYLON.WebXREnterExitUI(scene, uiOptions);
        }
    }

    /**
     * broken out from CreateAsync(); could possibly reduce replication by calling
     * this from there too
     */
    private async _initialize() : Promise<void> {
        if (this.options.ignoreNativeCameraTransformation) {
            this.baseExperience.camera.compensateOnFirstFrame = false;
        }

        // Add controller support
        this.input = new BABYLON.WebXRInput(this.baseExperience.sessionManager, this.baseExperience.camera, {
            controllerOptions: {
                renderingGroupId: this.options.renderingGroupId,
            },
            ...(this.options.inputOptions || {}),
        });

        if (!this.options.disablePointerSelection) {
            // Add default pointer selection
            const pointerSelectionOptions = {
                ...this.options.pointerSelectionOptions,
                xrInput: this.input,
                renderingGroupId: this.options.renderingGroupId,
            };

            this.pointerSelection = <BABYLON.WebXRControllerPointerSelection>(
                this.baseExperience.featuresManager.enableFeature(
                    BABYLON.WebXRControllerPointerSelection.Name,
                    this.options.useStablePlugins ? "stable" : "latest",
                    <BABYLON.IWebXRControllerPointerSelectionOptions>pointerSelectionOptions
                )
            );

            if (!this.options.disableTeleportation) {
                // Add default teleportation, including rotation
                this.teleportation = <BABYLON.WebXRMotionControllerTeleportation>this.baseExperience.featuresManager.enableFeature(
                    BABYLON.WebXRMotionControllerTeleportation.Name,
                    this.options.useStablePlugins ? "stable" : "latest",
                    <BABYLON.IWebXRTeleportationOptions>{
                        floorMeshes: this.options.floorMeshes,
                        xrInput: this.input,
                        renderingGroupId: this.options.renderingGroupId,
                        ...this.options.teleportationOptions,
                    }
                );
                this.teleportation.setSelectionFeature(this.pointerSelection);
            }
        }

        if (!this.options.disableNearInteraction) {
            // Add default pointer selection
            this.nearInteraction = <BABYLON.WebXRNearInteraction>this.baseExperience.featuresManager.enableFeature(
                BABYLON.WebXRNearInteraction.Name,
                this.options.useStablePlugins ? "stable" : "latest",
                <BABYLON.IWebXRNearInteractionOptions>{
                    xrInput: this.input,
                    farInteractionFeature: this.pointerSelection,
                    renderingGroupId: this.options.renderingGroupId,
                    useUtilityLayer: true,
                    enableNearInteractionOnAllControllers: true,
                    ...this.options.nearInteractionOptions,
                }
            );
        }

        // Create the WebXR output target
        this.renderTarget = this.baseExperience.sessionManager.getWebXRRenderTarget(this.options.outputCanvasOptions);

        if (!this.options.disableDefaultUI) {
            // Create ui for entering/exiting xr
            // changed a little here to contain promise chaining if persistent
            const promise = this.enterExitUI.setHelperAsync(this.baseExperience, this.renderTarget);
            if (this.persistent) await promise;
            else return promise;
        } else {
            return Promise.resolve();
        }
    }

    /**
     * @overide
     * not calling super as purpose is to remove nuking of baseExperience.
     * Why are not these members part of dispose:
     *  - pointerSelection
     *  - teleportation
     *  - nearInteraction
     */
    public dispose() {
        if (this.baseExperience && !this.persistent) {
            this.baseExperience.dispose();
        }
        if (this.input) {
            this.input.dispose();
        }
        if (this.enterExitUI) {
            this.enterExitUI.dispose();
        }
        if (this.renderTarget) {
            this.renderTarget.dispose();
        }
    }

    public moveXRToScene(nextScene : BABYLON.Scene, hookUp : (defExperience : DefExperience) => void) : void {
        // sanity check
        if (!this.persistent) throw 'DefaultExperience must be instanced with CreatePersistentAsync() to move XR';

        // call a persistence aware dispose
        this.dispose();

        // re-add the enter
        this._addUI(nextScene);
        this._initialize();

        // cascade down
        (<ExpHelper> this.baseExperience)._moveXRToScene(nextScene);
        (<XRSM> this.baseExperience.sessionManager)._moveXRToScene(nextScene, this, hookUp);
    }
}

Experience Helper Sub

//==========================================================================
/**
 * WebXRExperienceHelper Notes:
 * `  - Constructor is passed a scene, which adds a scene.onDisposeObservable.
 *      As this is only instanced once, disposer is premanently removed in
 *      DefExperience.CreatePersistentAsync()
 *
 *    - no dispose of feature manager; bug?
 *
 *    - The constructor & a # of privates need to be changed to protected to 
 *      avoid using an altered babylon.d.ts to transpile.
 */
export class ExpHelper extends BABYLON.WebXRExperienceHelper {
    public persistent = false; // only really needed, if changes end up directly in framework

     // super constructor's scene.onDisposeObservable() nuked in DefExperience
    constructor(scene: BABYLON.Scene) {
        super(scene);

        // replace some parts with sub classes
        this.sessionManager.dispose();
        this.sessionManager = new XRSM(scene);
    }

    /**
     * broken out from enterXRAsync(), could possibly reduce replication by calling
     * this from there too
     */
    private _adjustScene() {
        // Cache pre xr scene settings
        this._originalSceneAutoClear = this._scene.autoClear;
        this._nonVRCamera = this._scene.activeCamera;
        this._attachedToElement = !!this._nonVRCamera?.inputs?.attachedToElement;
        this._nonVRCamera?.detachControl();

        this._scene.activeCamera = this.camera;
        // do not compensate when AR session is used
        if (this.sessionManager.sessionMode !== "immersive-ar") {
            this._nonXRToXRCamera();
        } else {
            // Kept here, TODO - check if needed
            this._scene.autoClear = false;
            this.camera.compensateOnFirstFrame = false;
            // reset the camera's position to the origin
            this.camera.position.set(0, 0, 0);
            this.camera.rotationQuaternion.set(0, 0, 0, 1);
        }
    }

    /**
     * broken out from enterXRAsync(), could possibly reduce replication by calling
     * this from there too
     */
    public _onSessionEnded() : void {
        // when using the back button and not the exit button (default on mobile), the session is ending but the EXITING state was not set
        if (this.state !== BABYLON.WebXRState.EXITING_XR) {
            this._setState(BABYLON.WebXRState.EXITING_XR);
        }
        // Reset camera rigs output render target to ensure sessions render target is not drawn after it ends
        this.camera.rigCameras.forEach((c) => {
            c.outputRenderTarget = null;
        });

        // Restore scene settings
        this._scene.autoClear = this._originalSceneAutoClear;
        this._scene.activeCamera = this._nonVRCamera;
        if (this._attachedToElement && this._nonVRCamera) {
            this._nonVRCamera.attachControl(!!this._nonVRCamera.inputs.noPreventDefault);
        }
        if (this.sessionManager.sessionMode !== "immersive-ar" && this.camera.compensateOnFirstFrame) {
            if ((<any>this._nonVRCamera).setPosition) {
                (<any>this._nonVRCamera).setPosition(this.camera.position);
            } else {
                this._nonVRCamera!.position.copyFrom(this.camera.position);
            }
        }

        this._setState(BABYLON.WebXRState.NOT_IN_XR);
    }

    /**
     * Intended to be called by DefExperience.moveXRToScene()
     */
    public _moveXRToScene(nextScene : BABYLON.Scene) : void {
        this.dispose();

        // the 3 things assigned / instanced by the constructor
        this._scene = nextScene;
        this.camera = new BABYLON.WebXRCamera("webxr", this._scene, this.sessionManager);
        this.featuresManager = new BABYLON.WebXRFeaturesManager(this.sessionManager);

        this._adjustScene();

        // will possibly clear too much, but no choice
        nextScene.onDisposeObservable.clear();
    }

    /**
     * @overide
     * Not calling super as purpose is to stay inside an XR session.
     * Also, added disposal of featureManager.  Seems like a bug not to have.
     */
    public dispose() {
        if (!this.persistent) {
            this.exitXRAsync();
            this.sessionManager.dispose();
        }
        this.featuresManager.dispose(); // added from stock class
        this.camera.dispose();
        this.onStateChangedObservable.clear();
        this.onInitialXRPoseSetObservable.clear();
        this._spectatorCamera?.dispose();
        if (this._nonVRCamera) {
            this._scene.activeCamera = this._nonVRCamera;
        }
    }
}

Session Manager Sub

//==========================================================================
/**
 * WebXRSessionManager Notes:
 * `  - constructor is passed a scene, but all it wants is an Engine, except for a disposer.
 *      As this is only instanced once, disposer is premanently fixed in
 *      DefExperience.CreatePersistentAsync()
 *
 *   - _moveXRToScene() is passed a scene, which is not used; toss, maybe
 */
export class XRSM extends BABYLON.WebXRSessionManager {

    /**
     * Intended to be called by DefExperience.moveXRToScene()
     */
    public _moveXRToScene(nextScene : BABYLON.Scene, defExperience : DefExperience, hookUp : (defExperience : DefExperience) => void) : void {
        //very similar to dispose, but not calling it
        this.onXRFrameObservable.clear();
        this.onXRSessionEnded.clear();
        this.onXRReferenceSpaceChanged.clear();
        this.onXRSessionInit.clear();

        // all the old sessionInit observers are now gone; time to make some new ones
        hookUp(defExperience);

        // add back a cleared observer, if current session is ever exited
        const eHelper = <ExpHelper>defExperience.baseExperience;
        this.onXRSessionEnded.addOnce(eHelper._onSessionEnded);

        // now simulate that event for the new stuff
        this.onXRSessionInit.notifyObservers(this.session);
    }
}

I have transpiled successfully, & will start next week to try to get it to run.

4 Likes