123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- 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) {
- this.exitXRAsync();
- }
- 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.");
- }
- }
- return Promise.resolve();
- }
- /**
- * 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
- */
- initializeAsync() {
- // Check if the browser supports webXR
- this._xrNavigator = navigator;
- if (!this._xrNavigator.xr) {
- return Promise.reject("WebXR not available");
- }
- return Promise.resolve();
- }
- /**
- * 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
- */
- initializeSessionAsync(xrSessionMode = "immersive-vr", xrSessionInit = {}) {
- return this._xrNavigator.xr.requestSession(xrSessionMode, xrSessionInit).then((session) => {
- 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
- */
- isSessionSupportedAsync(sessionMode) {
- return 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
- */
- setReferenceSpaceTypeAsync(referenceSpaceType = "local-floor") {
- return this.session
- .requestReferenceSpace(referenceSpaceType)
- .then((referenceSpace) => {
- return referenceSpace;
- }, (rejectionReason) => {
- Logger.Error("XR.requestReferenceSpace failed for the following reason: ");
- Logger.Error(rejectionReason);
- Logger.Log('Defaulting to universally-supported "viewer" reference space type.');
- return this.session.requestReferenceSpace("viewer").then((referenceSpace) => {
- const heightCompensation = new XRRigidTransform({ x: 0, y: -this.defaultHeightCompensation, z: 0 });
- return referenceSpace.getOffsetReferenceSpace(heightCompensation);
- }, (rejectionReason) => {
- Logger.Error(rejectionReason);
- // eslint-disable-next-line no-throw-literal
- throw 'XR initialization failed: required "viewer" reference space type not supported.';
- });
- })
- .then((referenceSpace) => {
- // create viewer reference space before setting the first reference space
- return this.session.requestReferenceSpace("viewer").then((viewerReferenceSpace) => {
- this.viewerReferenceSpace = viewerReferenceSpace;
- return referenceSpace;
- });
- })
- .then((referenceSpace) => {
- // 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.
- */
- updateRenderStateAsync(state) {
- return Promise.resolve(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));
- }
- 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 IsSessionSupportedAsync(sessionMode) {
- if (!navigator.xr) {
- return Promise.resolve(false);
- }
- // When the specs are final, remove supportsSession!
- const functionToUse = navigator.xr.isSessionSupported || navigator.xr.supportsSession;
- if (!functionToUse) {
- return Promise.resolve(false);
- }
- else {
- return functionToUse
- .call(navigator.xr, sessionMode)
- .then((result) => {
- const returnValue = typeof result === "undefined" ? true : result;
- return Promise.resolve(returnValue);
- })
- .catch((e) => {
- Logger.Warn(e);
- return Promise.resolve(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
- */
- updateTargetFrameRate(rate) {
- return 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
|