webXRCamera.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. import { Vector3, Matrix, Quaternion } from "../Maths/math.vector";
  2. import { Scene } from "../scene";
  3. import { Camera } from "../Cameras/camera";
  4. import { FreeCamera } from "../Cameras/freeCamera";
  5. import { TargetCamera } from "../Cameras/targetCamera";
  6. import { WebXRSessionManager } from "./webXRSessionManager";
  7. import { Viewport } from "../Maths/math.viewport";
  8. import { Observable } from "../Misc/observable";
  9. /**
  10. * WebXR Camera which holds the views for the xrSession
  11. * @see https://doc.babylonjs.com/how_to/webxr_camera
  12. */
  13. export class WebXRCamera extends FreeCamera {
  14. private _firstFrame = false;
  15. private _referenceQuaternion: Quaternion = Quaternion.Identity();
  16. private _referencedPosition: Vector3 = new Vector3();
  17. private _xrInvPositionCache: Vector3 = new Vector3();
  18. private _xrInvQuaternionCache = Quaternion.Identity();
  19. /**
  20. * Observable raised before camera teleportation
  21. */
  22. public onBeforeCameraTeleport = new Observable<Vector3>();
  23. /**
  24. * Observable raised after camera teleportation
  25. */
  26. public onAfterCameraTeleport = new Observable<Vector3>();
  27. /**
  28. * Should position compensation execute on first frame.
  29. * This is used when copying the position from a native (non XR) camera
  30. */
  31. public compensateOnFirstFrame: boolean = true;
  32. /**
  33. * Creates a new webXRCamera, this should only be set at the camera after it has been updated by the xrSessionManager
  34. * @param name the name of the camera
  35. * @param scene the scene to add the camera to
  36. * @param _xrSessionManager a constructed xr session manager
  37. */
  38. constructor(name: string, scene: Scene, private _xrSessionManager: WebXRSessionManager) {
  39. super(name, Vector3.Zero(), scene);
  40. // Initial camera configuration
  41. this.minZ = 0.1;
  42. this.rotationQuaternion = new Quaternion();
  43. this.cameraRigMode = Camera.RIG_MODE_CUSTOM;
  44. this.updateUpVectorFromRotation = true;
  45. this._updateNumberOfRigCameras(1);
  46. // freeze projection matrix, which will be copied later
  47. this.freezeProjectionMatrix();
  48. this._xrSessionManager.onXRSessionInit.add(() => {
  49. this._referencedPosition.copyFromFloats(0, 0, 0);
  50. this._referenceQuaternion.copyFromFloats(0, 0, 0, 1);
  51. // first frame - camera's y position should be 0 for the correct offset
  52. this._firstFrame = this.compensateOnFirstFrame;
  53. });
  54. // Check transformation changes on each frame. Callback is added to be first so that the transformation will be
  55. // applied to the rest of the elements using the referenceSpace object
  56. this._xrSessionManager.onXRFrameObservable.add(
  57. (frame) => {
  58. if (this._firstFrame) {
  59. this._updateFromXRSession();
  60. }
  61. this._updateReferenceSpace();
  62. this._updateFromXRSession();
  63. },
  64. undefined,
  65. true
  66. );
  67. }
  68. /**
  69. * Return the user's height, unrelated to the current ground.
  70. * This will be the y position of this camera, when ground level is 0.
  71. */
  72. public get realWorldHeight(): number {
  73. const basePose = this._xrSessionManager.currentFrame && this._xrSessionManager.currentFrame.getViewerPose(this._xrSessionManager.baseReferenceSpace);
  74. if (basePose && basePose.transform) {
  75. return basePose.transform.position.y;
  76. } else {
  77. return 0;
  78. }
  79. }
  80. /** @hidden */
  81. public _updateForDualEyeDebugging(/*pupilDistance = 0.01*/) {
  82. // Create initial camera rigs
  83. this._updateNumberOfRigCameras(2);
  84. this.rigCameras[0].viewport = new Viewport(0, 0, 0.5, 1.0);
  85. // this.rigCameras[0].position.x = -pupilDistance / 2;
  86. this.rigCameras[0].outputRenderTarget = null;
  87. this.rigCameras[1].viewport = new Viewport(0.5, 0, 0.5, 1.0);
  88. // this.rigCameras[1].position.x = pupilDistance / 2;
  89. this.rigCameras[1].outputRenderTarget = null;
  90. }
  91. /**
  92. * Sets this camera's transformation based on a non-vr camera
  93. * @param otherCamera the non-vr camera to copy the transformation from
  94. * @param resetToBaseReferenceSpace should XR reset to the base reference space
  95. */
  96. public setTransformationFromNonVRCamera(otherCamera: Camera = this.getScene().activeCamera!, resetToBaseReferenceSpace: boolean = true) {
  97. if (!otherCamera || otherCamera === this) {
  98. return;
  99. }
  100. const mat = otherCamera.computeWorldMatrix();
  101. mat.decompose(undefined, this.rotationQuaternion, this.position);
  102. // set the ground level
  103. this.position.y = 0;
  104. Quaternion.FromEulerAnglesToRef(0, this.rotationQuaternion.toEulerAngles().y, 0, this.rotationQuaternion);
  105. this._firstFrame = true;
  106. if (resetToBaseReferenceSpace) {
  107. this._xrSessionManager.resetReferenceSpace();
  108. }
  109. }
  110. /**
  111. * Gets the current instance class name ("WebXRCamera").
  112. * @returns the class name
  113. */
  114. public getClassName(): string {
  115. return "WebXRCamera";
  116. }
  117. private _rotate180 = new Quaternion(0, 1, 0, 0);
  118. private _updateFromXRSession() {
  119. const pose = this._xrSessionManager.currentFrame && this._xrSessionManager.currentFrame.getViewerPose(this._xrSessionManager.referenceSpace);
  120. if (!pose) {
  121. return;
  122. }
  123. if (pose.transform) {
  124. const pos = pose.transform.position;
  125. this._referencedPosition.set(pos.x, pos.y, pos.z);
  126. const orientation = pose.transform.orientation;
  127. this._referenceQuaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
  128. if (!this._scene.useRightHandedSystem) {
  129. this._referencedPosition.z *= -1;
  130. this._referenceQuaternion.z *= -1;
  131. this._referenceQuaternion.w *= -1;
  132. }
  133. if (this._firstFrame) {
  134. this._firstFrame = false;
  135. // we have the XR reference, now use this to find the offset to get the camera to be
  136. // in the right position
  137. // set the height to correlate to the current height
  138. this.position.y += this._referencedPosition.y;
  139. // avoid using the head rotation on the first frame.
  140. this._referenceQuaternion.copyFromFloats(0, 0, 0, 1);
  141. } else {
  142. // update position and rotation as reference
  143. this.rotationQuaternion.copyFrom(this._referenceQuaternion);
  144. this.position.copyFrom(this._referencedPosition);
  145. }
  146. }
  147. // Update camera rigs
  148. if (this.rigCameras.length !== pose.views.length) {
  149. this._updateNumberOfRigCameras(pose.views.length);
  150. }
  151. pose.views.forEach((view: XRView, i: number) => {
  152. const currentRig = <TargetCamera>this.rigCameras[i];
  153. // update right and left, where applicable
  154. if (!currentRig.isLeftCamera && !currentRig.isRightCamera) {
  155. if (view.eye === "right") {
  156. currentRig._isRightCamera = true;
  157. } else if (view.eye === "left") {
  158. currentRig._isLeftCamera = true;
  159. }
  160. }
  161. // Update view/projection matrix
  162. const pos = view.transform.position;
  163. const orientation = view.transform.orientation;
  164. currentRig.position.set(pos.x, pos.y, pos.z);
  165. currentRig.rotationQuaternion.set(orientation.x, orientation.y, orientation.z, orientation.w);
  166. if (!this._scene.useRightHandedSystem) {
  167. currentRig.position.z *= -1;
  168. currentRig.rotationQuaternion.z *= -1;
  169. currentRig.rotationQuaternion.w *= -1;
  170. } else {
  171. currentRig.rotationQuaternion.multiplyInPlace(this._rotate180);
  172. }
  173. Matrix.FromFloat32ArrayToRefScaled(view.projectionMatrix, 0, 1, currentRig._projectionMatrix);
  174. if (!this._scene.useRightHandedSystem) {
  175. currentRig._projectionMatrix.toggleProjectionMatrixHandInPlace();
  176. }
  177. // first camera?
  178. if (i === 0) {
  179. this._projectionMatrix.copyFrom(currentRig._projectionMatrix);
  180. }
  181. // Update viewport
  182. if (this._xrSessionManager.session.renderState.baseLayer) {
  183. var viewport = this._xrSessionManager.session.renderState.baseLayer.getViewport(view);
  184. var width = this._xrSessionManager.session.renderState.baseLayer.framebufferWidth;
  185. var height = this._xrSessionManager.session.renderState.baseLayer.framebufferHeight;
  186. currentRig.viewport.width = viewport.width / width;
  187. currentRig.viewport.height = viewport.height / height;
  188. currentRig.viewport.x = viewport.x / width;
  189. currentRig.viewport.y = viewport.y / height;
  190. }
  191. // Set cameras to render to the session's render target
  192. currentRig.outputRenderTarget = this._xrSessionManager.getRenderTargetTextureForEye(view.eye);
  193. });
  194. }
  195. private _updateNumberOfRigCameras(viewCount = 1) {
  196. while (this.rigCameras.length < viewCount) {
  197. var newCamera = new TargetCamera("XR-RigCamera: " + this.rigCameras.length, Vector3.Zero(), this.getScene());
  198. newCamera.minZ = 0.1;
  199. newCamera.rotationQuaternion = new Quaternion();
  200. newCamera.updateUpVectorFromRotation = true;
  201. newCamera.isRigCamera = true;
  202. newCamera.rigParent = this;
  203. // do not compute projection matrix, provided by XR
  204. newCamera.freezeProjectionMatrix();
  205. this.rigCameras.push(newCamera);
  206. }
  207. while (this.rigCameras.length > viewCount) {
  208. var removedCamera = this.rigCameras.pop();
  209. if (removedCamera) {
  210. removedCamera.dispose();
  211. }
  212. }
  213. }
  214. private _updateReferenceSpace() {
  215. // were position & rotation updated OUTSIDE of the xr update loop
  216. if (!this.position.equals(this._referencedPosition) || !this.rotationQuaternion.equals(this._referenceQuaternion)) {
  217. this.position.subtractToRef(this._referencedPosition, this._referencedPosition);
  218. this._referenceQuaternion.conjugateInPlace();
  219. this._referenceQuaternion.multiplyToRef(this.rotationQuaternion, this._referenceQuaternion);
  220. this._updateReferenceSpaceOffset(this._referencedPosition, this._referenceQuaternion.normalize());
  221. }
  222. }
  223. private _updateReferenceSpaceOffset(positionOffset: Vector3, rotationOffset?: Quaternion, ignoreHeight: boolean = false) {
  224. if (!this._xrSessionManager.referenceSpace || !this._xrSessionManager.currentFrame) {
  225. return;
  226. }
  227. // Compute the origin offset based on player position/orientation.
  228. this._xrInvPositionCache.copyFrom(positionOffset);
  229. if (rotationOffset) {
  230. this._xrInvQuaternionCache.copyFrom(rotationOffset);
  231. } else {
  232. this._xrInvQuaternionCache.copyFromFloats(0, 0, 0, 1);
  233. }
  234. // right handed system
  235. if (!this._scene.useRightHandedSystem) {
  236. this._xrInvPositionCache.z *= -1;
  237. this._xrInvQuaternionCache.z *= -1;
  238. this._xrInvQuaternionCache.w *= -1;
  239. }
  240. this._xrInvPositionCache.negateInPlace();
  241. this._xrInvQuaternionCache.conjugateInPlace();
  242. // transform point according to rotation with pivot
  243. this._xrInvPositionCache.rotateByQuaternionToRef(this._xrInvQuaternionCache, this._xrInvPositionCache);
  244. if (ignoreHeight) {
  245. this._xrInvPositionCache.y = 0;
  246. }
  247. const transform = new XRRigidTransform({ x: this._xrInvPositionCache.x, y: this._xrInvPositionCache.y, z: this._xrInvPositionCache.z }, { x: this._xrInvQuaternionCache.x, y: this._xrInvQuaternionCache.y, z: this._xrInvQuaternionCache.z, w: this._xrInvQuaternionCache.w });
  248. // Update offset reference to use a new originOffset with the teleported
  249. // player position and orientation.
  250. // This new offset needs to be applied to the base ref space.
  251. const referenceSpace = this._xrSessionManager.referenceSpace.getOffsetReferenceSpace(transform);
  252. const pose = this._xrSessionManager.currentFrame && this._xrSessionManager.currentFrame.getViewerPose(referenceSpace);
  253. if (pose) {
  254. const pos = new Vector3(pose.transform.position.x, pose.transform.position.y, pose.transform.position.z);
  255. if (!this._scene.useRightHandedSystem) {
  256. pos.z *= -1;
  257. }
  258. this.position.subtractToRef(pos, pos);
  259. if (!this._scene.useRightHandedSystem) {
  260. pos.z *= -1;
  261. }
  262. pos.negateInPlace();
  263. const transform2 = new XRRigidTransform({ x: pos.x, y: pos.y, z: pos.z });
  264. // Update offset reference to use a new originOffset with the teleported
  265. // player position and orientation.
  266. // This new offset needs to be applied to the base ref space.
  267. this._xrSessionManager.referenceSpace = referenceSpace.getOffsetReferenceSpace(transform2);
  268. }
  269. }
  270. }