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.