webXRCamera.ts 13 KB

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