Pārlūkot izejas kodu

Merge pull request #7343 from RaananW/positioning-api

WebXR camera API
David Catuhe 5 gadi atpakaļ
vecāks
revīzija
af5648d9bb

+ 2 - 1
dist/preview release/what's new.md

@@ -180,6 +180,7 @@
 - WebXR teleportation can now be disabled after initialized ([RaananW](https://github.com/RaananW/))
 - New Features Manager for WebXR features ([RaananW](https://github.com/RaananW/))
 - New features - Plane detection, Hit Test, Background remover ([RaananW](https://github.com/RaananW/))
+- Camera's API works as expected (position, rotationQuaternion, world matrix etc') ([#7239](https://github.com/BabylonJS/Babylon.js/issues/7239)) ([RaananW](https://github.com/RaananW/))
 
 ### Ray
 
@@ -211,10 +212,10 @@
 - Added Light intensity output to LightInformationBlock ([Drigax](https://github.com/drigax))
 
 ### Serializers
+
 - Added support for `AnimationGroup` serialization ([Drigax](https://github.com/drigax/))
 - Expanded animation group serialization to include all targeted TransformNodes ([Drigax]https://github.com/drigax/)
 
-
 ### Documentation
 
 - Added a note on shallow bounding of getBoundingInfo ([tibotiber](https://github.com/tibotiber))

+ 129 - 24
src/Cameras/XR/webXRCamera.ts

@@ -17,12 +17,18 @@ export class WebXRCamera extends FreeCamera {
      */
     public debugMode = false;
 
+    private _firstFrame = false;
+    private _referencedPosition: Vector3 = new Vector3();
+    private _referenceQuaternion: Quaternion = Quaternion.Identity();
+    private _xrInvPositionCache: Vector3 = new Vector3();
+    private _xrInvQuaternionCache = Quaternion.Identity();
+
     /**
      * Creates a new webXRCamera, this should only be set at the camera after it has been updated by the xrSessionManager
      * @param name the name of the camera
      * @param scene the scene to add the camera to
      */
-    constructor(name: string, scene: Scene) {
+    constructor(name: string, scene: Scene, private _xrSessionManager: WebXRSessionManager) {
         super(name, Vector3.Zero(), scene);
 
         // Initial camera configuration
@@ -31,13 +37,19 @@ export class WebXRCamera extends FreeCamera {
         this.cameraRigMode = Camera.RIG_MODE_CUSTOM;
         this.updateUpVectorFromRotation = true;
         this._updateNumberOfRigCameras(1);
+
+        this._xrSessionManager.onXRSessionInit.add(() => {
+            this._referencedPosition.copyFromFloats(0, 0, 0);
+            this._referenceQuaternion.copyFromFloats(0, 0, 0, 1);
+            // first frame - camera's y position should be 0 for the correct offset
+            this._firstFrame = true;
+        });
     }
 
     private _updateNumberOfRigCameras(viewCount = 1) {
         while (this.rigCameras.length < viewCount) {
             var newCamera = new TargetCamera("view: " + this.rigCameras.length, Vector3.Zero(), this.getScene());
             newCamera.minZ = 0.1;
-            newCamera.parent = this;
             newCamera.rotationQuaternion = new Quaternion();
             newCamera.updateUpVectorFromRotation = true;
             this.rigCameras.push(newCamera);
@@ -65,31 +77,125 @@ export class WebXRCamera extends FreeCamera {
     /**
      * Updates the cameras position from the current pose information of the  XR session
      * @param xrSessionManager the session containing pose information
-     * @returns true if the camera has been updated, false if the session did not contain pose or frame data
      */
-    public updateFromXRSessionManager(xrSessionManager: WebXRSessionManager) {
-        // Ensure all frame data is available
-        if (!xrSessionManager.currentFrame || !xrSessionManager.currentFrame.getViewerPose) {
-            return false;
+    public update() {
+        if (!this._firstFrame) {
+            this._updateReferenceSpace();
+        }
+        this._updateFromXRSession();
+        super.update();
+    }
+
+    private _updateReferenceSpace(): boolean {
+        // were position & rotation updated OUTSIDE of the xr update loop
+        if (!this.position.equals(this._referencedPosition) || !this.rotationQuaternion.equals(this._referenceQuaternion)) {
+            this.position.subtractToRef(this._referencedPosition, this._referencedPosition);
+            this._referenceQuaternion.conjugateInPlace();
+            this._referenceQuaternion.multiplyToRef(this.rotationQuaternion, this._referenceQuaternion);
+            this._updateReferenceSpaceOffset(this._referencedPosition, this._referenceQuaternion.normalize());
+            return true;
+        }
+        return false;
+    }
+
+    private _updateReferenceSpaceOffset(positionOffset: Vector3, rotationOffset?: Quaternion, ignoreHeight: boolean = false) {
+        if (!this._xrSessionManager.referenceSpace || !this._xrSessionManager.currentFrame) {
+            return;
+        }
+        // Compute the origin offset based on player position/orientation.
+        this._xrInvPositionCache.copyFrom(positionOffset);
+        if (rotationOffset) {
+            this._xrInvQuaternionCache.copyFrom(rotationOffset);
+        } else {
+            this._xrInvQuaternionCache.copyFromFloats(0, 0, 0, 1);
+        }
+
+        // right handed system
+        if (!this._scene.useRightHandedSystem) {
+            this._xrInvPositionCache.z *= -1;
+            this._xrInvQuaternionCache.z *= -1;
+            this._xrInvQuaternionCache.w *= -1;
+        }
+
+        this._xrInvPositionCache.negateInPlace();
+        this._xrInvQuaternionCache.conjugateInPlace();
+        // transform point according to rotation with pivot
+        this._xrInvPositionCache.rotateByQuaternionToRef(this._xrInvQuaternionCache, this._xrInvPositionCache);
+        if (ignoreHeight) {
+            this._xrInvPositionCache.y = 0;
+        }
+        const transform = new XRRigidTransform(
+            { ...this._xrInvPositionCache },
+            { ...this._xrInvQuaternionCache });
+        // Update offset reference to use a new originOffset with the teleported
+        // player position and orientation.
+        // This new offset needs to be applied to the base ref space.
+        const referenceSpace = this._xrSessionManager.referenceSpace.getOffsetReferenceSpace(transform);
+
+        const pose = this._xrSessionManager.currentFrame && this._xrSessionManager.currentFrame.getViewerPose(referenceSpace);
+
+        if (pose) {
+            const pos = new Vector3();
+            pos.copyFrom(<any>(pose.transform.position));
+            if (!this._scene.useRightHandedSystem) {
+                pos.z *= -1;
+            }
+            this.position.subtractToRef(pos, pos);
+            if (!this._scene.useRightHandedSystem) {
+                pos.z *= -1;
+            }
+            pos.negateInPlace();
+
+            const transform2 = new XRRigidTransform(
+                { ...pos });
+            // Update offset reference to use a new originOffset with the teleported
+            // player position and orientation.
+            // This new offset needs to be applied to the base ref space.
+            this._xrSessionManager.referenceSpace = referenceSpace.getOffsetReferenceSpace(transform2);
         }
-        var pose = xrSessionManager.currentFrame.getViewerPose(xrSessionManager.referenceSpace);
+    }
+
+    private _updateFromXRSession() {
+
+        const pose = this._xrSessionManager.currentFrame && this._xrSessionManager.currentFrame.getViewerPose(this._xrSessionManager.referenceSpace);
+
         if (!pose) {
-            return false;
+            return;
         }
 
-        if (pose.transform && pose.emulatedPosition) {
-            this.position.copyFrom(<any>(pose.transform.position));
-            this.rotationQuaternion.copyFrom(<any>(pose.transform.orientation));
+        if (pose.transform) {
+            this._referencedPosition.copyFrom(<any>(pose.transform.position));
+            this._referenceQuaternion.copyFrom(<any>(pose.transform.orientation));
             if (!this._scene.useRightHandedSystem) {
-                this.position.z *= -1;
-                this.rotationQuaternion.z *= -1;
-                this.rotationQuaternion.w *= -1;
+                this._referencedPosition.z *= -1;
+                this._referenceQuaternion.z *= -1;
+                this._referenceQuaternion.w *= -1;
             }
-            this.computeWorldMatrix();
+
+            if (this._firstFrame) {
+                this._firstFrame = false;
+                // we have the XR reference, now use this to find the offset to get the camera to be
+                // in the right position
+
+                // set the height to correlate to the current height
+                this.position.y += this._referencedPosition.y;
+                // avoid using the head rotation on the first frame.
+                this._referenceQuaternion.copyFromFloats(0, 0, 0, 1);
+                // update the reference space so that the position will be correct
+
+                return this.update();
+
+            }
+
+            this.rotationQuaternion.copyFrom(this._referenceQuaternion);
+            this.position.copyFrom(this._referencedPosition);
         }
 
         // Update camera rigs
-        this._updateNumberOfRigCameras(pose.views.length);
+        if (this.rigCameras.length !== pose.views.length) {
+            this._updateNumberOfRigCameras(pose.views.length);
+        }
+
         pose.views.forEach((view: any, i: number) => {
             const currentRig = <TargetCamera>this.rigCameras[i];
             // update right and left, where applicable
@@ -101,7 +207,7 @@ export class WebXRCamera extends FreeCamera {
                 }
             }
             // Update view/projection matrix
-            if (view.transform.position && view.transform.orientation) {
+            if (view.transform.position) {
                 currentRig.position.copyFrom(view.transform.position);
                 currentRig.rotationQuaternion.copyFrom(view.transform.orientation);
                 if (!this._scene.useRightHandedSystem) {
@@ -122,10 +228,10 @@ export class WebXRCamera extends FreeCamera {
             }
 
             // Update viewport
-            if (xrSessionManager.session.renderState.baseLayer) {
-                var viewport = xrSessionManager.session.renderState.baseLayer.getViewport(view);
-                var width = xrSessionManager.session.renderState.baseLayer.framebufferWidth;
-                var height = xrSessionManager.session.renderState.baseLayer.framebufferHeight;
+            if (this._xrSessionManager.session.renderState.baseLayer) {
+                var viewport = this._xrSessionManager.session.renderState.baseLayer.getViewport(view);
+                var width = this._xrSessionManager.session.renderState.baseLayer.framebufferWidth;
+                var height = this._xrSessionManager.session.renderState.baseLayer.framebufferHeight;
                 currentRig.viewport.width = viewport.width / width;
                 currentRig.viewport.height = viewport.height / height;
                 currentRig.viewport.x = viewport.x / width;
@@ -136,8 +242,7 @@ export class WebXRCamera extends FreeCamera {
             }
 
             // Set cameras to render to the session's render target
-            currentRig.outputRenderTarget = xrSessionManager.getRenderTargetTextureForEye(view.eye);
+            currentRig.outputRenderTarget = this._xrSessionManager.getRenderTargetTextureForEye(view.eye);
         });
-        return true;
     }
 }

+ 1 - 1
src/Cameras/XR/webXRControllerModelLoader.ts

@@ -61,7 +61,7 @@ export class WebXRControllerModelLoader {
             controllerModel.hand = c.inputSource.handedness;
             controllerModel.isXR = true;
             controllerModel.initControllerMesh(c.getScene(), (m) => {
-                controllerModel.mesh!.parent = c.grip || input.baseExperience.container;
+                controllerModel.mesh!.parent = c.grip || null;
                 controllerModel.mesh!.rotationQuaternion = rotation;
                 controllerModel.mesh!.position = position;
                 m.isPickable = false;

+ 13 - 16
src/Cameras/XR/webXRControllerTeleportation.ts

@@ -111,31 +111,29 @@ export class WebXRControllerTeleportation {
                 }
 
                 if (c.inputSource.gamepad) {
-                    if (c.inputSource.gamepad.axes[3] !== undefined) {
+                    const yIndex = c.inputSource.gamepad.axes.length - 1;
+                    const xIndex = c.inputSource.gamepad.axes.length - 2;
+                    if (c.inputSource.gamepad.axes[yIndex] !== undefined) {
                         // Forward teleportation
-                        if (c.inputSource.gamepad.axes[3] < -0.7) {
+                        if (c.inputSource.gamepad.axes[yIndex] < -0.7) {
                             forwardReadyToTeleport = true;
                         } else {
                             if (forwardReadyToTeleport) {
                                 // Teleport the users feet to where they targeted
                                 this._tmpVector.copyFrom(teleportationTarget.position);
                                 this._tmpVector.y += input.baseExperience.camera.position.y;
-                                input.baseExperience.setPositionOfCameraUsingContainer(this._tmpVector);
+                                input.baseExperience.camera.position.copyFrom(this._tmpVector);
                             }
                             forwardReadyToTeleport = false;
                         }
 
                         // Backward teleportation
-                        if (c.inputSource.gamepad.axes[3] > 0.7) {
+                        if (c.inputSource.gamepad.axes[yIndex] > 0.7) {
                             backwardReadyToTeleport = true;
                         } else {
                             if (backwardReadyToTeleport) {
-                                // Cast a ray down from behind the user
-                                let camMat = input.baseExperience.camera.computeWorldMatrix();
-                                let q = new Quaternion();
-                                camMat.decompose(undefined, q, this._tmpRay.origin);
                                 this._tmpVector.set(0, 0, -1);
-                                this._tmpVector.rotateByQuaternionToRef(q, this._tmpVector);
+                                this._tmpVector.rotateByQuaternionToRef(input.baseExperience.camera.rotationQuaternion, this._tmpVector);
                                 this._tmpVector.y = 0;
                                 this._tmpVector.normalize();
                                 this._tmpVector.y = -1.5;
@@ -148,28 +146,27 @@ export class WebXRControllerTeleportation {
                                 if (pick && pick.pickedPoint) {
                                     // Teleport the users feet to where they targeted
                                     this._tmpVector.copyFrom(pick.pickedPoint);
-                                    this._tmpVector.y += input.baseExperience.camera.position.y;
-                                    input.baseExperience.setPositionOfCameraUsingContainer(this._tmpVector);
+                                    input.baseExperience.camera.position.addInPlace(this._tmpVector);
                                 }
                             }
                             backwardReadyToTeleport = false;
                         }
                     }
 
-                    if (c.inputSource.gamepad.axes[2] !== undefined) {
-                        if (c.inputSource.gamepad.axes[2] < -0.7) {
+                    if (c.inputSource.gamepad.axes[xIndex] !== undefined) {
+                        if (c.inputSource.gamepad.axes[xIndex] < -0.7) {
                             leftReadyToTeleport = true;
                         } else {
                             if (leftReadyToTeleport) {
-                                input.baseExperience.rotateCameraByQuaternionUsingContainer(Quaternion.FromEulerAngles(0, -Math.PI / 4, 0));
+                                input.baseExperience.camera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, -Math.PI / 4, 0));
                             }
                             leftReadyToTeleport = false;
                         }
-                        if (c.inputSource.gamepad.axes[2] > 0.7) {
+                        if (c.inputSource.gamepad.axes[xIndex] > 0.7) {
                             rightReadyToTeleport = true;
                         } else {
                             if (rightReadyToTeleport) {
-                                input.baseExperience.rotateCameraByQuaternionUsingContainer(Quaternion.FromEulerAngles(0, Math.PI / 4, 0));
+                                input.baseExperience.camera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, Math.PI / 4, 0));
                             }
                             rightReadyToTeleport = false;
                         }

+ 28 - 41
src/Cameras/XR/webXRExperienceHelper.ts

@@ -1,8 +1,7 @@
 import { Nullable } from "../../types";
 import { Observable } from "../../Misc/observable";
 import { IDisposable, Scene } from "../../scene";
-import { Quaternion, Vector3 } from "../../Maths/math.vector";
-import { AbstractMesh } from "../../Meshes/abstractMesh";
+import { Quaternion } from "../../Maths/math.vector";
 import { Camera } from "../../Cameras/camera";
 import { WebXRSessionManager } from "./webXRSessionManager";
 import { WebXRCamera } from "./webXRCamera";
@@ -15,10 +14,6 @@ import { WebXRFeaturesManager } from './webXRFeaturesManager';
  */
 export class WebXRExperienceHelper implements IDisposable {
     /**
-     * Container which stores the xr camera and controllers as children. This can be used to move the camera/user as the camera's position is updated by the xr device
-     */
-    public container: AbstractMesh;
-    /**
      * Camera used to render xr content
      */
     public camera: WebXRCamera;
@@ -33,13 +28,20 @@ export class WebXRExperienceHelper implements IDisposable {
         this.onStateChangedObservable.notifyObservers(this.state);
     }
 
-    private static _TmpVector = new Vector3();
-
     /**
      * Fires when the state of the experience helper has changed
      */
     public onStateChangedObservable = new Observable<WebXRState>();
 
+    /**
+     * Observers registered here will be triggered after the camera's initial transformation is set
+     * This can be used to set a different ground level or an extra rotation.
+     *
+     * Note that ground level is considered to be at 0. The height defined by the XR camera will be added
+     * to the position set after this observable is done executing.
+     */
+    public onInitialXRPoseSetObservable = new Observable<WebXRCamera>();
+
     /** Session manager used to keep track of xr session */
     public sessionManager: WebXRSessionManager;
 
@@ -71,11 +73,9 @@ export class WebXRExperienceHelper implements IDisposable {
      * @param scene The scene the helper should be created in
      */
     private constructor(private scene: Scene) {
-        this.camera = new WebXRCamera("", scene);
         this.sessionManager = new WebXRSessionManager(scene);
+        this.camera = new WebXRCamera("", scene, this.sessionManager);
         this.featuresManager = new WebXRFeaturesManager(this.sessionManager);
-        this.container = new AbstractMesh("WebXR Container", scene);
-        this.camera.parent = this.container;
 
         scene.onDisposeObservable.add(() => {
             this.exitXRAsync();
@@ -121,11 +121,8 @@ export class WebXRExperienceHelper implements IDisposable {
 
             // Overwrite current scene settings
             this.scene.autoClear = false;
-            this.scene.activeCamera = this.camera;
 
-            this.sessionManager.onXRFrameObservable.add(() => {
-                this.camera.updateFromXRSessionManager(this.sessionManager);
-            });
+            this._nonXRToXRCamera();
 
             this.sessionManager.onXRSessionEnded.addOnce(() => {
                 // Reset camera rigs output render target to ensure sessions render target is not drawn after it ends
@@ -136,6 +133,11 @@ export class WebXRExperienceHelper implements IDisposable {
                 // Restore scene settings
                 this.scene.autoClear = this._originalSceneAutoClear;
                 this.scene.activeCamera = this._nonVRCamera;
+                if ((<any>this._nonVRCamera).setPosition) {
+                    (<any>this._nonVRCamera).setPosition(this.camera.position);
+                } else {
+                    this._nonVRCamera!.position.copyFrom(this.camera.position);
+                }
 
                 this._setState(WebXRState.NOT_IN_XR);
             });
@@ -143,8 +145,6 @@ export class WebXRExperienceHelper implements IDisposable {
             // Wait until the first frame arrives before setting state to in xr
             this.sessionManager.onXRFrameObservable.addOnce(() => {
                 this._setState(WebXRState.IN_XR);
-
-                this.setPositionOfCameraUsingContainer(new Vector3(this._nonVRCamera!.position.x, this.camera.position.y, this._nonVRCamera!.position.z));
             });
 
             return this.sessionManager;
@@ -156,35 +156,22 @@ export class WebXRExperienceHelper implements IDisposable {
     }
 
     /**
-     * Updates the global position of the camera by moving the camera's container
-     * This should be used instead of modifying the camera's position as it will be overwritten by an xrSessions's update frame
-     * @param position The desired global position of the camera
-     */
-    public setPositionOfCameraUsingContainer(position: Vector3) {
-        this.camera.globalPosition.subtractToRef(position, WebXRExperienceHelper._TmpVector);
-        this.container.position.subtractInPlace(WebXRExperienceHelper._TmpVector);
-    }
-
-    /**
-     * Rotates the xr camera by rotating the camera's container around the camera's position
-     * This should be used instead of modifying the camera's rotation as it will be overwritten by an xrSessions's update frame
-     * @param rotation the desired quaternion rotation to apply to the camera
-     */
-    public rotateCameraByQuaternionUsingContainer(rotation: Quaternion) {
-        if (!this.container.rotationQuaternion) {
-            this.container.rotationQuaternion = Quaternion.FromEulerVector(this.container.rotation);
-        }
-        this.container.rotationQuaternion.multiplyInPlace(rotation);
-        this.container.position.rotateByQuaternionAroundPointToRef(rotation, this.camera.globalPosition, this.container.position);
-    }
-
-    /**
      * Disposes of the experience helper
      */
     public dispose() {
         this.camera.dispose();
-        this.container.dispose();
         this.onStateChangedObservable.clear();
         this.sessionManager.dispose();
     }
+
+    private _nonXRToXRCamera() {
+        this.scene.activeCamera = this.camera;
+        const mat = this._nonVRCamera!.computeWorldMatrix();
+        mat.decompose(undefined, this.camera.rotationQuaternion, this.camera.position);
+        // set the ground level
+        this.camera.position.y = 0;
+        Quaternion.FromEulerAnglesToRef(0, this.camera.rotationQuaternion.toEulerAngles().y, 0, this.camera.rotationQuaternion);
+
+        this.onInitialXRPoseSetObservable.notifyObservers(this.camera);
+    }
 }

+ 1 - 1
src/Cameras/XR/webXRInput.ts

@@ -69,7 +69,7 @@ export class WebXRInput implements IDisposable {
         let sources = this.controllers.map((c) => {return c.inputSource; });
         for (let input of addInputs) {
             if (sources.indexOf(input) === -1) {
-                let controller = new WebXRController(this.baseExperience.camera._scene, input, this.baseExperience.container);
+                let controller = new WebXRController(this.baseExperience.camera._scene, input);
                 this.controllers.push(controller);
                 this.onControllerAddedObservable.notifyObservers(controller);
             }

+ 32 - 4
src/Cameras/XR/webXRSessionManager.ts

@@ -47,11 +47,30 @@ export class WebXRSessionManager implements IDisposable {
     public session: XRSession;
 
     /**
-     * Type of reference space used when creating the session
+     * The viewer (head position) reference space. This can be used to get the XR world coordinates
+     * or get the offset the player is currently at.
+     */
+    public viewerReferenceSpace: XRReferenceSpace;
+
+    /**
+     * The current reference space used in this session. This reference space can constantly change!
+     * It is mainly used to offset the camera's position.
      */
     public referenceSpace: XRReferenceSpace;
 
     /**
+     * The base reference space from which the session started. good if you want to reset your
+     * reference space
+     */
+    public baseReferenceSpace: XRReferenceSpace;
+
+    /**
+     * Used just in case of a failure to initialize an immersive session.
+     * The viewer reference space is compensated using this height, creating a kind of "viewer-floor" reference space
+     */
+    public defaultHeightCompensation = 1.7;
+
+    /**
      * Current XR frame
      */
     public currentFrame: Nullable<XRFrame>;
@@ -128,18 +147,26 @@ export class WebXRSessionManager implements IDisposable {
      */
     public setReferenceSpaceAsync(referenceSpace: XRReferenceSpaceType) {
         return this.session.requestReferenceSpace(referenceSpace).then((referenceSpace: XRReferenceSpace) => {
-            this.referenceSpace = referenceSpace;
+            return referenceSpace;
         }, (rejectionReason) => {
             Logger.Error("XR.requestReferenceSpace failed for the following reason: ");
             Logger.Error(rejectionReason);
             Logger.Log("Defaulting to universally-supported \"viewer\" reference space type.");
 
             return this.session.requestReferenceSpace("viewer").then((referenceSpace: XRReferenceSpace) => {
-                this.referenceSpace = referenceSpace;
+                const heightCompensation = new XRRigidTransform({ x: 0, y: -this.defaultHeightCompensation, z: 0 });
+                return referenceSpace.getOffsetReferenceSpace(heightCompensation);
             }, (rejectionReason) => {
                 Logger.Error(rejectionReason);
                 throw "XR initialization failed: required \"viewer\" reference space type not supported.";
             });
+        }).then((referenceSpace) => {
+            // initialize the base and offset (currently the same)
+            this.referenceSpace = this.baseReferenceSpace = referenceSpace;
+
+            this.session.requestReferenceSpace("viewer").then((referenceSpace: XRReferenceSpace) => {
+                this.viewerReferenceSpace = referenceSpace;
+            });
         });
     }
 
@@ -172,8 +199,9 @@ export class WebXRSessionManager implements IDisposable {
                 this.currentTimestamp = timestamp;
                 if (xrFrame) {
                     this.onXRFrameObservable.notifyObservers(xrFrame);
+                    // only run the render loop if a frame exists
+                    this.scene.getEngine()._renderLoop();
                 }
-                this.scene.getEngine()._renderLoop();
             }
         };
 

+ 2 - 2
src/LibDeclarations/webxr.d.ts

@@ -124,7 +124,7 @@ interface XRWebGLLayer {
 }
 
 declare class XRRigidTransform {
-    constructor(matrix: Float32Array);
+    constructor(matrix: Float32Array | DOMPointInit, direction?: DOMPointInit);
     position: DOMPointReadOnly;
     orientation: DOMPointReadOnly;
     matrix: Float32Array;
@@ -150,7 +150,7 @@ interface XRInputSourceEvent extends Event {
 
 // Experimental(er) features
 declare class XRRay {
-    constructor(transformOrOrigin: XRRigidTransform | DOMPointReadOnly, direction?: DOMPointReadOnly);
+    constructor(transformOrOrigin: XRRigidTransform | DOMPointInit, direction?: DOMPointInit);
     origin: DOMPointReadOnly;
     direction: DOMPointReadOnly;
     matrix: Float32Array;

+ 25 - 0
src/Maths/math.vector.ts

@@ -840,6 +840,17 @@ export class Vector3 {
     }
 
     /**
+     * Negate this vector in place
+     * @returns this
+     */
+    public negateInPlace(): Vector3 {
+        this.x *= -1;
+        this.y *= -1;
+        this.z *= -1;
+        return this;
+    }
+
+    /**
      * Multiplies the Vector3 coordinates by the float "scale"
      * @param scale defines the multiplier factor
      * @returns the current updated Vector3
@@ -2655,6 +2666,20 @@ export class Quaternion {
     }
 
     /**
+     * Gets a boolean if two quaternions are equals (using an epsilon value)
+     * @param otherQuaternion defines the other quaternion
+     * @param epsilon defines the minimal distance to consider equality
+     * @returns true if the given quaternion coordinates are close to the current ones by a distance of epsilon.
+     */
+    public equalsWithEpsilon(otherQuaternion: DeepImmutable<Quaternion>, epsilon: number = Epsilon): boolean {
+        return otherQuaternion
+            && Scalar.WithinEpsilon(this.x, otherQuaternion.x, epsilon)
+            && Scalar.WithinEpsilon(this.y, otherQuaternion.y, epsilon)
+            && Scalar.WithinEpsilon(this.z, otherQuaternion.z, epsilon)
+            && Scalar.WithinEpsilon(this.w, otherQuaternion.w, epsilon);
+    }
+
+    /**
      * Clone the current quaternion
      * @returns a new quaternion copied from the current one
      */

+ 1 - 7
tests/validation/config.json

@@ -81,16 +81,10 @@
         },
         {
             "title": "Camera rig",
-            "playgroundId": "#ATL1CS#9",
+            "playgroundId": "#ATL1CS#11",
             "referenceImage": "cameraRig.png"
         },
         {
-            "title": "XR camera container rotation",
-            "playgroundId": "#JV98QW#5",
-            "referenceImage": "xrCameraContainerRotation.png",
-            "excludeFromAutomaticTesting": true
-        },
-        {
             "title": "Sliders",
             "playgroundId": "#HATGQZ#3",
             "referenceImage": "Sliders.png",