webXRSessionManager.ts 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import { Logger } from "../Misc/logger";
  2. import { Observable } from "../Misc/observable";
  3. import { Nullable } from "../types";
  4. import { IDisposable, Scene } from "../scene";
  5. import { InternalTexture, InternalTextureSource } from "../Materials/Textures/internalTexture";
  6. import { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture";
  7. import { WebXRRenderTarget } from './webXRTypes';
  8. import { WebXRManagedOutputCanvas, WebXRManagedOutputCanvasOptions } from './webXRManagedOutputCanvas';
  9. interface IRenderTargetProvider {
  10. getRenderTargetForEye(eye: XREye): RenderTargetTexture;
  11. }
  12. class RenderTargetProvider implements IRenderTargetProvider {
  13. private _texture: RenderTargetTexture;
  14. public constructor(texture: RenderTargetTexture) {
  15. this._texture = texture;
  16. }
  17. public getRenderTargetForEye(eye: XREye): RenderTargetTexture {
  18. return this._texture;
  19. }
  20. }
  21. /**
  22. * Manages an XRSession to work with Babylon's engine
  23. * @see https://doc.babylonjs.com/how_to/webxr
  24. */
  25. export class WebXRSessionManager implements IDisposable {
  26. private _referenceSpace: XRReferenceSpace;
  27. private _rttProvider: Nullable<IRenderTargetProvider>;
  28. private _sessionEnded: boolean = false;
  29. private _xrNavigator: any;
  30. private baseLayer: Nullable<XRWebGLLayer> = null;
  31. /**
  32. * The base reference space from which the session started. good if you want to reset your
  33. * reference space
  34. */
  35. public baseReferenceSpace: XRReferenceSpace;
  36. /**
  37. * Current XR frame
  38. */
  39. public currentFrame: Nullable<XRFrame>;
  40. /** WebXR timestamp updated every frame */
  41. public currentTimestamp: number = -1;
  42. /**
  43. * Used just in case of a failure to initialize an immersive session.
  44. * The viewer reference space is compensated using this height, creating a kind of "viewer-floor" reference space
  45. */
  46. public defaultHeightCompensation = 1.7;
  47. /**
  48. * Fires every time a new xrFrame arrives which can be used to update the camera
  49. */
  50. public onXRFrameObservable: Observable<XRFrame> = new Observable<XRFrame>();
  51. /**
  52. * Fires when the reference space changed
  53. */
  54. public onXRReferenceSpaceChanged: Observable<XRReferenceSpace> = new Observable();
  55. /**
  56. * Fires when the xr session is ended either by the device or manually done
  57. */
  58. public onXRSessionEnded: Observable<any> = new Observable<any>();
  59. /**
  60. * Fires when the xr session is ended either by the device or manually done
  61. */
  62. public onXRSessionInit: Observable<XRSession> = new Observable<XRSession>();
  63. /**
  64. * Underlying xr session
  65. */
  66. public session: XRSession;
  67. /**
  68. * The viewer (head position) reference space. This can be used to get the XR world coordinates
  69. * or get the offset the player is currently at.
  70. */
  71. public viewerReferenceSpace: XRReferenceSpace;
  72. /**
  73. * Constructs a WebXRSessionManager, this must be initialized within a user action before usage
  74. * @param scene The scene which the session should be created for
  75. */
  76. constructor(
  77. /** The scene which the session should be created for */
  78. public scene: Scene
  79. ) {
  80. }
  81. /**
  82. * The current reference space used in this session. This reference space can constantly change!
  83. * It is mainly used to offset the camera's position.
  84. */
  85. public get referenceSpace(): XRReferenceSpace {
  86. return this._referenceSpace;
  87. }
  88. /**
  89. * Set a new reference space and triggers the observable
  90. */
  91. public set referenceSpace(newReferenceSpace: XRReferenceSpace) {
  92. this._referenceSpace = newReferenceSpace;
  93. this.onXRReferenceSpaceChanged.notifyObservers(this._referenceSpace);
  94. }
  95. /**
  96. * Disposes of the session manager
  97. */
  98. public dispose() {
  99. // disposing without leaving XR? Exit XR first
  100. if (!this._sessionEnded) {
  101. this.exitXRAsync();
  102. }
  103. this.onXRFrameObservable.clear();
  104. this.onXRSessionEnded.clear();
  105. this.onXRReferenceSpaceChanged.clear();
  106. this.onXRSessionInit.clear();
  107. }
  108. /**
  109. * Stops the xrSession and restores the render loop
  110. * @returns Promise which resolves after it exits XR
  111. */
  112. public exitXRAsync() {
  113. if (this.session && !this._sessionEnded) {
  114. return this.session.end().catch((e) => {
  115. Logger.Warn("could not end XR session. It has ended already.");
  116. });
  117. }
  118. return Promise.resolve();
  119. }
  120. /**
  121. * Gets the correct render target texture to be rendered this frame for this eye
  122. * @param eye the eye for which to get the render target
  123. * @returns the render target for the specified eye
  124. */
  125. public getRenderTargetTextureForEye(eye: XREye): RenderTargetTexture {
  126. return this._rttProvider!.getRenderTargetForEye(eye);
  127. }
  128. /**
  129. * Creates a WebXRRenderTarget object for the XR session
  130. * @param onStateChangedObservable optional, mechanism for enabling/disabling XR rendering canvas, used only on Web
  131. * @param options optional options to provide when creating a new render target
  132. * @returns a WebXR render target to which the session can render
  133. */
  134. public getWebXRRenderTarget(options?: WebXRManagedOutputCanvasOptions): WebXRRenderTarget {
  135. const engine = this.scene.getEngine();
  136. if (this._xrNavigator.xr.native) {
  137. return this._xrNavigator.xr.getWebXRRenderTarget(engine);
  138. }
  139. else {
  140. options = options || {};
  141. options.canvasElement = engine.getRenderingCanvas() || undefined;
  142. return new WebXRManagedOutputCanvas(this, options);
  143. }
  144. }
  145. /**
  146. * Initializes the manager
  147. * After initialization enterXR can be called to start an XR session
  148. * @returns Promise which resolves after it is initialized
  149. */
  150. public initializeAsync(): Promise<void> {
  151. // Check if the browser supports webXR
  152. this._xrNavigator = navigator;
  153. if (!this._xrNavigator.xr) {
  154. return Promise.reject("WebXR not available");
  155. }
  156. return Promise.resolve();
  157. }
  158. /**
  159. * Initializes an xr session
  160. * @param xrSessionMode mode to initialize
  161. * @param xrSessionInit defines optional and required values to pass to the session builder
  162. * @returns a promise which will resolve once the session has been initialized
  163. */
  164. public initializeSessionAsync(xrSessionMode: XRSessionMode = 'immersive-vr', xrSessionInit: XRSessionInit = {}): Promise<XRSession> {
  165. return this._xrNavigator.xr.requestSession(xrSessionMode, xrSessionInit).then((session: XRSession) => {
  166. this.session = session;
  167. this.onXRSessionInit.notifyObservers(session);
  168. this._sessionEnded = false;
  169. // handle when the session is ended (By calling session.end or device ends its own session eg. pressing home button on phone)
  170. this.session.addEventListener("end", () => {
  171. const engine = this.scene.getEngine();
  172. this._sessionEnded = true;
  173. // Remove render target texture and notify frame observers
  174. this._rttProvider = null;
  175. // Restore frame buffer to avoid clear on xr framebuffer after session end
  176. engine.restoreDefaultFramebuffer();
  177. // Need to restart render loop as after the session is ended the last request for new frame will never call callback
  178. engine.customAnimationFrameRequester = null;
  179. this.onXRSessionEnded.notifyObservers(null);
  180. engine._renderLoop();
  181. }, { once: true });
  182. return this.session;
  183. });
  184. }
  185. /**
  186. * Checks if a session would be supported for the creation options specified
  187. * @param sessionMode session mode to check if supported eg. immersive-vr
  188. * @returns A Promise that resolves to true if supported and false if not
  189. */
  190. public isSessionSupportedAsync(sessionMode: XRSessionMode): Promise<boolean> {
  191. return WebXRSessionManager.IsSessionSupportedAsync(sessionMode);
  192. }
  193. /**
  194. * Resets the reference space to the one started the session
  195. */
  196. public resetReferenceSpace() {
  197. this.referenceSpace = this.baseReferenceSpace;
  198. }
  199. /**
  200. * Starts rendering to the xr layer
  201. */
  202. public runXRRenderLoop() {
  203. const engine = this.scene.getEngine();
  204. // Tell the engine's render loop to be driven by the xr session's refresh rate and provide xr pose information
  205. engine.customAnimationFrameRequester = {
  206. requestAnimationFrame: this.session.requestAnimationFrame.bind(this.session),
  207. renderFunction: (timestamp: number, xrFrame: Nullable<XRFrame>) => {
  208. if (this._sessionEnded) {
  209. return;
  210. }
  211. // Store the XR frame and timestamp in the session manager
  212. this.currentFrame = xrFrame;
  213. this.currentTimestamp = timestamp;
  214. if (xrFrame) {
  215. this.onXRFrameObservable.notifyObservers(xrFrame);
  216. // only run the render loop if a frame exists
  217. engine._renderLoop();
  218. }
  219. }
  220. };
  221. if (this._xrNavigator.xr.native) {
  222. this._rttProvider = this._xrNavigator.xr.getNativeRenderTargetProvider(this.session, (width: number, height: number) => {
  223. return engine.createRenderTargetTexture({ width: width, height: height }, false);
  224. });
  225. } else {
  226. // Create render target texture from WebXR's webgl render target
  227. this._rttProvider = new RenderTargetProvider(WebXRSessionManager._CreateRenderTargetTextureFromSession(this.session, this.scene, this.baseLayer!));
  228. }
  229. // Stop window's animation frame and trigger sessions animation frame
  230. if (window.cancelAnimationFrame) { window.cancelAnimationFrame(engine._frameHandler); }
  231. engine._renderLoop();
  232. }
  233. /**
  234. * Sets the reference space on the xr session
  235. * @param referenceSpaceType space to set
  236. * @returns a promise that will resolve once the reference space has been set
  237. */
  238. public setReferenceSpaceTypeAsync(referenceSpaceType: XRReferenceSpaceType = "local-floor"): Promise<XRReferenceSpace> {
  239. return this.session.requestReferenceSpace(referenceSpaceType).then((referenceSpace: XRReferenceSpace) => {
  240. return referenceSpace;
  241. }, (rejectionReason) => {
  242. Logger.Error("XR.requestReferenceSpace failed for the following reason: ");
  243. Logger.Error(rejectionReason);
  244. Logger.Log("Defaulting to universally-supported \"viewer\" reference space type.");
  245. return this.session.requestReferenceSpace("viewer").then((referenceSpace: XRReferenceSpace) => {
  246. const heightCompensation = new XRRigidTransform({ x: 0, y: -this.defaultHeightCompensation, z: 0 });
  247. return referenceSpace.getOffsetReferenceSpace(heightCompensation);
  248. }, (rejectionReason) => {
  249. Logger.Error(rejectionReason);
  250. throw "XR initialization failed: required \"viewer\" reference space type not supported.";
  251. });
  252. }).then((referenceSpace) => {
  253. // initialize the base and offset (currently the same)
  254. this.referenceSpace = this.baseReferenceSpace = referenceSpace;
  255. this.session.requestReferenceSpace("viewer").then((referenceSpace: XRReferenceSpace) => {
  256. this.viewerReferenceSpace = referenceSpace;
  257. });
  258. return this.referenceSpace;
  259. });
  260. }
  261. /**
  262. * Updates the render state of the session
  263. * @param state state to set
  264. * @returns a promise that resolves once the render state has been updated
  265. */
  266. public updateRenderStateAsync(state: XRRenderState) {
  267. if (state.baseLayer) {
  268. this.baseLayer = state.baseLayer;
  269. }
  270. return this.session.updateRenderState(state);
  271. }
  272. /**
  273. * Returns a promise that resolves with a boolean indicating if the provided session mode is supported by this browser
  274. * @param sessionMode defines the session to test
  275. * @returns a promise with boolean as final value
  276. */
  277. public static IsSessionSupportedAsync(sessionMode: XRSessionMode): Promise<boolean> {
  278. if (!(navigator as any).xr) {
  279. return Promise.resolve(false);
  280. }
  281. // When the specs are final, remove supportsSession!
  282. const functionToUse = (navigator as any).xr.isSessionSupported || (navigator as any).xr.supportsSession;
  283. if (!functionToUse) {
  284. return Promise.resolve(false);
  285. } else {
  286. return functionToUse.call((navigator as any).xr, sessionMode).then((result: boolean) => {
  287. const returnValue = (typeof result === "undefined") ? true : result;
  288. return Promise.resolve(returnValue);
  289. }).catch((e: any) => {
  290. Logger.Warn(e);
  291. return Promise.resolve(false);
  292. });
  293. }
  294. }
  295. /**
  296. * @hidden
  297. * Converts the render layer of xrSession to a render target
  298. * @param session session to create render target for
  299. * @param scene scene the new render target should be created for
  300. * @param baseLayer the webgl layer to create the render target for
  301. */
  302. public static _CreateRenderTargetTextureFromSession(_session: XRSession, scene: Scene, baseLayer: XRWebGLLayer) {
  303. if (!baseLayer) {
  304. throw "no layer";
  305. }
  306. // Create internal texture
  307. var internalTexture = new InternalTexture(scene.getEngine(), InternalTextureSource.Unknown, true);
  308. internalTexture.width = baseLayer.framebufferWidth;
  309. internalTexture.height = baseLayer.framebufferHeight;
  310. internalTexture._framebuffer = baseLayer.framebuffer;
  311. // Create render target texture from the internal texture
  312. var renderTargetTexture = new RenderTargetTexture("XR renderTargetTexture", { width: internalTexture.width, height: internalTexture.height }, scene, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, true);
  313. renderTargetTexture._texture = internalTexture;
  314. return renderTargetTexture;
  315. }
  316. }