webXRExperienceHelper.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import { Observable } from "../Misc/observable.js";
  2. import { WebXRSessionManager } from "./webXRSessionManager.js";
  3. import { WebXRCamera } from "./webXRCamera.js";
  4. import { WebXRState } from "./webXRTypes.js";
  5. import { WebXRFeatureName, WebXRFeaturesManager } from "./webXRFeaturesManager.js";
  6. import { Logger } from "../Misc/logger.js";
  7. import { UniversalCamera } from "../Cameras/universalCamera.js";
  8. import { Quaternion, Vector3 } from "../Maths/math.vector.js";
  9. /**
  10. * Base set of functionality needed to create an XR experience (WebXRSessionManager, Camera, StateManagement, etc.)
  11. * @see https://doc.babylonjs.com/features/featuresDeepDive/webXR/webXRExperienceHelpers
  12. */
  13. export class WebXRExperienceHelper {
  14. /**
  15. * Creates a WebXRExperienceHelper
  16. * @param _scene The scene the helper should be created in
  17. */
  18. constructor(_scene) {
  19. this._scene = _scene;
  20. this._nonVRCamera = null;
  21. this._attachedToElement = false;
  22. this._spectatorCamera = null;
  23. this._originalSceneAutoClear = true;
  24. this._supported = false;
  25. this._spectatorMode = false;
  26. this._lastTimestamp = 0;
  27. /**
  28. * Observers registered here will be triggered after the camera's initial transformation is set
  29. * This can be used to set a different ground level or an extra rotation.
  30. *
  31. * Note that ground level is considered to be at 0. The height defined by the XR camera will be added
  32. * to the position set after this observable is done executing.
  33. */
  34. this.onInitialXRPoseSetObservable = new Observable();
  35. /**
  36. * Fires when the state of the experience helper has changed
  37. */
  38. this.onStateChangedObservable = new Observable();
  39. /**
  40. * The current state of the XR experience (eg. transitioning, in XR or not in XR)
  41. */
  42. this.state = WebXRState.NOT_IN_XR;
  43. this.sessionManager = new WebXRSessionManager(_scene);
  44. this.camera = new WebXRCamera("webxr", _scene, this.sessionManager);
  45. this.featuresManager = new WebXRFeaturesManager(this.sessionManager);
  46. _scene.onDisposeObservable.addOnce(() => {
  47. this.dispose();
  48. });
  49. }
  50. /**
  51. * Creates the experience helper
  52. * @param scene the scene to attach the experience helper to
  53. * @returns a promise for the experience helper
  54. */
  55. static CreateAsync(scene) {
  56. const helper = new WebXRExperienceHelper(scene);
  57. return helper.sessionManager
  58. .initializeAsync()
  59. .then(() => {
  60. helper._supported = true;
  61. return helper;
  62. })
  63. .catch((e) => {
  64. helper._setState(WebXRState.NOT_IN_XR);
  65. helper.dispose();
  66. throw e;
  67. });
  68. }
  69. /**
  70. * Disposes of the experience helper
  71. */
  72. dispose() {
  73. this.exitXRAsync();
  74. this.camera.dispose();
  75. this.onStateChangedObservable.clear();
  76. this.onInitialXRPoseSetObservable.clear();
  77. this.sessionManager.dispose();
  78. this._spectatorCamera?.dispose();
  79. if (this._nonVRCamera) {
  80. this._scene.activeCamera = this._nonVRCamera;
  81. }
  82. }
  83. /**
  84. * Enters XR mode (This must be done within a user interaction in most browsers eg. button click)
  85. * @param sessionMode options for the XR session
  86. * @param referenceSpaceType frame of reference of the XR session
  87. * @param renderTarget the output canvas that will be used to enter XR mode
  88. * @param sessionCreationOptions optional XRSessionInit object to init the session with
  89. * @returns promise that resolves after xr mode has entered
  90. */
  91. async enterXRAsync(sessionMode, referenceSpaceType, renderTarget = this.sessionManager.getWebXRRenderTarget(), sessionCreationOptions = {}) {
  92. if (!this._supported) {
  93. // eslint-disable-next-line no-throw-literal
  94. throw "WebXR not supported in this browser or environment";
  95. }
  96. this._setState(WebXRState.ENTERING_XR);
  97. if (referenceSpaceType !== "viewer" && referenceSpaceType !== "local") {
  98. sessionCreationOptions.optionalFeatures = sessionCreationOptions.optionalFeatures || [];
  99. sessionCreationOptions.optionalFeatures.push(referenceSpaceType);
  100. }
  101. sessionCreationOptions = await this.featuresManager._extendXRSessionInitObject(sessionCreationOptions);
  102. // we currently recommend "unbounded" space in AR (#7959)
  103. if (sessionMode === "immersive-ar" && referenceSpaceType !== "unbounded") {
  104. Logger.Warn("We recommend using 'unbounded' reference space type when using 'immersive-ar' session mode");
  105. }
  106. // make sure that the session mode is supported
  107. try {
  108. await this.sessionManager.initializeSessionAsync(sessionMode, sessionCreationOptions);
  109. await this.sessionManager.setReferenceSpaceTypeAsync(referenceSpaceType);
  110. const xrRenderState = {
  111. // if maxZ is 0 it should be "Infinity", but it doesn't work with the WebXR API. Setting to a large number.
  112. depthFar: this.camera.maxZ || 10000,
  113. depthNear: this.camera.minZ,
  114. };
  115. // The layers feature will have already initialized the xr session's layers on session init.
  116. if (!this.featuresManager.getEnabledFeature(WebXRFeatureName.LAYERS)) {
  117. const baseLayer = await renderTarget.initializeXRLayerAsync(this.sessionManager.session);
  118. xrRenderState.baseLayer = baseLayer;
  119. }
  120. this.sessionManager.updateRenderState(xrRenderState);
  121. // run the render loop
  122. this.sessionManager.runXRRenderLoop();
  123. // Cache pre xr scene settings
  124. this._originalSceneAutoClear = this._scene.autoClear;
  125. this._nonVRCamera = this._scene.activeCamera;
  126. this._attachedToElement = !!this._nonVRCamera?.inputs?.attachedToElement;
  127. this._nonVRCamera?.detachControl();
  128. this._scene.activeCamera = this.camera;
  129. // do not compensate when AR session is used
  130. if (sessionMode !== "immersive-ar") {
  131. this._nonXRToXRCamera();
  132. }
  133. else {
  134. // Kept here, TODO - check if needed
  135. this._scene.autoClear = false;
  136. this.camera.compensateOnFirstFrame = false;
  137. // reset the camera's position to the origin
  138. this.camera.position.set(0, 0, 0);
  139. this.camera.rotationQuaternion.set(0, 0, 0, 1);
  140. this.onInitialXRPoseSetObservable.notifyObservers(this.camera);
  141. }
  142. this.sessionManager.onXRSessionEnded.addOnce(() => {
  143. // when using the back button and not the exit button (default on mobile), the session is ending but the EXITING state was not set
  144. if (this.state !== WebXRState.EXITING_XR) {
  145. this._setState(WebXRState.EXITING_XR);
  146. }
  147. // Reset camera rigs output render target to ensure sessions render target is not drawn after it ends
  148. this.camera.rigCameras.forEach((c) => {
  149. c.outputRenderTarget = null;
  150. });
  151. // Restore scene settings
  152. this._scene.autoClear = this._originalSceneAutoClear;
  153. this._scene.activeCamera = this._nonVRCamera;
  154. if (this._attachedToElement && this._nonVRCamera) {
  155. this._nonVRCamera.attachControl(!!this._nonVRCamera.inputs.noPreventDefault);
  156. }
  157. if (sessionMode !== "immersive-ar" && this.camera.compensateOnFirstFrame) {
  158. if (this._nonVRCamera.setPosition) {
  159. this._nonVRCamera.setPosition(this.camera.position);
  160. }
  161. else {
  162. this._nonVRCamera.position.copyFrom(this.camera.position);
  163. }
  164. }
  165. this._setState(WebXRState.NOT_IN_XR);
  166. });
  167. // Wait until the first frame arrives before setting state to in xr
  168. this.sessionManager.onXRFrameObservable.addOnce(() => {
  169. this._setState(WebXRState.IN_XR);
  170. });
  171. return this.sessionManager;
  172. }
  173. catch (e) {
  174. Logger.Log(e);
  175. Logger.Log(e.message);
  176. this._setState(WebXRState.NOT_IN_XR);
  177. throw e;
  178. }
  179. }
  180. /**
  181. * Exits XR mode and returns the scene to its original state
  182. * @returns promise that resolves after xr mode has exited
  183. */
  184. exitXRAsync() {
  185. // only exit if state is IN_XR
  186. if (this.state !== WebXRState.IN_XR) {
  187. return Promise.resolve();
  188. }
  189. this._setState(WebXRState.EXITING_XR);
  190. return this.sessionManager.exitXRAsync();
  191. }
  192. /**
  193. * Enable spectator mode for desktop VR experiences.
  194. * When spectator mode is enabled a camera will be attached to the desktop canvas and will
  195. * display the first rig camera's view on the desktop canvas.
  196. * Please note that this will degrade performance, as it requires another camera render.
  197. * It is also not recommended to enable this in devices like the quest, as it brings no benefit there.
  198. * @param options giving WebXRSpectatorModeOption for specutator camera to setup when the spectator mode is enabled.
  199. */
  200. enableSpectatorMode(options) {
  201. if (!this._spectatorMode) {
  202. this._spectatorMode = true;
  203. this._switchSpectatorMode(options);
  204. }
  205. }
  206. /**
  207. * Disable spectator mode for desktop VR experiences.
  208. */
  209. disableSpecatatorMode() {
  210. if (this._spectatorMode) {
  211. this._spectatorMode = false;
  212. this._switchSpectatorMode();
  213. }
  214. }
  215. _switchSpectatorMode(options) {
  216. const fps = options?.fps ? options.fps : 1000.0;
  217. const refreshRate = (1.0 / fps) * 1000.0;
  218. const cameraIndex = options?.preferredCameraIndex ? options?.preferredCameraIndex : 0;
  219. const updateSpectatorCamera = () => {
  220. if (this._spectatorCamera) {
  221. const delta = this.sessionManager.currentTimestamp - this._lastTimestamp;
  222. if (delta >= refreshRate) {
  223. this._lastTimestamp = this.sessionManager.currentTimestamp;
  224. this._spectatorCamera.position.copyFrom(this.camera.rigCameras[cameraIndex].globalPosition);
  225. this._spectatorCamera.rotationQuaternion.copyFrom(this.camera.rigCameras[cameraIndex].absoluteRotation);
  226. }
  227. }
  228. };
  229. if (this._spectatorMode) {
  230. if (cameraIndex >= this.camera.rigCameras.length) {
  231. throw new Error("the preferred camera index is beyond the length of rig camera array.");
  232. }
  233. const onStateChanged = () => {
  234. if (this.state === WebXRState.IN_XR) {
  235. this._spectatorCamera = new UniversalCamera("webxr-spectator", Vector3.Zero(), this._scene);
  236. this._spectatorCamera.rotationQuaternion = new Quaternion();
  237. this._scene.activeCameras = [this.camera, this._spectatorCamera];
  238. this.sessionManager.onXRFrameObservable.add(updateSpectatorCamera);
  239. this._scene.onAfterRenderCameraObservable.add((camera) => {
  240. if (camera === this.camera) {
  241. // reset the dimensions object for correct resizing
  242. this._scene.getEngine().framebufferDimensionsObject = null;
  243. }
  244. });
  245. }
  246. else if (this.state === WebXRState.EXITING_XR) {
  247. this.sessionManager.onXRFrameObservable.removeCallback(updateSpectatorCamera);
  248. this._scene.activeCameras = null;
  249. }
  250. };
  251. this.onStateChangedObservable.add(onStateChanged);
  252. onStateChanged();
  253. }
  254. else {
  255. this.sessionManager.onXRFrameObservable.removeCallback(updateSpectatorCamera);
  256. this._scene.activeCameras = [this.camera];
  257. }
  258. }
  259. _nonXRToXRCamera() {
  260. this.camera.setTransformationFromNonVRCamera(this._nonVRCamera);
  261. this.onInitialXRPoseSetObservable.notifyObservers(this.camera);
  262. }
  263. _setState(val) {
  264. if (this.state === val) {
  265. return;
  266. }
  267. this.state = val;
  268. this.onStateChangedObservable.notifyObservers(this.state);
  269. }
  270. }
  271. //# sourceMappingURL=webXRExperienceHelper.js.map