@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
452 lines • 18.5 kB
JavaScript
import { Logger } from "../Misc/logger.js";
import { Observable } from "../Misc/observable.js";
import { WebXRManagedOutputCanvas, WebXRManagedOutputCanvasOptions } from "./webXRManagedOutputCanvas.js";
import { NativeXRLayerWrapper, NativeXRRenderTarget } from "./native/nativeXRRenderTarget.js";
import { WebXRWebGLLayerWrapper } from "./webXRWebGLLayer.js";
/**
* Manages an XRSession to work with Babylon's engine
* @see https://doc.babylonjs.com/features/featuresDeepDive/webXR/webXRSessionManagers
*/
export class WebXRSessionManager {
/**
* Scale factor to apply to all XR-related elements (camera, controllers)
*/
get worldScalingFactor() {
return this._worldScalingFactor;
}
set worldScalingFactor(value) {
const oldValue = this._worldScalingFactor;
this._worldScalingFactor = value;
this.onWorldScaleFactorChangedObservable.notifyObservers({
previousScaleFactor: oldValue,
newScaleFactor: value,
});
}
/**
* Constructs a WebXRSessionManager, this must be initialized within a user action before usage
* @param scene The scene which the session should be created for
*/
constructor(
/** The scene which the session should be created for */
scene) {
this.scene = scene;
/** WebXR timestamp updated every frame */
this.currentTimestamp = -1;
/**
* Used just in case of a failure to initialize an immersive session.
* The viewer reference space is compensated using this height, creating a kind of "viewer-floor" reference space
*/
this.defaultHeightCompensation = 1.7;
/**
* Fires every time a new xrFrame arrives which can be used to update the camera
*/
this.onXRFrameObservable = new Observable();
/**
* Fires when the reference space changed
*/
this.onXRReferenceSpaceChanged = new Observable();
/**
* Fires when the xr session is ended either by the device or manually done
*/
this.onXRSessionEnded = new Observable();
/**
* Fires when the xr session is initialized: right after requestSession was called and returned with a successful result
*/
this.onXRSessionInit = new Observable();
/**
* Fires when the xr reference space has been initialized
*/
this.onXRReferenceSpaceInitialized = new Observable();
/**
* Fires when the session manager is rendering the first frame
*/
this.onXRReady = new Observable();
/**
* Are we currently in the XR loop?
*/
this.inXRFrameLoop = false;
/**
* Are we in an XR session?
*/
this.inXRSession = false;
this._worldScalingFactor = 1;
/**
* Observable raised when the world scale has changed
*/
this.onWorldScaleFactorChangedObservable = new Observable(undefined, true);
this._engine = scene.getEngine();
this._onEngineDisposedObserver = this._engine.onDisposeObservable.addOnce(() => {
this._engine = null;
});
scene.onDisposeObservable.addOnce(() => {
this.dispose();
});
}
/**
* The current reference space used in this session. This reference space can constantly change!
* It is mainly used to offset the camera's position.
*/
get referenceSpace() {
return this._referenceSpace;
}
/**
* Set a new reference space and triggers the observable
*/
set referenceSpace(newReferenceSpace) {
this._referenceSpace = newReferenceSpace;
this.onXRReferenceSpaceChanged.notifyObservers(this._referenceSpace);
}
/**
* The mode for the managed XR session
*/
get sessionMode() {
return this._sessionMode;
}
/**
* Disposes of the session manager
* This should be called explicitly by the dev, if required.
*/
dispose() {
// disposing without leaving XR? Exit XR first
if (this.inXRSession) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.exitXRAsync();
}
this.onXRReady.clear();
this.onXRFrameObservable.clear();
this.onXRSessionEnded.clear();
this.onXRReferenceSpaceChanged.clear();
this.onXRSessionInit.clear();
this.onWorldScaleFactorChangedObservable.clear();
this._engine?.onDisposeObservable.remove(this._onEngineDisposedObserver);
this._engine = null;
}
/**
* Stops the xrSession and restores the render loop
* @returns Promise which resolves after it exits XR
*/
async exitXRAsync() {
if (this.session && this.inXRSession) {
this.inXRSession = false;
try {
return await this.session.end();
}
catch {
Logger.Warn("Could not end XR session.");
}
}
}
/**
* Attempts to set the framebuffer-size-normalized viewport to be rendered this frame for this view.
* In the event of a failure, the supplied viewport is not updated.
* @param viewport the viewport to which the view will be rendered
* @param view the view for which to set the viewport
* @returns whether the operation was successful
*/
trySetViewportForView(viewport, view) {
return this._baseLayerRTTProvider?.trySetViewportForView(viewport, view) || false;
}
/**
* Gets the correct render target texture to be rendered this frame for this eye
* @param eye the eye for which to get the render target
* @returns the render target for the specified eye or null if not available
*/
getRenderTargetTextureForEye(eye) {
return this._baseLayerRTTProvider?.getRenderTargetTextureForEye(eye) || null;
}
/**
* Gets the correct render target texture to be rendered this frame for this view
* @param view the view for which to get the render target
* @returns the render target for the specified view or null if not available
*/
getRenderTargetTextureForView(view) {
return this._baseLayerRTTProvider?.getRenderTargetTextureForView(view) || null;
}
/**
* Creates a WebXRRenderTarget object for the XR session
* @param options optional options to provide when creating a new render target
* @returns a WebXR render target to which the session can render
*/
getWebXRRenderTarget(options) {
const engine = this.scene.getEngine();
if (this._xrNavigator.xr.native) {
return new NativeXRRenderTarget(this);
}
else {
options = options || WebXRManagedOutputCanvasOptions.GetDefaults(engine);
options.canvasElement = options.canvasElement || engine.getRenderingCanvas() || undefined;
return new WebXRManagedOutputCanvas(this, options);
}
}
/**
* Initializes the manager
* After initialization enterXR can be called to start an XR session
* @returns Promise which resolves after it is initialized
*/
async initializeAsync() {
// Check if the browser supports webXR
this._xrNavigator = navigator;
if (!this._xrNavigator.xr) {
throw new Error("WebXR not supported on this browser.");
}
}
/**
* Initializes an xr session
* @param xrSessionMode mode to initialize
* @param xrSessionInit defines optional and required values to pass to the session builder
* @returns a promise which will resolve once the session has been initialized
*/
async initializeSessionAsync(xrSessionMode = "immersive-vr", xrSessionInit = {}) {
const session = await this._xrNavigator.xr.requestSession(xrSessionMode, xrSessionInit);
this.session = session;
this._sessionMode = xrSessionMode;
this.inXRSession = true;
this.onXRSessionInit.notifyObservers(session);
// handle when the session is ended (By calling session.end or device ends its own session eg. pressing home button on phone)
this.session.addEventListener("end", () => {
this.inXRSession = false;
// Notify frame observers
this.onXRSessionEnded.notifyObservers(null);
if (this._engine) {
// make sure dimensions object is restored
this._engine.framebufferDimensionsObject = null;
// Restore frame buffer to avoid clear on xr framebuffer after session end
this._engine.restoreDefaultFramebuffer();
// Need to restart render loop as after the session is ended the last request for new frame will never call callback
this._engine.customAnimationFrameRequester = null;
this._engine._renderLoop();
}
// Dispose render target textures.
// Only dispose on native because we can't destroy opaque textures on browser.
if (this.isNative) {
this._baseLayerRTTProvider?.dispose();
}
this._baseLayerRTTProvider = null;
this._baseLayerWrapper = null;
}, { once: true });
return this.session;
}
/**
* Checks if a session would be supported for the creation options specified
* @param sessionMode session mode to check if supported eg. immersive-vr
* @returns A Promise that resolves to true if supported and false if not
*/
async isSessionSupportedAsync(sessionMode) {
return await WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
}
/**
* Resets the reference space to the one started the session
*/
resetReferenceSpace() {
this.referenceSpace = this.baseReferenceSpace;
}
/**
* Starts rendering to the xr layer
*/
runXRRenderLoop() {
if (!this.inXRSession || !this._engine) {
return;
}
// Tell the engine's render loop to be driven by the xr session's refresh rate and provide xr pose information
this._engine.customAnimationFrameRequester = {
requestAnimationFrame: (callback) => this.session.requestAnimationFrame(callback),
renderFunction: (timestamp, xrFrame) => {
if (!this.inXRSession || !this._engine) {
return;
}
// Store the XR frame and timestamp in the session manager
this.currentFrame = xrFrame;
this.currentTimestamp = timestamp;
if (xrFrame) {
this.inXRFrameLoop = true;
const framebufferDimensionsObject = this._baseLayerRTTProvider?.getFramebufferDimensions() || null;
// equality can be tested as it should be the same object
if (this._engine.framebufferDimensionsObject !== framebufferDimensionsObject) {
this._engine.framebufferDimensionsObject = framebufferDimensionsObject;
}
this.onXRFrameObservable.notifyObservers(xrFrame);
this._engine._renderLoop();
this._engine.framebufferDimensionsObject = null;
this.inXRFrameLoop = false;
}
},
};
this._engine.framebufferDimensionsObject = this._baseLayerRTTProvider?.getFramebufferDimensions() || null;
this.onXRFrameObservable.addOnce(() => {
this.onXRReady.notifyObservers(this);
});
// Stop window's animation frame and trigger sessions animation frame
if (typeof window !== "undefined" && window.cancelAnimationFrame) {
window.cancelAnimationFrame(this._engine._frameHandler);
}
this._engine._renderLoop();
}
/**
* Sets the reference space on the xr session
* @param referenceSpaceType space to set
* @returns a promise that will resolve once the reference space has been set
*/
async setReferenceSpaceTypeAsync(referenceSpaceType = "local-floor") {
let referenceSpace;
try {
referenceSpace = await this.session.requestReferenceSpace(referenceSpaceType);
}
catch (rejectionReason) {
Logger.Error("XR.requestReferenceSpace failed for the following reason: ");
Logger.Error(rejectionReason);
Logger.Log('Defaulting to universally-supported "viewer" reference space type.');
try {
const referenceSpace = await this.session.requestReferenceSpace("viewer");
const heightCompensation = new XRRigidTransform({ x: 0, y: -this.defaultHeightCompensation, z: 0 });
return referenceSpace.getOffsetReferenceSpace(heightCompensation);
}
catch (rejectionReason) {
Logger.Error(rejectionReason);
// eslint-disable-next-line no-throw-literal
throw 'XR initialization failed: required "viewer" reference space type not supported.';
}
}
// create viewer reference space before setting the first reference space
const viewerReferenceSpace = await this.session.requestReferenceSpace("viewer");
this.viewerReferenceSpace = viewerReferenceSpace;
// initialize the base and offset (currently the same)
this.referenceSpace = this.baseReferenceSpace = referenceSpace;
this.onXRReferenceSpaceInitialized.notifyObservers(referenceSpace);
return this.referenceSpace;
}
/**
* Updates the render state of the session.
* Note that this is deprecated in favor of WebXRSessionManager.updateRenderState().
* @param state state to set
* @returns a promise that resolves once the render state has been updated
* @deprecated Use updateRenderState() instead.
*/
async updateRenderStateAsync(state) {
return await this.session.updateRenderState(state);
}
/**
* @internal
*/
_setBaseLayerWrapper(baseLayerWrapper) {
if (this.isNative) {
this._baseLayerRTTProvider?.dispose();
}
this._baseLayerWrapper = baseLayerWrapper;
this._baseLayerRTTProvider = this._baseLayerWrapper?.createRenderTargetTextureProvider(this) || null;
}
/**
* @internal
*/
_getBaseLayerWrapper() {
return this._baseLayerWrapper;
}
/**
* Updates the render state of the session
* @param state state to set
*/
updateRenderState(state) {
if (state.baseLayer) {
this._setBaseLayerWrapper(this.isNative ? new NativeXRLayerWrapper(state.baseLayer) : new WebXRWebGLLayerWrapper(state.baseLayer));
}
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.session.updateRenderState(state);
}
/**
* Returns a promise that resolves with a boolean indicating if the provided session mode is supported by this browser
* @param sessionMode defines the session to test
* @returns a promise with boolean as final value
*/
static async IsSessionSupportedAsync(sessionMode) {
if (!navigator.xr) {
return false;
}
// When the specs are final, remove supportsSession!
const functionToUse = navigator.xr.isSessionSupported || navigator.xr.supportsSession;
if (!functionToUse) {
return false;
}
else {
try {
const result = functionToUse.call(navigator.xr, sessionMode);
const returnValue = typeof result === "undefined" ? true : result;
return returnValue;
}
catch (e) {
Logger.Warn(e);
return false;
}
}
}
/**
* Returns true if Babylon.js is using the BabylonNative backend, otherwise false
*/
get isNative() {
return this._xrNavigator.xr.native ?? false;
}
/**
* The current frame rate as reported by the device
*/
get currentFrameRate() {
return this.session?.frameRate;
}
/**
* A list of supported frame rates (only available in-session!
*/
get supportedFrameRates() {
return this.session?.supportedFrameRates;
}
/**
* Set the framerate of the session.
* @param rate the new framerate. This value needs to be in the supportedFrameRates array
* @returns a promise that resolves once the framerate has been set
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
async updateTargetFrameRate(rate) {
return await this.session.updateTargetFrameRate(rate);
}
/**
* Run a callback in the xr render loop
* @param callback the callback to call when in XR Frame
* @param ignoreIfNotInSession if no session is currently running, run it first thing on the next session
*/
runInXRFrame(callback, ignoreIfNotInSession = true) {
if (this.inXRFrameLoop) {
callback();
}
else if (this.inXRSession || !ignoreIfNotInSession) {
this.onXRFrameObservable.addOnce(callback);
}
}
/**
* Check if fixed foveation is supported on this device
*/
get isFixedFoveationSupported() {
return this._baseLayerWrapper?.isFixedFoveationSupported || false;
}
/**
* Get the fixed foveation currently set, as specified by the webxr specs
* If this returns null, then fixed foveation is not supported
*/
get fixedFoveation() {
return this._baseLayerWrapper?.fixedFoveation || null;
}
/**
* Set the fixed foveation to the specified value, as specified by the webxr specs
* This value will be normalized to be between 0 and 1, 1 being max foveation, 0 being no foveation
*/
set fixedFoveation(value) {
const val = Math.max(0, Math.min(1, value || 0));
if (this._baseLayerWrapper) {
this._baseLayerWrapper.fixedFoveation = val;
}
}
/**
* Get the features enabled on the current session
* This is only available in-session!
* @see https://www.w3.org/TR/webxr/#dom-xrsession-enabledfeatures
*/
get enabledFeatures() {
return this.session?.enabledFeatures ?? null;
}
}
//# sourceMappingURL=webXRSessionManager.js.map