Browse Source

[XR] Optional (xr)camera gaze mode (#8605)

* get forward ray to ref

* enable gaze mode

* nuwatz
Raanan Weber 5 years ago
parent
commit
a4e3e07698

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

@@ -157,6 +157,7 @@
 - Canvas is being resized when entering XR ([RaananW](https://github.com/RaananW))
 - All camera view matrices are now calculated by Babylon to support left and right handed systems ([RaananW](https://github.com/RaananW))
 - WebXR Features Manager now has the ability to check if a feature can be enabled, and set native features optional or required ([RaananW](https://github.com/RaananW))
+- Optional camera gaze mode added to the pointer selection feature ([RaananW](https://github.com/RaananW))
 
 ### Collisions
 

+ 12 - 0
src/Cameras/camera.ts

@@ -853,6 +853,18 @@ export class Camera extends Node {
     }
 
     /**
+     * Gets a ray in the forward direction from the camera.
+     * @param refRay the ray to (re)use when setting the values
+     * @param length Defines the length of the ray to create
+     * @param transform Defines the transform to apply to the ray, by default the world matrx is used to create a workd space ray
+     * @param origin Defines the start point of the ray which defaults to the camera position
+     * @returns the forward ray
+     */
+    public getForwardRayToRef(refRay: Ray, length = 100, transform?: Matrix, origin?: Vector3): Ray {
+        throw _DevTools.WarnImport("Ray");
+    }
+
+    /**
      * Releases resources associated with this node.
      * @param doNotRecurse Set to true to not recurse into each children (recurse into each children by default)
      * @param disposeMaterialAndTextures Set to true to also dispose referenced materials and textures (false by default)

+ 152 - 138
src/Culling/ray.ts

@@ -6,9 +6,9 @@ import { PickingInfo } from "../Collisions/pickingInfo";
 import { IntersectionInfo } from "../Collisions/intersectionInfo";
 import { BoundingBox } from "./boundingBox";
 import { BoundingSphere } from "./boundingSphere";
-import { Scene } from '../scene';
-import { Camera } from '../Cameras/camera';
-import { Plane } from '../Maths/math.plane';
+import { Scene } from "../scene";
+import { Camera } from "../Cameras/camera";
+import { Plane } from "../Maths/math.plane";
 /**
  * Class representing a ray with position and direction
  */
@@ -28,8 +28,8 @@ export class Ray {
         /** direction */
         public direction: Vector3,
         /** length of the ray */
-        public length: number = Number.MAX_VALUE) {
-    }
+        public length: number = Number.MAX_VALUE
+    ) {}
 
     // Methods
     /**
@@ -53,8 +53,7 @@ export class Ray {
             if (this.origin.x < newMinimum.x || this.origin.x > newMaximum.x) {
                 return false;
             }
-        }
-        else {
+        } else {
             inv = 1.0 / this.direction.x;
             min = (newMinimum.x - this.origin.x) * inv;
             max = (newMaximum.x - this.origin.x) * inv;
@@ -80,8 +79,7 @@ export class Ray {
             if (this.origin.y < newMinimum.y || this.origin.y > newMaximum.y) {
                 return false;
             }
-        }
-        else {
+        } else {
             inv = 1.0 / this.direction.y;
             min = (newMinimum.y - this.origin.y) * inv;
             max = (newMaximum.y - this.origin.y) * inv;
@@ -108,8 +106,7 @@ export class Ray {
             if (this.origin.z < newMinimum.z || this.origin.z > newMaximum.z) {
                 return false;
             }
-        }
-        else {
+        } else {
             inv = 1.0 / this.direction.z;
             min = (newMinimum.z - this.origin.z) * inv;
             max = (newMaximum.z - this.origin.z) * inv;
@@ -155,7 +152,7 @@ export class Ray {
         var x = sphere.center.x - this.origin.x;
         var y = sphere.center.y - this.origin.y;
         var z = sphere.center.z - this.origin.z;
-        var pyth = (x * x) + (y * y) + (z * z);
+        var pyth = x * x + y * y + z * z;
         const radius = sphere.radius + intersectionTreshold;
         var rr = radius * radius;
 
@@ -163,12 +160,12 @@ export class Ray {
             return true;
         }
 
-        var dot = (x * this.direction.x) + (y * this.direction.y) + (z * this.direction.z);
+        var dot = x * this.direction.x + y * this.direction.y + z * this.direction.z;
         if (dot < 0.0) {
             return false;
         }
 
-        var temp = pyth - (dot * dot);
+        var temp = pyth - dot * dot;
 
         return temp <= rr;
     }
@@ -231,14 +228,13 @@ export class Ray {
     public intersectsPlane(plane: DeepImmutable<Plane>): Nullable<number> {
         var distance: number;
         var result1 = Vector3.Dot(plane.normal, this.direction);
-        if (Math.abs(result1) < 9.99999997475243E-07) {
+        if (Math.abs(result1) < 9.99999997475243e-7) {
             return null;
-        }
-        else {
+        } else {
             var result2 = Vector3.Dot(plane.normal, this.origin);
             distance = (-plane.d - result2) / result1;
             if (distance < 0.0) {
-                if (distance < -9.99999997475243E-07) {
+                if (distance < -9.99999997475243e-7) {
                     return null;
                 } else {
                     return 0;
@@ -256,24 +252,24 @@ export class Ray {
      */
     public intersectsAxis(axis: string, offset: number = 0): Nullable<Vector3> {
         switch (axis) {
-            case 'y':
+            case "y":
                 var t = (this.origin.y - offset) / this.direction.y;
                 if (t > 0) {
                     return null;
                 }
-                return new Vector3(this.origin.x + (this.direction.x * -t), offset, this.origin.z + (this.direction.z * -t));
-            case 'x':
+                return new Vector3(this.origin.x + this.direction.x * -t, offset, this.origin.z + this.direction.z * -t);
+            case "x":
                 var t = (this.origin.x - offset) / this.direction.x;
                 if (t > 0) {
                     return null;
                 }
-                return new Vector3(offset, this.origin.y + (this.direction.y * -t), this.origin.z + (this.direction.z * -t));
-            case 'z':
+                return new Vector3(offset, this.origin.y + this.direction.y * -t, this.origin.z + this.direction.z * -t);
+            case "z":
                 var t = (this.origin.z - offset) / this.direction.z;
                 if (t > 0) {
                     return null;
                 }
-                return new Vector3(this.origin.x + (this.direction.x * -t), this.origin.y + (this.direction.y * -t), offset);
+                return new Vector3(this.origin.x + this.direction.x * -t, this.origin.y + this.direction.y * -t, offset);
             default:
                 return null;
         }
@@ -286,7 +282,6 @@ export class Ray {
      * @returns picking info of the intersecton
      */
     public intersectsMesh(mesh: DeepImmutable<AbstractMesh>, fastCheck?: boolean): PickingInfo {
-
         var tm = TmpVectors.Matrix[0];
 
         mesh.getWorldMatrix().invertToRef(tm);
@@ -298,7 +293,6 @@ export class Ray {
         }
 
         return mesh.intersects(this._tmpRay, fastCheck);
-
     }
 
     /**
@@ -309,7 +303,6 @@ export class Ray {
      * @returns Array of picking infos
      */
     public intersectsMeshes(meshes: Array<DeepImmutable<AbstractMesh>>, fastCheck?: boolean, results?: Array<PickingInfo>): Array<PickingInfo> {
-
         if (results) {
             results.length = 0;
         } else {
@@ -327,11 +320,9 @@ export class Ray {
         results.sort(this._comparePickingInfo);
 
         return results;
-
     }
 
     private _comparePickingInfo(pickingInfoA: DeepImmutable<PickingInfo>, pickingInfoB: DeepImmutable<PickingInfo>): number {
-
         if (pickingInfoA.distance < pickingInfoB.distance) {
             return -1;
         } else if (pickingInfoA.distance > pickingInfoB.distance) {
@@ -339,7 +330,6 @@ export class Ray {
         } else {
             return 0;
         }
-
     }
 
     private static smallnum = 0.00000001;
@@ -366,63 +356,71 @@ export class Ray {
 
         sega.subtractToRef(o, w);
 
-        var a = Vector3.Dot(u, u);                  // always >= 0
+        var a = Vector3.Dot(u, u); // always >= 0
         var b = Vector3.Dot(u, v);
-        var c = Vector3.Dot(v, v);                  // always >= 0
+        var c = Vector3.Dot(v, v); // always >= 0
         var d = Vector3.Dot(u, w);
         var e = Vector3.Dot(v, w);
-        var D = a * c - b * b;                      // always >= 0
-        var sc: number, sN: number, sD = D;         // sc = sN / sD, default sD = D >= 0
-        var tc: number, tN: number, tD = D;         // tc = tN / tD, default tD = D >= 0
+        var D = a * c - b * b; // always >= 0
+        var sc: number,
+            sN: number,
+            sD = D; // sc = sN / sD, default sD = D >= 0
+        var tc: number,
+            tN: number,
+            tD = D; // tc = tN / tD, default tD = D >= 0
 
         // compute the line parameters of the two closest points
-        if (D < Ray.smallnum) {                     // the lines are almost parallel
-            sN = 0.0;                               // force using point P0 on segment S1
-            sD = 1.0;                               // to prevent possible division by 0.0 later
+        if (D < Ray.smallnum) {
+            // the lines are almost parallel
+            sN = 0.0; // force using point P0 on segment S1
+            sD = 1.0; // to prevent possible division by 0.0 later
             tN = e;
             tD = c;
-        }
-        else {                                      // get the closest points on the infinite lines
-            sN = (b * e - c * d);
-            tN = (a * e - b * d);
-            if (sN < 0.0) {                         // sc < 0 => the s=0 edge is visible
+        } else {
+            // get the closest points on the infinite lines
+            sN = b * e - c * d;
+            tN = a * e - b * d;
+            if (sN < 0.0) {
+                // sc < 0 => the s=0 edge is visible
                 sN = 0.0;
                 tN = e;
                 tD = c;
-            } else if (sN > sD) {                   // sc > 1 => the s=1 edge is visible
+            } else if (sN > sD) {
+                // sc > 1 => the s=1 edge is visible
                 sN = sD;
                 tN = e + b;
                 tD = c;
             }
         }
 
-        if (tN < 0.0) {                             // tc < 0 => the t=0 edge is visible
+        if (tN < 0.0) {
+            // tc < 0 => the t=0 edge is visible
             tN = 0.0;
             // recompute sc for this edge
             if (-d < 0.0) {
                 sN = 0.0;
             } else if (-d > a) {
                 sN = sD;
-            }
-            else {
+            } else {
                 sN = -d;
                 sD = a;
             }
-        } else if (tN > tD) {                       // tc > 1 => the t=1 edge is visible
+        } else if (tN > tD) {
+            // tc > 1 => the t=1 edge is visible
             tN = tD;
             // recompute sc for this edge
-            if ((-d + b) < 0.0) {
+            if (-d + b < 0.0) {
                 sN = 0;
-            } else if ((-d + b) > a) {
+            } else if (-d + b > a) {
                 sN = sD;
             } else {
-                sN = (-d + b);
+                sN = -d + b;
                 sD = a;
             }
         }
         // finally do the division to get sc and tc
-        sc = (Math.abs(sN) < Ray.smallnum ? 0.0 : sN / sD);
-        tc = (Math.abs(tN) < Ray.smallnum ? 0.0 : tN / tD);
+        sc = Math.abs(sN) < Ray.smallnum ? 0.0 : sN / sD;
+        tc = Math.abs(tN) < Ray.smallnum ? 0.0 : tN / tD;
 
         // get the difference of the two closest points
         const qtc = TmpVectors.Vector3[4];
@@ -433,7 +431,7 @@ export class Ray {
         const dP = TmpVectors.Vector3[6];
         qsc.subtractToRef(qtc, dP); // = S1(sc) - S2(tc)
 
-        var isIntersected = (tc > 0) && (tc <= this.length) && (dP.lengthSquared() < (threshold * threshold));   // return intersection result
+        var isIntersected = tc > 0 && tc <= this.length && dP.lengthSquared() < threshold * threshold; // return intersection result
 
         if (isIntersected) {
             return qsc.length();
@@ -484,16 +482,16 @@ export class Ray {
     }
 
     /**
-    * Function will create a new transformed ray starting from origin and ending at the end point. Ray's length will be set, and ray will be
-    * transformed to the given world matrix.
-    * @param origin The origin point
-    * @param end The end point
-    * @param world a matrix to transform the ray to. Default is the identity matrix.
-    * @returns the new ray
-    */
+     * Function will create a new transformed ray starting from origin and ending at the end point. Ray's length will be set, and ray will be
+     * transformed to the given world matrix.
+     * @param origin The origin point
+     * @param end The end point
+     * @param world a matrix to transform the ray to. Default is the identity matrix.
+     * @returns the new ray
+     */
     public static CreateNewFromTo(origin: Vector3, end: Vector3, world: DeepImmutable<Matrix> = Matrix.IdentityReadOnly): Ray {
         var direction = end.subtract(origin);
-        var length = Math.sqrt((direction.x * direction.x) + (direction.y * direction.y) + (direction.z * direction.z));
+        var length = Math.sqrt(direction.x * direction.x + direction.y * direction.y + direction.z * direction.z);
         direction.normalize();
 
         return Ray.Transform(new Ray(origin, direction, length), world);
@@ -536,23 +534,23 @@ export class Ray {
     }
 
     /**
-      * Unproject a ray from screen space to object space
-      * @param sourceX defines the screen space x coordinate to use
-      * @param sourceY defines the screen space y coordinate to use
-      * @param viewportWidth defines the current width of the viewport
-      * @param viewportHeight defines the current height of the viewport
-      * @param world defines the world matrix to use (can be set to Identity to go to world space)
-      * @param view defines the view matrix to use
-      * @param projection defines the projection matrix to use
-      */
+     * Unproject a ray from screen space to object space
+     * @param sourceX defines the screen space x coordinate to use
+     * @param sourceY defines the screen space y coordinate to use
+     * @param viewportWidth defines the current width of the viewport
+     * @param viewportHeight defines the current height of the viewport
+     * @param world defines the world matrix to use (can be set to Identity to go to world space)
+     * @param view defines the view matrix to use
+     * @param projection defines the projection matrix to use
+     */
     public unprojectRayToRef(sourceX: float, sourceY: float, viewportWidth: number, viewportHeight: number, world: DeepImmutable<Matrix>, view: DeepImmutable<Matrix>, projection: DeepImmutable<Matrix>): void {
         var matrix = TmpVectors.Matrix[0];
         world.multiplyToRef(view, matrix);
         matrix.multiplyToRef(projection, matrix);
         matrix.invert();
         var nearScreenSource = TmpVectors.Vector3[0];
-        nearScreenSource.x = sourceX / viewportWidth * 2 - 1;
-        nearScreenSource.y = -(sourceY / viewportHeight * 2 - 1);
+        nearScreenSource.x = (sourceX / viewportWidth) * 2 - 1;
+        nearScreenSource.y = -((sourceY / viewportHeight) * 2 - 1);
         nearScreenSource.z = -1.0;
         var farScreenSource = TmpVectors.Vector3[1].copyFromFloats(nearScreenSource.x, nearScreenSource.y, 1.0);
         const nearVec3 = TmpVectors.Vector3[2];
@@ -588,11 +586,10 @@ declare module "../scene" {
 
         /** @hidden */
         _internalMultiPick(rayFunction: (world: Matrix) => Ray, predicate?: (mesh: AbstractMesh) => boolean, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo[]>;
-
     }
 }
 
-Scene.prototype.createPickingRay = function(x: number, y: number, world: Matrix, camera: Nullable<Camera>, cameraViewSpace = false): Ray {
+Scene.prototype.createPickingRay = function (x: number, y: number, world: Matrix, camera: Nullable<Camera>, cameraViewSpace = false): Ray {
     let result = Ray.Zero();
 
     this.createPickingRayToRef(x, y, world, result, camera, cameraViewSpace);
@@ -600,7 +597,7 @@ Scene.prototype.createPickingRay = function(x: number, y: number, world: Matrix,
     return result;
 };
 
-Scene.prototype.createPickingRayToRef = function(x: number, y: number, world: Matrix, result: Ray, camera: Nullable<Camera>, cameraViewSpace = false): Scene {
+Scene.prototype.createPickingRayToRef = function (x: number, y: number, world: Matrix, result: Ray, camera: Nullable<Camera>, cameraViewSpace = false): Scene {
     var engine = this.getEngine();
 
     if (!camera) {
@@ -622,7 +619,7 @@ Scene.prototype.createPickingRayToRef = function(x: number, y: number, world: Ma
     return this;
 };
 
-Scene.prototype.createPickingRayInCameraSpace = function(x: number, y: number, camera?: Camera): Ray {
+Scene.prototype.createPickingRayInCameraSpace = function (x: number, y: number, camera?: Camera): Ray {
     let result = Ray.Zero();
 
     this.createPickingRayInCameraSpaceToRef(x, y, result, camera);
@@ -630,7 +627,7 @@ Scene.prototype.createPickingRayInCameraSpace = function(x: number, y: number, c
     return result;
 };
 
-Scene.prototype.createPickingRayInCameraSpaceToRef = function(x: number, y: number, result: Ray, camera?: Camera): Scene {
+Scene.prototype.createPickingRayInCameraSpaceToRef = function (x: number, y: number, result: Ray, camera?: Camera): Scene {
     if (!PickingInfo) {
         return this;
     }
@@ -656,10 +653,7 @@ Scene.prototype.createPickingRayInCameraSpaceToRef = function(x: number, y: numb
     return this;
 };
 
-Scene.prototype._internalPick = function(rayFunction: (world: Matrix) => Ray, predicate?: (mesh: AbstractMesh) => boolean,
-    fastCheck?: boolean,
-    onlyBoundingInfo?: boolean,
-    trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo> {
+Scene.prototype._internalPick = function (rayFunction: (world: Matrix) => Ray, predicate?: (mesh: AbstractMesh) => boolean, fastCheck?: boolean, onlyBoundingInfo?: boolean, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo> {
     if (!PickingInfo) {
         return null;
     }
@@ -699,9 +693,7 @@ Scene.prototype._internalPick = function(rayFunction: (world: Matrix) => Ray, pr
     return pickingInfo || new PickingInfo();
 };
 
-Scene.prototype._internalMultiPick = function(rayFunction: (world: Matrix) => Ray,
-    predicate?: (mesh: AbstractMesh) => boolean,
-    trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo[]> {
+Scene.prototype._internalMultiPick = function (rayFunction: (world: Matrix) => Ray, predicate?: (mesh: AbstractMesh) => boolean, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo[]> {
     if (!PickingInfo) {
         return null;
     }
@@ -732,100 +724,122 @@ Scene.prototype._internalMultiPick = function(rayFunction: (world: Matrix) => Ra
     return pickingInfos;
 };
 
-Scene.prototype.pickWithBoundingInfo = function(x: number, y: number, predicate?: (mesh: AbstractMesh) => boolean,
-    fastCheck?: boolean, camera?: Nullable<Camera>): Nullable<PickingInfo> {
+Scene.prototype.pickWithBoundingInfo = function (x: number, y: number, predicate?: (mesh: AbstractMesh) => boolean, fastCheck?: boolean, camera?: Nullable<Camera>): Nullable<PickingInfo> {
     if (!PickingInfo) {
         return null;
     }
-    var result = this._internalPick((world) => {
-        if (!this._tempPickingRay) {
-            this._tempPickingRay = Ray.Zero();
-        }
+    var result = this._internalPick(
+        (world) => {
+            if (!this._tempPickingRay) {
+                this._tempPickingRay = Ray.Zero();
+            }
 
-        this.createPickingRayToRef(x, y, world, this._tempPickingRay, camera || null);
-        return this._tempPickingRay;
-    }, predicate, fastCheck, true);
+            this.createPickingRayToRef(x, y, world, this._tempPickingRay, camera || null);
+            return this._tempPickingRay;
+        },
+        predicate,
+        fastCheck,
+        true
+    );
     if (result) {
         result.ray = this.createPickingRay(x, y, Matrix.Identity(), camera || null);
     }
     return result;
 };
 
-Scene.prototype.pick = function(x: number, y: number, predicate?: (mesh: AbstractMesh) => boolean,
-    fastCheck?: boolean, camera?: Nullable<Camera>,
-    trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo> {
+Scene.prototype.pick = function (x: number, y: number, predicate?: (mesh: AbstractMesh) => boolean, fastCheck?: boolean, camera?: Nullable<Camera>, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo> {
     if (!PickingInfo) {
         return null;
     }
-    var result = this._internalPick((world) => {
-        if (!this._tempPickingRay) {
-            this._tempPickingRay = Ray.Zero();
-        }
+    var result = this._internalPick(
+        (world) => {
+            if (!this._tempPickingRay) {
+                this._tempPickingRay = Ray.Zero();
+            }
 
-        this.createPickingRayToRef(x, y, world, this._tempPickingRay, camera || null);
-        return this._tempPickingRay;
-    }, predicate, fastCheck, false, trianglePredicate);
+            this.createPickingRayToRef(x, y, world, this._tempPickingRay, camera || null);
+            return this._tempPickingRay;
+        },
+        predicate,
+        fastCheck,
+        false,
+        trianglePredicate
+    );
     if (result) {
         result.ray = this.createPickingRay(x, y, Matrix.Identity(), camera || null);
     }
     return result;
 };
 
-Scene.prototype.pickWithRay = function(ray: Ray, predicate?: (mesh: AbstractMesh) => boolean,
-    fastCheck?: boolean, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo> {
-    var result = this._internalPick((world) => {
-        if (!this._pickWithRayInverseMatrix) {
-            this._pickWithRayInverseMatrix = Matrix.Identity();
-        }
-        world.invertToRef(this._pickWithRayInverseMatrix);
+Scene.prototype.pickWithRay = function (ray: Ray, predicate?: (mesh: AbstractMesh) => boolean, fastCheck?: boolean, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo> {
+    var result = this._internalPick(
+        (world) => {
+            if (!this._pickWithRayInverseMatrix) {
+                this._pickWithRayInverseMatrix = Matrix.Identity();
+            }
+            world.invertToRef(this._pickWithRayInverseMatrix);
 
-        if (!this._cachedRayForTransform) {
-            this._cachedRayForTransform = Ray.Zero();
-        }
+            if (!this._cachedRayForTransform) {
+                this._cachedRayForTransform = Ray.Zero();
+            }
 
-        Ray.TransformToRef(ray, this._pickWithRayInverseMatrix, this._cachedRayForTransform);
-        return this._cachedRayForTransform;
-    }, predicate, fastCheck, false, trianglePredicate);
+            Ray.TransformToRef(ray, this._pickWithRayInverseMatrix, this._cachedRayForTransform);
+            return this._cachedRayForTransform;
+        },
+        predicate,
+        fastCheck,
+        false,
+        trianglePredicate
+    );
     if (result) {
         result.ray = ray;
     }
     return result;
 };
 
-Scene.prototype.multiPick = function(x: number, y: number,
-    predicate?: (mesh: AbstractMesh) => boolean, camera?: Camera, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo[]> {
+Scene.prototype.multiPick = function (x: number, y: number, predicate?: (mesh: AbstractMesh) => boolean, camera?: Camera, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo[]> {
     return this._internalMultiPick((world) => this.createPickingRay(x, y, world, camera || null), predicate, trianglePredicate);
 };
 
-Scene.prototype.multiPickWithRay = function(ray: Ray,
-    predicate: (mesh: AbstractMesh) => boolean, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo[]> {
-    return this._internalMultiPick((world) => {
-        if (!this._pickWithRayInverseMatrix) {
-            this._pickWithRayInverseMatrix = Matrix.Identity();
-        }
-        world.invertToRef(this._pickWithRayInverseMatrix);
+Scene.prototype.multiPickWithRay = function (ray: Ray, predicate: (mesh: AbstractMesh) => boolean, trianglePredicate?: TrianglePickingPredicate): Nullable<PickingInfo[]> {
+    return this._internalMultiPick(
+        (world) => {
+            if (!this._pickWithRayInverseMatrix) {
+                this._pickWithRayInverseMatrix = Matrix.Identity();
+            }
+            world.invertToRef(this._pickWithRayInverseMatrix);
 
-        if (!this._cachedRayForTransform) {
-            this._cachedRayForTransform = Ray.Zero();
-        }
+            if (!this._cachedRayForTransform) {
+                this._cachedRayForTransform = Ray.Zero();
+            }
+
+            Ray.TransformToRef(ray, this._pickWithRayInverseMatrix, this._cachedRayForTransform);
+            return this._cachedRayForTransform;
+        },
+        predicate,
+        trianglePredicate
+    );
+};
 
-        Ray.TransformToRef(ray, this._pickWithRayInverseMatrix, this._cachedRayForTransform);
-        return this._cachedRayForTransform;
-    }, predicate, trianglePredicate);
+Camera.prototype.getForwardRay = function (length = 100, transform?: Matrix, origin?: Vector3): Ray {
+    return this.getForwardRayToRef(new Ray(Vector3.Zero(), Vector3.Zero(), length), length, transform, origin);
 };
 
-Camera.prototype.getForwardRay = function(length = 100, transform?: Matrix, origin?: Vector3): Ray {
+Camera.prototype.getForwardRayToRef = function (refRay: Ray, length = 100, transform?: Matrix, origin?: Vector3): Ray {
     if (!transform) {
         transform = this.getWorldMatrix();
     }
+    refRay.length = length;
 
     if (!origin) {
-        origin = this.position;
+        refRay.origin.copyFrom(this.position);
+    } else {
+        refRay.origin.copyFrom(origin);
     }
-    var forward = this._scene.useRightHandedSystem ? new Vector3(0, 0, -1) : new Vector3(0, 0, 1);
-    var forwardWorld = Vector3.TransformNormal(forward, transform);
+    TmpVectors.Vector3[2].set(0, 0, this._scene.useRightHandedSystem ? -1 : 1);
+    Vector3.TransformNormalToRef(TmpVectors.Vector3[2], transform, TmpVectors.Vector3[3]);
 
-    var direction = Vector3.Normalize(forwardWorld);
+    Vector3.NormalizeToRef(TmpVectors.Vector3[3], refRay.direction);
 
-    return new Ray(origin, direction, length);
+    return refRay;
 };

+ 49 - 18
src/XR/features/WebXRControllerPointerSelection.ts

@@ -18,6 +18,8 @@ import { PickingInfo } from "../../Collisions/pickingInfo";
 import { WebXRAbstractFeature } from "./WebXRAbstractFeature";
 import { UtilityLayerRenderer } from "../../Rendering/utilityLayerRenderer";
 import { WebXRAbstractMotionController } from "../motionController/webXRAbstractMotionController";
+import { WebXRCamera } from "../webXRCamera";
+import { Node } from "../../node";
 
 /**
  * Options interface for the pointer selection module
@@ -35,7 +37,7 @@ export interface IWebXRControllerPointerSelectionOptions {
      */
     disablePointerUpOnTouchOut: boolean;
     /**
-     * For gaze mode (time to select instead of press)
+     * For gaze mode for tracked-pointer / controllers (time to select instead of button press)
      */
     forceGazeMode: boolean;
     /**
@@ -63,6 +65,10 @@ export interface IWebXRControllerPointerSelectionOptions {
      */
     useUtilityLayer?: boolean;
     /**
+     * Optional WebXR camera to be used for gaze selection
+     */
+    gazeCamera?: WebXRCamera;
+    /**
      * the xr input to use with this pointer selection
      */
     xrInput: WebXRInput;
@@ -80,7 +86,7 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
             return;
         }
         // only support tracker pointer
-        const { laserPointer, selectionMesh } = this._generateNewMeshPair(xrController);
+        const { laserPointer, selectionMesh } = this._generateNewMeshPair(xrController.pointer);
 
         // get two new meshes
         this._controllers[xrController.uniqueId] = {
@@ -104,7 +110,8 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
 
     private _controllers: {
         [controllerUniqueId: string]: {
-            xrController: WebXRInputSource;
+            xrController?: WebXRInputSource;
+            webXRCamera?: WebXRCamera;
             selectionComponent?: WebXRControllerComponent;
             onButtonChangedObserver?: Nullable<Observer<WebXRControllerComponent>>;
             onFrameObserver?: Nullable<Observer<XRFrame>>;
@@ -201,6 +208,23 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
 
         this._scene.constantlyUpdateMeshUnderPointer = true;
 
+        if (this._options.gazeCamera) {
+            const webXRCamera = this._options.gazeCamera;
+
+            const { laserPointer, selectionMesh } = this._generateNewMeshPair(webXRCamera);
+
+            this._controllers["camera"] = {
+                webXRCamera,
+                laserPointer,
+                selectionMesh,
+                meshUnderPointer: null,
+                pick: null,
+                tmpRay: new Ray(new Vector3(), new Vector3()),
+                id: WebXRControllerPointerSelection._idCounter++,
+            };
+            this._attachGazeMode();
+        }
+
         return true;
     }
 
@@ -247,7 +271,7 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
 
         for (let i = 0; i < keys.length; ++i) {
             if (this._controllers[keys[i]].id === id) {
-                return this._controllers[keys[i]].xrController;
+                return this._controllers[keys[i]].xrController || null;
             }
         }
         return null;
@@ -258,7 +282,13 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
             const controllerData = this._controllers[id];
 
             // Every frame check collisions/input
-            controllerData.xrController.getWorldPointerRayToRef(controllerData.tmpRay);
+            if (controllerData.xrController) {
+                controllerData.xrController.getWorldPointerRayToRef(controllerData.tmpRay);
+            } else if (controllerData.webXRCamera) {
+                controllerData.webXRCamera.getForwardRayToRef(controllerData.tmpRay);
+            } else {
+                return;
+            }
             controllerData.pick = this._scene.pickWithRay(controllerData.tmpRay, this._scene.pointerMovePredicate || this.raySelectionPredicate);
 
             const pick = controllerData.pick;
@@ -292,8 +322,8 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
         });
     }
 
-    private _attachGazeMode(xrController: WebXRInputSource) {
-        const controllerData = this._controllers[xrController.uniqueId];
+    private _attachGazeMode(xrController?: WebXRInputSource) {
+        const controllerData = this._controllers[(xrController && xrController.uniqueId) || "camera"];
         // attached when touched, detaches when raised
         const timeToSelect = this._options.timeToSelect || 3000;
         const sceneToRenderTo = this._options.useUtilityLayer ? this._options.customUtilityLayerScene || UtilityLayerRenderer.DefaultUtilityLayer.utilityLayerScene : this._scene;
@@ -359,12 +389,14 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
         if (this._options.renderingGroupId !== undefined) {
             discMesh.renderingGroupId = this._options.renderingGroupId;
         }
-        xrController.onDisposeObservable.addOnce(() => {
-            if (controllerData.pick && !this._options.disablePointerUpOnTouchOut && downTriggered) {
-                this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
-            }
-            discMesh.dispose();
-        });
+        if (xrController) {
+            xrController.onDisposeObservable.addOnce(() => {
+                if (controllerData.pick && !this._options.disablePointerUpOnTouchOut && downTriggered) {
+                    this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
+                }
+                discMesh.dispose();
+            });
+        }
     }
 
     private _attachScreenRayMode(xrController: WebXRInputSource) {
@@ -439,7 +471,7 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
         } else {
             // use the select and squeeze events
             const selectStartListener = (event: XRInputSourceEvent) => {
-                if (event.inputSource === controllerData.xrController.inputSource && controllerData.pick) {
+                if (controllerData.xrController && event.inputSource === controllerData.xrController.inputSource && controllerData.pick) {
                     this._scene.simulatePointerDown(controllerData.pick, { pointerId: controllerData.id });
                     (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshPickedColor;
                     (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.laserPointerPickedColor;
@@ -447,7 +479,7 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
             };
 
             const selectEndListener = (event: XRInputSourceEvent) => {
-                if (event.inputSource === controllerData.xrController.inputSource && controllerData.pick) {
+                if (controllerData.xrController && event.inputSource === controllerData.xrController.inputSource && controllerData.pick) {
                     this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
                     (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshDefaultColor;
                     (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.laserPointerDefaultColor;
@@ -501,7 +533,7 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
         delete this._controllers[xrControllerUniqueId];
     }
 
-    private _generateNewMeshPair(xrController: WebXRInputSource) {
+    private _generateNewMeshPair(meshParent: Node) {
         const sceneToRenderTo = this._options.useUtilityLayer ? this._options.customUtilityLayerScene || UtilityLayerRenderer.DefaultUtilityLayer.utilityLayerScene : this._scene;
         const laserPointer = CylinderBuilder.CreateCylinder(
             "laserPointer",
@@ -514,7 +546,7 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
             },
             sceneToRenderTo
         );
-        laserPointer.parent = xrController.pointer;
+        laserPointer.parent = meshParent;
         let laserPointerMaterial = new StandardMaterial("laserPointerMat", sceneToRenderTo);
         laserPointerMaterial.emissiveColor = this.laserPointerDefaultColor;
         laserPointerMaterial.alpha = 0.7;
@@ -567,7 +599,6 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
         this._tmpVectorForPickCompare.set(Math.abs(this._tmpVectorForPickCompare.x), Math.abs(this._tmpVectorForPickCompare.y), Math.abs(this._tmpVectorForPickCompare.z));
         const delta = (this._options.gazeModePointerMovedFactor || 1) * 0.01 * newPick.distance;
         const length = this._tmpVectorForPickCompare.length();
-        console.log(delta, length, newPick.distance);
         if (length > delta) {
             return true;
         }