webXRSessionManager.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. import { Logger } from "../Misc/logger.js";
  2. import { Observable } from "../Misc/observable.js";
  3. import { WebXRManagedOutputCanvas, WebXRManagedOutputCanvasOptions } from "./webXRManagedOutputCanvas.js";
  4. import { NativeXRLayerWrapper, NativeXRRenderTarget } from "./native/nativeXRRenderTarget.js";
  5. import { WebXRWebGLLayerWrapper } from "./webXRWebGLLayer.js";
  6. /**
  7. * Manages an XRSession to work with Babylon's engine
  8. * @see https://doc.babylonjs.com/features/featuresDeepDive/webXR/webXRSessionManagers
  9. */
  10. export class WebXRSessionManager {
  11. /**
  12. * Scale factor to apply to all XR-related elements (camera, controllers)
  13. */
  14. get worldScalingFactor() {
  15. return this._worldScalingFactor;
  16. }
  17. set worldScalingFactor(value) {
  18. const oldValue = this._worldScalingFactor;
  19. this._worldScalingFactor = value;
  20. this.onWorldScaleFactorChangedObservable.notifyObservers({
  21. previousScaleFactor: oldValue,
  22. newScaleFactor: value,
  23. });
  24. }
  25. /**
  26. * Constructs a WebXRSessionManager, this must be initialized within a user action before usage
  27. * @param scene The scene which the session should be created for
  28. */
  29. constructor(
  30. /** The scene which the session should be created for */
  31. scene) {
  32. this.scene = scene;
  33. /** WebXR timestamp updated every frame */
  34. this.currentTimestamp = -1;
  35. /**
  36. * Used just in case of a failure to initialize an immersive session.
  37. * The viewer reference space is compensated using this height, creating a kind of "viewer-floor" reference space
  38. */
  39. this.defaultHeightCompensation = 1.7;
  40. /**
  41. * Fires every time a new xrFrame arrives which can be used to update the camera
  42. */
  43. this.onXRFrameObservable = new Observable();
  44. /**
  45. * Fires when the reference space changed
  46. */
  47. this.onXRReferenceSpaceChanged = new Observable();
  48. /**
  49. * Fires when the xr session is ended either by the device or manually done
  50. */
  51. this.onXRSessionEnded = new Observable();
  52. /**
  53. * Fires when the xr session is initialized: right after requestSession was called and returned with a successful result
  54. */
  55. this.onXRSessionInit = new Observable();
  56. /**
  57. * Fires when the xr reference space has been initialized
  58. */
  59. this.onXRReferenceSpaceInitialized = new Observable();
  60. /**
  61. * Fires when the session manager is rendering the first frame
  62. */
  63. this.onXRReady = new Observable();
  64. /**
  65. * Are we currently in the XR loop?
  66. */
  67. this.inXRFrameLoop = false;
  68. /**
  69. * Are we in an XR session?
  70. */
  71. this.inXRSession = false;
  72. this._worldScalingFactor = 1;
  73. /**
  74. * Observable raised when the world scale has changed
  75. */
  76. this.onWorldScaleFactorChangedObservable = new Observable(undefined, true);
  77. this._engine = scene.getEngine();
  78. this._onEngineDisposedObserver = this._engine.onDisposeObservable.addOnce(() => {
  79. this._engine = null;
  80. });
  81. scene.onDisposeObservable.addOnce(() => {
  82. this.dispose();
  83. });
  84. }
  85. /**
  86. * The current reference space used in this session. This reference space can constantly change!
  87. * It is mainly used to offset the camera's position.
  88. */
  89. get referenceSpace() {
  90. return this._referenceSpace;
  91. }
  92. /**
  93. * Set a new reference space and triggers the observable
  94. */
  95. set referenceSpace(newReferenceSpace) {
  96. this._referenceSpace = newReferenceSpace;
  97. this.onXRReferenceSpaceChanged.notifyObservers(this._referenceSpace);
  98. }
  99. /**
  100. * The mode for the managed XR session
  101. */
  102. get sessionMode() {
  103. return this._sessionMode;
  104. }
  105. /**
  106. * Disposes of the session manager
  107. * This should be called explicitly by the dev, if required.
  108. */
  109. dispose() {
  110. // disposing without leaving XR? Exit XR first
  111. if (this.inXRSession) {
  112. this.exitXRAsync();
  113. }
  114. this.onXRFrameObservable.clear();
  115. this.onXRSessionEnded.clear();
  116. this.onXRReferenceSpaceChanged.clear();
  117. this.onXRSessionInit.clear();
  118. this.onWorldScaleFactorChangedObservable.clear();
  119. this._engine?.onDisposeObservable.remove(this._onEngineDisposedObserver);
  120. this._engine = null;
  121. }
  122. /**
  123. * Stops the xrSession and restores the render loop
  124. * @returns Promise which resolves after it exits XR
  125. */
  126. async exitXRAsync() {
  127. if (this.session && this.inXRSession) {
  128. this.inXRSession = false;
  129. try {
  130. return await this.session.end();
  131. }
  132. catch {
  133. Logger.Warn("Could not end XR session.");
  134. }
  135. }
  136. return Promise.resolve();
  137. }
  138. /**
  139. * Attempts to set the framebuffer-size-normalized viewport to be rendered this frame for this view.
  140. * In the event of a failure, the supplied viewport is not updated.
  141. * @param viewport the viewport to which the view will be rendered
  142. * @param view the view for which to set the viewport
  143. * @returns whether the operation was successful
  144. */
  145. trySetViewportForView(viewport, view) {
  146. return this._baseLayerRTTProvider?.trySetViewportForView(viewport, view) || false;
  147. }
  148. /**
  149. * Gets the correct render target texture to be rendered this frame for this eye
  150. * @param eye the eye for which to get the render target
  151. * @returns the render target for the specified eye or null if not available
  152. */
  153. getRenderTargetTextureForEye(eye) {
  154. return this._baseLayerRTTProvider?.getRenderTargetTextureForEye(eye) || null;
  155. }
  156. /**
  157. * Gets the correct render target texture to be rendered this frame for this view
  158. * @param view the view for which to get the render target
  159. * @returns the render target for the specified view or null if not available
  160. */
  161. getRenderTargetTextureForView(view) {
  162. return this._baseLayerRTTProvider?.getRenderTargetTextureForView(view) || null;
  163. }
  164. /**
  165. * Creates a WebXRRenderTarget object for the XR session
  166. * @param options optional options to provide when creating a new render target
  167. * @returns a WebXR render target to which the session can render
  168. */
  169. getWebXRRenderTarget(options) {
  170. const engine = this.scene.getEngine();
  171. if (this._xrNavigator.xr.native) {
  172. return new NativeXRRenderTarget(this);
  173. }
  174. else {
  175. options = options || WebXRManagedOutputCanvasOptions.GetDefaults(engine);
  176. options.canvasElement = options.canvasElement || engine.getRenderingCanvas() || undefined;
  177. return new WebXRManagedOutputCanvas(this, options);
  178. }
  179. }
  180. /**
  181. * Initializes the manager
  182. * After initialization enterXR can be called to start an XR session
  183. * @returns Promise which resolves after it is initialized
  184. */
  185. initializeAsync() {
  186. // Check if the browser supports webXR
  187. this._xrNavigator = navigator;
  188. if (!this._xrNavigator.xr) {
  189. return Promise.reject("WebXR not available");
  190. }
  191. return Promise.resolve();
  192. }
  193. /**
  194. * Initializes an xr session
  195. * @param xrSessionMode mode to initialize
  196. * @param xrSessionInit defines optional and required values to pass to the session builder
  197. * @returns a promise which will resolve once the session has been initialized
  198. */
  199. initializeSessionAsync(xrSessionMode = "immersive-vr", xrSessionInit = {}) {
  200. return this._xrNavigator.xr.requestSession(xrSessionMode, xrSessionInit).then((session) => {
  201. this.session = session;
  202. this._sessionMode = xrSessionMode;
  203. this.inXRSession = true;
  204. this.onXRSessionInit.notifyObservers(session);
  205. // handle when the session is ended (By calling session.end or device ends its own session eg. pressing home button on phone)
  206. this.session.addEventListener("end", () => {
  207. this.inXRSession = false;
  208. // Notify frame observers
  209. this.onXRSessionEnded.notifyObservers(null);
  210. if (this._engine) {
  211. // make sure dimensions object is restored
  212. this._engine.framebufferDimensionsObject = null;
  213. // Restore frame buffer to avoid clear on xr framebuffer after session end
  214. this._engine.restoreDefaultFramebuffer();
  215. // Need to restart render loop as after the session is ended the last request for new frame will never call callback
  216. this._engine.customAnimationFrameRequester = null;
  217. this._engine._renderLoop();
  218. }
  219. // Dispose render target textures.
  220. // Only dispose on native because we can't destroy opaque textures on browser.
  221. if (this.isNative) {
  222. this._baseLayerRTTProvider?.dispose();
  223. }
  224. this._baseLayerRTTProvider = null;
  225. this._baseLayerWrapper = null;
  226. }, { once: true });
  227. return this.session;
  228. });
  229. }
  230. /**
  231. * Checks if a session would be supported for the creation options specified
  232. * @param sessionMode session mode to check if supported eg. immersive-vr
  233. * @returns A Promise that resolves to true if supported and false if not
  234. */
  235. isSessionSupportedAsync(sessionMode) {
  236. return WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
  237. }
  238. /**
  239. * Resets the reference space to the one started the session
  240. */
  241. resetReferenceSpace() {
  242. this.referenceSpace = this.baseReferenceSpace;
  243. }
  244. /**
  245. * Starts rendering to the xr layer
  246. */
  247. runXRRenderLoop() {
  248. if (!this.inXRSession || !this._engine) {
  249. return;
  250. }
  251. // Tell the engine's render loop to be driven by the xr session's refresh rate and provide xr pose information
  252. this._engine.customAnimationFrameRequester = {
  253. requestAnimationFrame: (callback) => this.session.requestAnimationFrame(callback),
  254. renderFunction: (timestamp, xrFrame) => {
  255. if (!this.inXRSession || !this._engine) {
  256. return;
  257. }
  258. // Store the XR frame and timestamp in the session manager
  259. this.currentFrame = xrFrame;
  260. this.currentTimestamp = timestamp;
  261. if (xrFrame) {
  262. this.inXRFrameLoop = true;
  263. const framebufferDimensionsObject = this._baseLayerRTTProvider?.getFramebufferDimensions() || null;
  264. // equality can be tested as it should be the same object
  265. if (this._engine.framebufferDimensionsObject !== framebufferDimensionsObject) {
  266. this._engine.framebufferDimensionsObject = framebufferDimensionsObject;
  267. }
  268. this.onXRFrameObservable.notifyObservers(xrFrame);
  269. this._engine._renderLoop();
  270. this._engine.framebufferDimensionsObject = null;
  271. this.inXRFrameLoop = false;
  272. }
  273. },
  274. };
  275. this._engine.framebufferDimensionsObject = this._baseLayerRTTProvider?.getFramebufferDimensions() || null;
  276. this.onXRFrameObservable.addOnce(() => {
  277. this.onXRReady.notifyObservers(this);
  278. });
  279. // Stop window's animation frame and trigger sessions animation frame
  280. if (typeof window !== "undefined" && window.cancelAnimationFrame) {
  281. window.cancelAnimationFrame(this._engine._frameHandler);
  282. }
  283. this._engine._renderLoop();
  284. }
  285. /**
  286. * Sets the reference space on the xr session
  287. * @param referenceSpaceType space to set
  288. * @returns a promise that will resolve once the reference space has been set
  289. */
  290. setReferenceSpaceTypeAsync(referenceSpaceType = "local-floor") {
  291. return this.session
  292. .requestReferenceSpace(referenceSpaceType)
  293. .then((referenceSpace) => {
  294. return referenceSpace;
  295. }, (rejectionReason) => {
  296. Logger.Error("XR.requestReferenceSpace failed for the following reason: ");
  297. Logger.Error(rejectionReason);
  298. Logger.Log('Defaulting to universally-supported "viewer" reference space type.');
  299. return this.session.requestReferenceSpace("viewer").then((referenceSpace) => {
  300. const heightCompensation = new XRRigidTransform({ x: 0, y: -this.defaultHeightCompensation, z: 0 });
  301. return referenceSpace.getOffsetReferenceSpace(heightCompensation);
  302. }, (rejectionReason) => {
  303. Logger.Error(rejectionReason);
  304. // eslint-disable-next-line no-throw-literal
  305. throw 'XR initialization failed: required "viewer" reference space type not supported.';
  306. });
  307. })
  308. .then((referenceSpace) => {
  309. // create viewer reference space before setting the first reference space
  310. return this.session.requestReferenceSpace("viewer").then((viewerReferenceSpace) => {
  311. this.viewerReferenceSpace = viewerReferenceSpace;
  312. return referenceSpace;
  313. });
  314. })
  315. .then((referenceSpace) => {
  316. // initialize the base and offset (currently the same)
  317. this.referenceSpace = this.baseReferenceSpace = referenceSpace;
  318. this.onXRReferenceSpaceInitialized.notifyObservers(referenceSpace);
  319. return this.referenceSpace;
  320. });
  321. }
  322. /**
  323. * Updates the render state of the session.
  324. * Note that this is deprecated in favor of WebXRSessionManager.updateRenderState().
  325. * @param state state to set
  326. * @returns a promise that resolves once the render state has been updated
  327. * @deprecated Use updateRenderState() instead.
  328. */
  329. updateRenderStateAsync(state) {
  330. return Promise.resolve(this.session.updateRenderState(state));
  331. }
  332. /**
  333. * @internal
  334. */
  335. _setBaseLayerWrapper(baseLayerWrapper) {
  336. if (this.isNative) {
  337. this._baseLayerRTTProvider?.dispose();
  338. }
  339. this._baseLayerWrapper = baseLayerWrapper;
  340. this._baseLayerRTTProvider = this._baseLayerWrapper?.createRenderTargetTextureProvider(this) || null;
  341. }
  342. /**
  343. * @internal
  344. */
  345. _getBaseLayerWrapper() {
  346. return this._baseLayerWrapper;
  347. }
  348. /**
  349. * Updates the render state of the session
  350. * @param state state to set
  351. */
  352. updateRenderState(state) {
  353. if (state.baseLayer) {
  354. this._setBaseLayerWrapper(this.isNative ? new NativeXRLayerWrapper(state.baseLayer) : new WebXRWebGLLayerWrapper(state.baseLayer));
  355. }
  356. this.session.updateRenderState(state);
  357. }
  358. /**
  359. * Returns a promise that resolves with a boolean indicating if the provided session mode is supported by this browser
  360. * @param sessionMode defines the session to test
  361. * @returns a promise with boolean as final value
  362. */
  363. static IsSessionSupportedAsync(sessionMode) {
  364. if (!navigator.xr) {
  365. return Promise.resolve(false);
  366. }
  367. // When the specs are final, remove supportsSession!
  368. const functionToUse = navigator.xr.isSessionSupported || navigator.xr.supportsSession;
  369. if (!functionToUse) {
  370. return Promise.resolve(false);
  371. }
  372. else {
  373. return functionToUse
  374. .call(navigator.xr, sessionMode)
  375. .then((result) => {
  376. const returnValue = typeof result === "undefined" ? true : result;
  377. return Promise.resolve(returnValue);
  378. })
  379. .catch((e) => {
  380. Logger.Warn(e);
  381. return Promise.resolve(false);
  382. });
  383. }
  384. }
  385. /**
  386. * Returns true if Babylon.js is using the BabylonNative backend, otherwise false
  387. */
  388. get isNative() {
  389. return this._xrNavigator.xr.native ?? false;
  390. }
  391. /**
  392. * The current frame rate as reported by the device
  393. */
  394. get currentFrameRate() {
  395. return this.session?.frameRate;
  396. }
  397. /**
  398. * A list of supported frame rates (only available in-session!
  399. */
  400. get supportedFrameRates() {
  401. return this.session?.supportedFrameRates;
  402. }
  403. /**
  404. * Set the framerate of the session.
  405. * @param rate the new framerate. This value needs to be in the supportedFrameRates array
  406. * @returns a promise that resolves once the framerate has been set
  407. */
  408. updateTargetFrameRate(rate) {
  409. return this.session.updateTargetFrameRate(rate);
  410. }
  411. /**
  412. * Run a callback in the xr render loop
  413. * @param callback the callback to call when in XR Frame
  414. * @param ignoreIfNotInSession if no session is currently running, run it first thing on the next session
  415. */
  416. runInXRFrame(callback, ignoreIfNotInSession = true) {
  417. if (this.inXRFrameLoop) {
  418. callback();
  419. }
  420. else if (this.inXRSession || !ignoreIfNotInSession) {
  421. this.onXRFrameObservable.addOnce(callback);
  422. }
  423. }
  424. /**
  425. * Check if fixed foveation is supported on this device
  426. */
  427. get isFixedFoveationSupported() {
  428. return this._baseLayerWrapper?.isFixedFoveationSupported || false;
  429. }
  430. /**
  431. * Get the fixed foveation currently set, as specified by the webxr specs
  432. * If this returns null, then fixed foveation is not supported
  433. */
  434. get fixedFoveation() {
  435. return this._baseLayerWrapper?.fixedFoveation || null;
  436. }
  437. /**
  438. * Set the fixed foveation to the specified value, as specified by the webxr specs
  439. * This value will be normalized to be between 0 and 1, 1 being max foveation, 0 being no foveation
  440. */
  441. set fixedFoveation(value) {
  442. const val = Math.max(0, Math.min(1, value || 0));
  443. if (this._baseLayerWrapper) {
  444. this._baseLayerWrapper.fixedFoveation = val;
  445. }
  446. }
  447. /**
  448. * Get the features enabled on the current session
  449. * This is only available in-session!
  450. * @see https://www.w3.org/TR/webxr/#dom-xrsession-enabledfeatures
  451. */
  452. get enabledFeatures() {
  453. return this.session?.enabledFeatures ?? null;
  454. }
  455. }
  456. //# sourceMappingURL=webXRSessionManager.js.map