Browse Source

Merge branch 'master' of https://github.com/BabylonJS/Babylon.js

David Catuhe 5 years ago
parent
commit
c7927423ac

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

@@ -53,6 +53,7 @@
 - PNG support for browsers not supporting SVG ([RaananW](https://github.com/RaananW/))
 - Device orientation event permissions for iOS 13+ ([RaananW](https://github.com/RaananW/))
 - Added `DirectionalLight.autoCalcShadowZBounds` to automatically compute the `shadowMinZ` and `shadowMaxZ` values ([Popov72](https://github.com/Popov72))
+- Improved cascade blending in CSM shadow technic ([Popov72](https://github.com/Popov72))
 
 ### Engine
 
@@ -183,7 +184,9 @@
 - 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 is Babylon-conform (position, rotationQuaternion, world matrix, direction etc') ([#7239](https://github.com/BabylonJS/Babylon.js/issues/7239)) ([RaananW](https://github.com/RaananW/))
-- XR Input now using standard profiles and completely separated from the babylon gamepad class ([#7348](https://github.com/BabylonJS/Babylon.js/issues/7348)) ([RaananW](https://github.com/RaananW/))
+- XR Input now using standard profiles and completely separated from the gamepad class ([#7348](https://github.com/BabylonJS/Babylon.js/issues/7348)) ([RaananW](https://github.com/RaananW/))
+- Teleportation and controller selection are now WebXR features. ([#7290](https://github.com/BabylonJS/Babylon.js/issues/7290)) ([RaananW](https://github.com/RaananW/))
+- Teleportation allows selecting direction before teleporting when using thumbstick/touchpad. ([#7290](https://github.com/BabylonJS/Babylon.js/issues/7290)) ([RaananW](https://github.com/RaananW/))
 - It is now possible to force a certain profile type for the controllers ([#7348](https://github.com/BabylonJS/Babylon.js/issues/7375)) ([RaananW](https://github.com/RaananW/))
 
 ### Ray

+ 9 - 19
src/Cameras/VR/vrExperienceHelper.ts

@@ -39,7 +39,6 @@ import { Axis } from '../../Maths/math.axis';
 import { WebXRSessionManager } from '../XR/webXRSessionManager';
 import { WebXRDefaultExperience } from '../XR/webXRDefaultExperience';
 import { WebXRState } from '../XR/webXRTypes';
-import { WebXRControllerTeleportation } from '../XR/webXRControllerTeleportation';
 
 /**
  * Options to modify the vr teleportation behavior.
@@ -842,21 +841,6 @@ export class VRExperienceHelper {
                                     break;
                             }
                         });
-
-                        this.xr.input.onControllerAddedObservable.add((controller) => {
-                            // var webVRController = controller.gamepadController;
-                            // if (webVRController) {
-                            //     var localController = new VRExperienceHelperControllerGazer(webVRController, this._scene, this._cameraGazer._gazeTracker);
-
-                            //     if (controller.inputSource.handedness === "right" || (this._leftController)) {
-                            //         this._rightController = localController;
-                            //     } else {
-                            //         this._leftController = localController;
-                            //     }
-
-                            //     this._tryEnableInteractionOnController(localController);
-                            // }
-                        });
                     });
                 } else {
                     // XR not supported (thou exists), continue WebVR init
@@ -1460,14 +1444,18 @@ export class VRExperienceHelper {
                     }
                 }
                 if (this.xr) {
-                    this.xr.teleportation = new WebXRControllerTeleportation(this.xr.input, floorMeshes);
+                    if (!this.xr.teleportation.attached) {
+                        this.xr.teleportation.attach();
+                    }
                     return;
                 } else if (!this.xrTestDone) {
                     const waitForXr = () => {
                         if (this.xrTestDone) {
                             this._scene.unregisterBeforeRender(waitForXr);
                             if (this.xr) {
-                                this.xr.teleportation = new WebXRControllerTeleportation(this.xr.input, floorMeshes);
+                                if (!this.xr.teleportation.attached) {
+                                    this.xr.teleportation.attach();
+                                }
                             } else {
                                 this.enableTeleportation(vrTeleportationOptions);
                             }
@@ -1479,7 +1467,9 @@ export class VRExperienceHelper {
             }
 
             if (this.xr && vrTeleportationOptions.floorMeshes) {
-                this.xr.teleportation = new WebXRControllerTeleportation(this.xr.input, vrTeleportationOptions.floorMeshes);
+                if (!this.xr.teleportation.attached) {
+                    this.xr.teleportation.attach();
+                }
                 return;
             } else {
                 if (this.webVROptions.useXR && !this.xrTestDone) {

+ 7 - 1
src/Cameras/XR/features/WebXRAnchorSystem.ts

@@ -81,11 +81,17 @@ export class WebXRAnchorSystem implements IWebXRFeature {
      */
     public onAnchorRemovedObservable: Observable<IWebXRAnchor> = new Observable();
 
+    private _attached: boolean = false;
+    /**
+     * Is this feature attached
+     */
+    public get attached() {
+        return this._attached;
+    }
     private _planeDetector: WebXRPlaneDetector;
     private _hitTestModule: WebXRHitTestLegacy;
 
     private _enabled: boolean = false;
-    private _attached: boolean = false;
     private _trackedAnchors: Array<IWebXRAnchor> = [];
     private _lastFrameDetected: XRAnchorSet = new Set();
     private _observerTracked: Nullable<Observer<XRFrame>>;

+ 13 - 0
src/Cameras/XR/features/WebXRBackgroundRemover.ts

@@ -54,6 +54,14 @@ export class WebXRBackgroundRemover implements IWebXRFeature {
      */
     public onBackgroundStateChangedObservable: Observable<boolean> = new Observable();
 
+    private _attached: boolean = false;
+    /**
+     * Is this feature attached
+     */
+    public get attached() {
+        return this._attached;
+    }
+
     /**
      * constructs a new background remover module
      * @param _xrSessionManager the session manager for this module
@@ -76,6 +84,8 @@ export class WebXRBackgroundRemover implements IWebXRFeature {
     attach(): boolean {
         this._setBackgroundState(false);
 
+        this._attached = true;
+
         return true;
     }
 
@@ -88,6 +98,8 @@ export class WebXRBackgroundRemover implements IWebXRFeature {
     detach(): boolean {
         this._setBackgroundState(true);
 
+        this._attached = false;
+
         return true;
     }
 
@@ -126,6 +138,7 @@ export class WebXRBackgroundRemover implements IWebXRFeature {
      * Dispose this feature and all of the resources attached
      */
     dispose(): void {
+        this.detach();
         this.onBackgroundStateChangedObservable.clear();
     }
 }

+ 321 - 0
src/Cameras/XR/features/WebXRControllerPointerSelection.ts

@@ -0,0 +1,321 @@
+import { WebXRFeaturesManager, IWebXRFeature } from "../webXRFeaturesManager";
+import { WebXRSessionManager } from '../webXRSessionManager';
+import { AbstractMesh } from '../../../Meshes/abstractMesh';
+import { Observer } from '../../../Misc/observable';
+import { WebXRInput } from '../webXRInput';
+import { WebXRController } from '../webXRController';
+import { Scene } from '../../../scene';
+import { WebXRControllerComponent } from '../motionController/webXRControllerComponent';
+import { Nullable } from '../../../types';
+import { Vector3 } from '../../../Maths/math.vector';
+import { Color3 } from '../../../Maths/math.color';
+import { Axis } from '../../../Maths/math.axis';
+import { StandardMaterial } from '../../../Materials/standardMaterial';
+import { CylinderBuilder } from '../../../Meshes/Builders/cylinderBuilder';
+import { TorusBuilder } from '../../../Meshes/Builders/torusBuilder';
+import { Ray } from '../../../Culling/ray';
+import { PickingInfo } from '../../../Collisions/pickingInfo';
+
+const Name = "xr-controller-pointer-selection";
+
+/**
+ * Options interface for the pointer selection module
+ */
+export interface IWebXRControllerPointerSelectionOptions {
+    /**
+     * the xr input to use with this pointer selection
+     */
+    xrInput: WebXRInput;
+    /**
+     * Different button type to use instead of the main component
+     */
+    overrideButtonId?: string;
+}
+
+/**
+ * A module that will enable pointer selection for motion controllers of XR Input Sources
+ */
+export class WebXRControllerPointerSelection implements IWebXRFeature {
+
+    /**
+     * The module's name
+     */
+    public static readonly Name = Name;
+    /**
+     * The (Babylon) version of this module.
+     * This is an integer representing the implementation version.
+     * This number does not correspond to the webxr specs version
+     */
+    public static readonly Version = 1;
+
+    /**
+     * This color will be set to the laser pointer when selection is triggered
+     */
+    public onPickedLaserPointerColor: Color3 = new Color3(0.7, 0.7, 0.7);
+    /**
+     * This color will be applied to the selection ring when selection is triggered
+     */
+    public onPickedSelectionMeshColor: Color3 = new Color3(0.7, 0.7, 0.7);
+    /**
+     * default color of the selection ring
+     */
+    public selectionMeshDefaultColor: Color3 = new Color3(0.5, 0.5, 0.5);
+    /**
+     * Default color of the laser pointer
+     */
+    public lasterPointerDefaultColor: Color3 = new Color3(0.5, 0.5, 0.5);
+
+    private static _idCounter = 0;
+
+    private _observerTracked: Nullable<Observer<XRFrame>>;
+    private _attached: boolean = false;
+    private _tmpRay = new Ray(new Vector3(), new Vector3());
+
+    private _controllers: {
+        [controllerUniqueId: string]: {
+            xrController: WebXRController;
+            selectionComponent?: WebXRControllerComponent;
+            onButtonChangedObserver?: Nullable<Observer<WebXRControllerComponent>>;
+            laserPointer: AbstractMesh;
+            selectionMesh: AbstractMesh;
+            pick: Nullable<PickingInfo>;
+            id: number;
+        };
+    } = {};
+
+    /**
+     * Is this feature attached
+     */
+    public get attached() {
+        return this._attached;
+    }
+
+    private _scene: Scene;
+
+    /**
+     * constructs a new background remover module
+     * @param _xrSessionManager the session manager for this module
+     * @param _options read-only options to be used in this module
+     */
+    constructor(private _xrSessionManager: WebXRSessionManager, private readonly _options: IWebXRControllerPointerSelectionOptions) {
+        this._scene = this._xrSessionManager.scene;
+    }
+
+    /**
+     * attach this feature
+     * Will usually be called by the features manager
+     *
+     * @returns true if successful.
+     */
+    attach(): boolean {
+
+        this._options.xrInput.controllers.forEach(this._attachController);
+        this._options.xrInput.onControllerAddedObservable.add(this._attachController);
+        this._options.xrInput.onControllerRemovedObservable.add((controller) => {
+            // REMOVE the controller
+            this._detachController(controller.uniqueId);
+        });
+
+        this._observerTracked = this._xrSessionManager.onXRFrameObservable.add(() => {
+            Object.keys(this._controllers).forEach((id) => {
+                const controllerData = this._controllers[id];
+
+                // Every frame check collisions/input
+                controllerData.xrController.getWorldPointerRayToRef(this._tmpRay);
+                controllerData.pick = this._scene.pickWithRay(this._tmpRay);
+
+                if (controllerData.selectionComponent && controllerData.selectionComponent.pressed) {
+                    (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.onPickedSelectionMeshColor;
+                    (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.onPickedLaserPointerColor;
+                } else {
+                    (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshDefaultColor;
+                    (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.lasterPointerDefaultColor;
+                }
+
+                const pick = controllerData.pick;
+
+                if (pick && pick.pickedPoint && pick.hit) {
+                    // Update laser state
+                    this._updatePointerDistance(controllerData.laserPointer, pick.distance);
+
+                    // Update cursor state
+                    controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
+                    controllerData.selectionMesh.scaling.x = Math.sqrt(pick.distance);
+                    controllerData.selectionMesh.scaling.y = Math.sqrt(pick.distance);
+                    controllerData.selectionMesh.scaling.z = Math.sqrt(pick.distance);
+
+                    // To avoid z-fighting
+                    let pickNormal = this._convertNormalToDirectionOfRay(pick.getNormal(true), this._tmpRay);
+                    let deltaFighting = 0.001;
+                    controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
+                    if (pickNormal) {
+                        let axis1 = Vector3.Cross(Axis.Y, pickNormal);
+                        let axis2 = Vector3.Cross(pickNormal, axis1);
+                        Vector3.RotationFromAxisToRef(axis2, pickNormal, axis1, controllerData.selectionMesh.rotation);
+                        controllerData.selectionMesh.position.addInPlace(pickNormal.scale(deltaFighting));
+                    }
+                    controllerData.selectionMesh.isVisible = true;
+                } else {
+                    controllerData.selectionMesh.isVisible = false;
+                }
+            });
+        });
+
+        this._attached = true;
+
+        return true;
+    }
+
+    /**
+     * detach this feature.
+     * Will usually be called by the features manager
+     *
+     * @returns true if successful.
+     */
+    detach(): boolean {
+
+        if (this._observerTracked) {
+            this._xrSessionManager.onXRFrameObservable.remove(this._observerTracked);
+        }
+
+        Object.keys(this._controllers).forEach((controllerId) => {
+            this._detachController(controllerId);
+        });
+
+        this._attached = false;
+
+        return true;
+    }
+
+    private _attachController = (xrController: WebXRController) => {
+        // only support tracker pointer
+        if (xrController.inputSource.targetRayMode !== "tracked-pointer") {
+            return;
+        }
+
+        if (this._controllers[xrController.uniqueId] || !xrController.gamepadController) {
+            // already attached
+            return;
+        }
+
+        const { laserPointer, selectionMesh } = this._generateNewMeshPair(xrController);
+
+        // get two new meshes
+        this._controllers[xrController.uniqueId] = {
+            xrController,
+            laserPointer,
+            selectionMesh,
+            pick: null,
+            id: WebXRControllerPointerSelection._idCounter++
+        };
+        const controllerData = this._controllers[xrController.uniqueId];
+
+        if (this._options.overrideButtonId) {
+            controllerData.selectionComponent = xrController.gamepadController.getComponent(this._options.overrideButtonId);
+        }
+        if (!controllerData.selectionComponent) {
+            controllerData.selectionComponent = xrController.gamepadController.getMainComponent();
+        }
+
+        let observer: Nullable<Observer<XRFrame>> = null;
+
+        controllerData.onButtonChangedObserver = controllerData.selectionComponent.onButtonStateChanged.add((component) => {
+            if (component.changes.pressed) {
+                const pressed = component.changes.pressed.current;
+                if (controllerData.pick) {
+                    if (pressed) {
+                        this._scene.simulatePointerDown(controllerData.pick, { pointerId: controllerData.id });
+                        observer = this._xrSessionManager.onXRFrameObservable.add(() => {
+                            if (controllerData.pick) {
+                                this._scene.simulatePointerMove(controllerData.pick, { pointerId: controllerData.id });
+                            }
+                        });
+                    } else {
+                        this._xrSessionManager.onXRFrameObservable.remove(observer);
+                        this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
+                    }
+                }
+            }
+        });
+
+    }
+
+    private _detachController(xrControllerUniqueId: string) {
+        const controllerData = this._controllers[xrControllerUniqueId];
+        if (!controllerData) { return; }
+        if (controllerData.selectionComponent) {
+            if (controllerData.onButtonChangedObserver) {
+                controllerData.selectionComponent.onButtonStateChanged.remove(controllerData.onButtonChangedObserver);
+            }
+        }
+        controllerData.selectionMesh.dispose();
+        controllerData.laserPointer.dispose();
+        // remove from the map
+        delete this._controllers[xrControllerUniqueId];
+    }
+
+    private _generateNewMeshPair(xrController: WebXRController) {
+        const laserPointer = CylinderBuilder.CreateCylinder("laserPointer", {
+            height: 1,
+            diameterTop: 0.0002,
+            diameterBottom: 0.004,
+            tessellation: 20,
+            subdivisions: 1
+        }, this._scene);
+        laserPointer.parent = xrController.pointer;
+        let laserPointerMaterial = new StandardMaterial("laserPointerMat", this._scene);
+        laserPointerMaterial.emissiveColor = this.lasterPointerDefaultColor;
+        laserPointerMaterial.alpha = 0.6;
+        laserPointer.material = laserPointerMaterial;
+        laserPointer.rotation.x = Math.PI / 2;
+        this._updatePointerDistance(laserPointer, 1);
+        laserPointer.isPickable = false;
+
+        // Create a gaze tracker for the  XR controller
+        const selectionMesh = TorusBuilder.CreateTorus("gazeTracker", {
+            diameter: 0.0035 * 3,
+            thickness: 0.0025 * 3,
+            tessellation: 20
+        }, this._scene);
+        selectionMesh.bakeCurrentTransformIntoVertices();
+        selectionMesh.isPickable = false;
+        selectionMesh.isVisible = false;
+        let targetMat = new StandardMaterial("targetMat", this._scene);
+        targetMat.specularColor = Color3.Black();
+        targetMat.emissiveColor = this.selectionMeshDefaultColor;
+        targetMat.backFaceCulling = false;
+        selectionMesh.material = targetMat;
+
+        return {
+            laserPointer,
+            selectionMesh
+        };
+    }
+
+    private _convertNormalToDirectionOfRay(normal: Nullable<Vector3>, ray: Ray) {
+        if (normal) {
+            let angle = Math.acos(Vector3.Dot(normal, ray.direction));
+            if (angle < Math.PI / 2) {
+                normal.scaleInPlace(-1);
+            }
+        }
+        return normal;
+    }
+
+    private _updatePointerDistance(_laserPointer: AbstractMesh, distance: number = 100) {
+        _laserPointer.scaling.y = distance;
+        _laserPointer.position.z = distance / 2;
+    }
+
+    /**
+     * Dispose this feature and all of the resources attached
+     */
+    dispose(): void {
+        this.detach();
+    }
+}
+
+//register the plugin
+WebXRFeaturesManager.AddWebXRFeature(WebXRControllerPointerSelection.Name, (xrSessionManager, options) => {
+    return () => new WebXRControllerPointerSelection(xrSessionManager, options);
+}, WebXRControllerPointerSelection.Version, true);

+ 542 - 0
src/Cameras/XR/features/WebXRControllerTeleportation.ts

@@ -0,0 +1,542 @@
+import { IWebXRFeature, WebXRFeaturesManager } from '../webXRFeaturesManager';
+import { Observer } from '../../../Misc/observable';
+import { WebXRSessionManager } from '../webXRSessionManager';
+import { Nullable } from '../../../types';
+import { WebXRInput } from '../webXRInput';
+import { WebXRController } from '../webXRController';
+import { WebXRControllerComponent, IWebXRMotionControllerAxesValue } from '../motionController/webXRControllerComponent';
+import { AbstractMesh } from '../../../Meshes/abstractMesh';
+import { Vector3, Quaternion } from '../../../Maths/math.vector';
+import { Ray } from '../../../Culling/ray';
+import { Material } from '../../../Materials/material';
+import { DynamicTexture } from '../../../Materials/Textures/dynamicTexture';
+import { CylinderBuilder } from '../../../Meshes/Builders/cylinderBuilder';
+import { SineEase, EasingFunction } from '../../../Animations/easing';
+import { Animation } from '../../../Animations/animation';
+import { Axis } from '../../../Maths/math.axis';
+import { StandardMaterial } from '../../../Materials/standardMaterial';
+import { GroundBuilder } from '../../../Meshes/Builders/groundBuilder';
+import { TorusBuilder } from '../../../Meshes/Builders/torusBuilder';
+import { PickingInfo } from '../../../Collisions/pickingInfo';
+import { Curve3 } from '../../../Maths/math.path';
+import { LinesBuilder } from '../../../Meshes/Builders/linesBuilder';
+
+const Name = "xr-controller-teleportation";
+
+/**
+ * The options container for the teleportation module
+ */
+export interface IWebXRTeleportationOptions {
+    /**
+     * Babylon XR Input class for controller
+     */
+    xrInput: WebXRInput;
+    /**
+     * A list of meshes to use as floor meshes.
+     * Meshes can be added and removed after initializing the feature using the
+     * addFloorMesh and removeFloorMesh functions
+     */
+    floorMeshes: AbstractMesh[];
+    /**
+     * Provide your own teleportation mesh instead of babylon's wonderful doughnut.
+     * If you want to support rotation, make sure your mesh has a direction indicator.
+     *
+     * When left untouched, the default mesh will be initialized.
+     */
+    teleportationTargetMesh?: AbstractMesh;
+    /**
+     * Values to configure the default target mesh
+     */
+    defaultTargetMeshOptions?: {
+        /**
+         * Fill color of the teleportation area
+         */
+        teleportationFillColor?: string;
+        /**
+         * Border color for the teleportation area
+         */
+        teleportationBorderColor?: string;
+        /**
+         * Override the default material of the torus and arrow
+         */
+        torusArrowMaterial?: Material;
+        /**
+         * Disable the mesh's animation sequence
+         */
+        disableAnimation?: boolean;
+    };
+}
+
+/**
+ * This is a teleportation feature to be used with webxr-enabled motion controllers.
+ * When enabled and attached, the feature will allow a user to move aroundand rotate in the scene using
+ * the input of the attached controllers.
+ */
+export class WebXRMotionControllerTeleportation implements IWebXRFeature {
+    /**
+     * The module's name
+     */
+    public static readonly Name = Name;
+    /**
+     * The (Babylon) version of this module.
+     * This is an integer representing the implementation version.
+     * This number does not correspond to the webxr specs version
+     */
+    public static readonly Version = 1;
+
+    /**
+     * Is rotation enabled when moving forward?
+     * Disabling this feature will prevent the user from deciding the direction when teleporting
+     */
+    public rotationEnabled: boolean = true;
+    /**
+     * Should the module support parabolic ray on top of direct ray
+     * If enabled, the user will be able to point "at the sky" and move according to predefined radius distance
+     * Very helpful when moving between floors / different heights
+     */
+    public parabolicRayEnabled: boolean = true;
+    /**
+     * The distance from the user to the inspection point in the direction of the controller
+     * A higher number will allow the user to move further
+     * defaults to 5 (meters, in xr units)
+     */
+    public parabolicCheckRadius: number = 5;
+    /**
+     * How much rotation should be applied when rotating right and left
+     */
+    public rotationAngle: number = Math.PI / 8;
+
+    /**
+     * Distance to travel when moving backwards
+     */
+    public backwardsTeleportationDistance: number = 0.5;
+
+    private _observerTracked: Nullable<Observer<XRFrame>>;
+
+    private _attached: boolean = false;
+    /**
+     * Is this feature attached
+     */
+    public get attached() {
+        return this._attached;
+    }
+
+    /**
+     * Add a new mesh to the floor meshes array
+     * @param mesh the mesh to use as floor mesh
+     */
+    public addFloorMesh(mesh: AbstractMesh) {
+        this._options.floorMeshes.push(mesh);
+    }
+
+    /**
+     * Remove a mesh from the floor meshes array
+     * @param mesh the mesh to remove
+     */
+    public removeFloorMesh(mesh: AbstractMesh) {
+        const index = this._options.floorMeshes.indexOf(mesh);
+        if (index !== -1) {
+            this._options.floorMeshes.splice(index, 1);
+        }
+    }
+
+    /**
+     * Remove a mesh from the floor meshes array using its name
+     * @param name the mesh name to remove
+     */
+    public removeFloorMeshByName(name: string) {
+        const mesh = this._xrSessionManager.scene.getMeshByName(name);
+        if (mesh) {
+            this.removeFloorMesh(mesh);
+        }
+    }
+
+    private _tmpRay = new Ray(new Vector3(), new Vector3());
+    private _tmpVector = new Vector3();
+
+    private _controllers: {
+        [controllerUniqueId: string]: {
+            xrController: WebXRController;
+            teleportationComponent?: WebXRControllerComponent;
+            teleportationState: {
+                forward: boolean;
+                backwards: boolean;
+                currentRotation: number;
+                baseRotation: number;
+                rotating: boolean;
+            }
+            onAxisChangedObserver?: Nullable<Observer<IWebXRMotionControllerAxesValue>>;
+            onButtonChangedObserver?: Nullable<Observer<WebXRControllerComponent>>;
+        };
+    } = {};
+
+    /**
+     * constructs a new anchor system
+     * @param _xrSessionManager an instance of WebXRSessionManager
+     * @param _options configuration object for this feature
+     */
+    constructor(private _xrSessionManager: WebXRSessionManager, private _options: IWebXRTeleportationOptions) {
+        // create default mesh if not provided
+        if (!this._options.teleportationTargetMesh) {
+            this.createDefaultTargetMesh();
+        }
+
+        this.setTargetMeshVisibility(false);
+    }
+
+    private _selectionFeature: IWebXRFeature;
+
+    /**
+     * This function sets a selection feature that will be disabled when
+     * the forward ray is shown and will be reattached when hidden.
+     * This is used to remove the selection rays when moving.
+     * @param selectionFeature the feature to disable when forward movement is enabled
+     */
+    public setSelectionFeature(selectionFeature: IWebXRFeature) {
+        this._selectionFeature = selectionFeature;
+    }
+
+    /**
+     * attach this feature
+     * Will usually be called by the features manager
+     *
+     * @returns true if successful.
+     */
+    attach(): boolean {
+
+        this._options.xrInput.controllers.forEach(this._attachController);
+        this._options.xrInput.onControllerAddedObservable.add(this._attachController);
+        this._options.xrInput.onControllerRemovedObservable.add((controller) => {
+            // REMOVE the controller
+            this._detachController(controller.uniqueId);
+        });
+
+        this._observerTracked = this._xrSessionManager.onXRFrameObservable.add(() => {
+            const frame = this._xrSessionManager.currentFrame;
+            const scene = this._xrSessionManager.scene;
+            if (!this._attached || !frame) { return; }
+
+            // render target if needed
+            const targetMesh = this._options.teleportationTargetMesh;
+            if (this._currentTeleportationControllerId) {
+                if (!targetMesh) {
+                    return;
+                }
+                targetMesh.rotationQuaternion = targetMesh.rotationQuaternion || new Quaternion();
+                const controllerData = this._controllers[this._currentTeleportationControllerId];
+                if (controllerData.teleportationState.forward) {
+                    // set the rotation
+                    Quaternion.RotationYawPitchRollToRef(controllerData.teleportationState.currentRotation + controllerData.teleportationState.baseRotation, 0, 0, targetMesh.rotationQuaternion);
+                    // set the ray and position
+
+                    let hitPossible = false;
+                    // first check if direct ray possible
+                    controllerData.xrController.getWorldPointerRayToRef(this._tmpRay);
+                    let pick = scene.pickWithRay(this._tmpRay, (o) => {
+                        return this._options.floorMeshes.indexOf(o) !== -1;
+                    });
+                    if (pick && pick.pickedPoint) {
+                        hitPossible = true;
+                        this.setTargetMeshPosition(pick.pickedPoint);
+                        this.setTargetMeshVisibility(true);
+                        this.showParabolicPath(pick);
+                    } else {
+                        if (this.parabolicRayEnabled) {
+                            // check parabolic ray
+                            const radius = this.parabolicCheckRadius;
+                            this._tmpRay.origin.addToRef(this._tmpRay.direction.scale(radius * 2), this._tmpVector);
+                            this._tmpVector.y = this._tmpRay.origin.y;
+                            this._tmpRay.origin.addInPlace(this._tmpRay.direction.scale(radius));
+                            this._tmpVector.subtractToRef(this._tmpRay.origin, this._tmpRay.direction);
+                            this._tmpRay.direction.normalize();
+
+                            let pick = scene.pickWithRay(this._tmpRay, (o) => {
+                                return this._options.floorMeshes.indexOf(o) !== -1;
+                            });
+                            if (pick && pick.pickedPoint) {
+                                hitPossible = true;
+                                this.setTargetMeshPosition(pick.pickedPoint);
+                                this.setTargetMeshVisibility(true);
+                                this.showParabolicPath(pick);
+                            }
+                        }
+                    }
+
+                    // if needed, set visible:
+                    this.setTargetMeshVisibility(hitPossible);
+                } else {
+                    this.setTargetMeshVisibility(false);
+                }
+            } else {
+                this.setTargetMeshVisibility(false);
+            }
+        });
+
+        this._attached = true;
+        return true;
+    }
+
+    /**
+     * detach this feature.
+     * Will usually be called by the features manager
+     *
+     * @returns true if successful.
+     */
+    detach(): boolean {
+        this._attached = false;
+
+        if (this._observerTracked) {
+            this._xrSessionManager.onXRFrameObservable.remove(this._observerTracked);
+        }
+
+        Object.keys(this._controllers).forEach((controllerId) => {
+            this._detachController(controllerId);
+        });
+
+        return true;
+    }
+
+    /**
+     * Dispose this feature and all of the resources attached
+     */
+    dispose(): void {
+        this.detach();
+        this._options.teleportationTargetMesh && this._options.teleportationTargetMesh.dispose(false, true);
+    }
+
+    private _currentTeleportationControllerId: string;
+
+    private _attachController = (xrController: WebXRController) => {
+        if (this._controllers[xrController.uniqueId]) {
+            // already attached
+            return;
+        }
+        this._controllers[xrController.uniqueId] = {
+            xrController,
+            teleportationState: {
+                forward: false,
+                backwards: false,
+                rotating: false,
+                currentRotation: 0,
+                baseRotation: 0
+            }
+        };
+        const controllerData = this._controllers[xrController.uniqueId];
+        // motion controller support
+        if (xrController.gamepadController) {
+            const movementController = xrController.gamepadController.getComponent(WebXRControllerComponent.THUMBSTICK) || xrController.gamepadController.getComponent(WebXRControllerComponent.TOUCHPAD);
+            if (!movementController) {
+                // use trigger to move on long press
+            } else {
+                controllerData.onButtonChangedObserver = movementController.onButtonStateChanged.add(() => {
+                    if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId && controllerData.teleportationState.forward && !movementController.touched) {
+                        controllerData.teleportationState.forward = false;
+                        this._currentTeleportationControllerId = "";
+                        // do the movement forward here
+                        if (this._options.teleportationTargetMesh && this._options.teleportationTargetMesh.isVisible) {
+                            const height = this._options.xrInput.xrCamera.position.y - this._options.teleportationTargetMesh.position.y;
+                            this._options.xrInput.xrCamera.position.copyFrom(this._options.teleportationTargetMesh.position);
+                            this._options.xrInput.xrCamera.position.y += height;
+                            this._options.xrInput.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, controllerData.teleportationState.currentRotation, 0));
+                        }
+                    }
+                });
+                // use thumbstick (or touchpad if thumbstick not available)
+                controllerData.onAxisChangedObserver = movementController.onAxisValueChanged.add((axesData) => {
+                    if (axesData.y <= 0.7 && controllerData.teleportationState.backwards) {
+                        //if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId) {
+                        controllerData.teleportationState.backwards = false;
+                        //this._currentTeleportationControllerId = "";
+                        //}
+                    }
+                    if (axesData.y > 0.7 && !controllerData.teleportationState.forward) {
+                        // teleport backwards
+                        if (!controllerData.teleportationState.backwards) {
+                            controllerData.teleportationState.backwards = true;
+                            // teleport backwards ONCE
+                            this._tmpVector.set(0, 0, -this.backwardsTeleportationDistance!);
+                            this._tmpVector.addInPlace(this._options.xrInput.xrCamera.position);
+                            this._tmpRay.origin.copyFrom(this._tmpVector);
+                            this._tmpRay.direction.set(0, -1, 0);
+                            let pick = this._xrSessionManager.scene.pickWithRay(this._tmpRay, (o) => {
+                                return this._options.floorMeshes.indexOf(o) !== -1;
+                            });
+
+                            // pick must exist, but stay safe
+                            if (pick && pick.pickedPoint) {
+                                // Teleport the users feet to where they targeted
+                                this._options.xrInput.xrCamera.position.addInPlace(pick.pickedPoint);
+                            }
+
+                        }
+                    }
+                    if (axesData.y < -0.7 && !this._currentTeleportationControllerId && !controllerData.teleportationState.rotating) {
+                        controllerData.teleportationState.forward = true;
+                        this._currentTeleportationControllerId = controllerData.xrController.uniqueId;
+                        controllerData.teleportationState.baseRotation = this._options.xrInput.xrCamera.rotationQuaternion.toEulerAngles().y;
+                    }
+                    if (axesData.x) {
+                        if (!controllerData.teleportationState.forward) {
+                            if (!controllerData.teleportationState.rotating && Math.abs(axesData.x) > 0.7) {
+                                // rotate in the right direction positive is right
+                                controllerData.teleportationState.rotating = true;
+                                const rotation = this.rotationAngle * (axesData.x > 0 ? 1 : -1);
+                                this._options.xrInput.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, rotation, 0));
+                            }
+                        } else {
+                            if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId) {
+                                // set the rotation of the forward movement
+                                if (this.rotationEnabled) {
+                                    setTimeout(() => {
+                                        controllerData.teleportationState.currentRotation = Math.atan2(axesData.x, -axesData.y);
+                                    });
+                                }
+                            }
+                        }
+                    } else {
+                        controllerData.teleportationState.rotating = false;
+                    }
+                });
+            }
+        }
+    }
+
+    private _detachController(xrControllerUniqueId: string) {
+        const controllerData = this._controllers[xrControllerUniqueId];
+        if (!controllerData) { return; }
+        if (controllerData.teleportationComponent) {
+            if (controllerData.onAxisChangedObserver) {
+                controllerData.teleportationComponent.onAxisValueChanged.remove(controllerData.onAxisChangedObserver);
+            }
+            if (controllerData.onButtonChangedObserver) {
+                controllerData.teleportationComponent.onButtonStateChanged.remove(controllerData.onButtonChangedObserver);
+            }
+        }
+        // remove from the map
+        delete this._controllers[xrControllerUniqueId];
+    }
+
+    private createDefaultTargetMesh() {
+        // set defaults
+        this._options.defaultTargetMeshOptions = this._options.defaultTargetMeshOptions || {};
+        const scene = this._xrSessionManager.scene;
+        let teleportationTarget = GroundBuilder.CreateGround("teleportationTarget", { width: 2, height: 2, subdivisions: 2 }, scene);
+        teleportationTarget.isPickable = false;
+        let length = 512;
+        let dynamicTexture = new DynamicTexture("DynamicTexture", length, scene, true);
+        dynamicTexture.hasAlpha = true;
+        let context = dynamicTexture.getContext();
+        let centerX = length / 2;
+        let centerY = length / 2;
+        let radius = 200;
+        context.beginPath();
+        context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
+        context.fillStyle = this._options.defaultTargetMeshOptions.teleportationFillColor || "#444444";
+        context.fill();
+        context.lineWidth = 10;
+        context.strokeStyle = this._options.defaultTargetMeshOptions.teleportationBorderColor || "#FFFFFF";
+        context.stroke();
+        context.closePath();
+        dynamicTexture.update();
+        let teleportationCircleMaterial = new StandardMaterial("TextPlaneMaterial", scene);
+        teleportationCircleMaterial.diffuseTexture = dynamicTexture;
+        teleportationTarget.material = teleportationCircleMaterial;
+        let torus = TorusBuilder.CreateTorus("torusTeleportation", {
+            diameter: 0.75,
+            thickness: 0.1,
+            tessellation: 20
+        }, scene);
+        torus.isPickable = false;
+        torus.parent = teleportationTarget;
+        if (!this._options.defaultTargetMeshOptions.disableAnimation) {
+            let animationInnerCircle = new Animation("animationInnerCircle", "position.y", 30, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CYCLE);
+            let keys = [];
+            keys.push({
+                frame: 0,
+                value: 0
+            });
+            keys.push({
+                frame: 30,
+                value: 0.4
+            });
+            keys.push({
+                frame: 60,
+                value: 0
+            });
+            animationInnerCircle.setKeys(keys);
+            let easingFunction = new SineEase();
+            easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);
+            animationInnerCircle.setEasingFunction(easingFunction);
+            torus.animations = [];
+            torus.animations.push(animationInnerCircle);
+            scene.beginAnimation(torus, 0, 60, true);
+        }
+
+        var cone = CylinderBuilder.CreateCylinder("cone", { diameterTop: 0, tessellation: 4 }, scene);
+        cone.isPickable = false;
+        cone.scaling.set(0.5, 0.12, 0.2);
+
+        cone.rotate(Axis.X, Math.PI / 2);
+
+        cone.position.z = 0.6;
+        cone.parent = torus;
+
+        if (this._options.defaultTargetMeshOptions.torusArrowMaterial) {
+            torus.material = this._options.defaultTargetMeshOptions.torusArrowMaterial;
+            cone.material = this._options.defaultTargetMeshOptions.torusArrowMaterial;
+        }
+
+        this._options.teleportationTargetMesh = teleportationTarget;
+    }
+
+    private setTargetMeshVisibility(visible: boolean) {
+        if (!this._options.teleportationTargetMesh) { return; }
+        if (this._options.teleportationTargetMesh.isVisible === visible) { return; }
+        this._options.teleportationTargetMesh.isVisible = visible;
+        this._options.teleportationTargetMesh.getChildren(undefined, false).forEach((m) => { (<any>(m)).isVisible = visible; });
+
+        if (!visible) {
+            if (this._quadraticBezierCurve) {
+                this._quadraticBezierCurve.dispose();
+            }
+            if (this._selectionFeature) {
+                this._selectionFeature.attach();
+            }
+        } else {
+            if (this._selectionFeature) {
+                this._selectionFeature.detach();
+            }
+        }
+    }
+
+    private setTargetMeshPosition(newPosition: Vector3) {
+        if (!this._options.teleportationTargetMesh) { return; }
+        this._options.teleportationTargetMesh.position.copyFrom(newPosition);
+        this._options.teleportationTargetMesh.position.y += 0.01;
+    }
+
+    private _quadraticBezierCurve: AbstractMesh;
+
+    private showParabolicPath(pickInfo: PickingInfo) {
+        if (!pickInfo.pickedPoint) { return; }
+
+        const controllerData = this._controllers[this._currentTeleportationControllerId];
+
+        const quadraticBezierVectors = Curve3.CreateQuadraticBezier(
+            controllerData.xrController.pointer.absolutePosition,
+            pickInfo.ray!.origin,
+            pickInfo.pickedPoint,
+            25);
+
+        if (this._quadraticBezierCurve) {
+            this._quadraticBezierCurve.dispose();
+        }
+
+        this._quadraticBezierCurve = LinesBuilder.CreateLines("path line", { points: quadraticBezierVectors.getPoints() });
+        this._quadraticBezierCurve.isPickable = false;
+    }
+}
+
+WebXRFeaturesManager.AddWebXRFeature(WebXRMotionControllerTeleportation.Name, (xrSessionManager, options) => {
+    return () => new WebXRMotionControllerTeleportation(xrSessionManager, options);
+}, WebXRMotionControllerTeleportation.Version, true);

+ 12 - 4
src/Cameras/XR/features/WebXRHitTestLegacy.ts

@@ -58,6 +58,14 @@ export class WebXRHitTestLegacy implements IWebXRFeature {
      */
     public static readonly Version = 1;
 
+    private _attached: boolean = false;
+    /**
+     * Is this feature attached
+     */
+    public get attached() {
+        return this._attached;
+    }
+
     /**
      * Execute a hit test on the current running session using a select event returned from a transient input (such as touch)
      * @param event the (select) event to use to select with
@@ -95,6 +103,8 @@ export class WebXRHitTestLegacy implements IWebXRFeature {
      */
     public onHitTestResultObservable: Observable<IWebXRHitResult[]> = new Observable();
 
+    private _onSelectEnabled = false;
+    private _xrFrameObserver: Nullable<Observer<XRFrame>>;
     /**
      * Creates a new instance of the (legacy version) hit test feature
      * @param _xrSessionManager an instance of WebXRSessionManager
@@ -104,11 +114,9 @@ export class WebXRHitTestLegacy implements IWebXRFeature {
         /**
          * options to use when constructing this feature
          */
-        public readonly options: IWebXRHitTestOptions = {}) { }
+        public readonly options: IWebXRHitTestOptions = {}) {
 
-    private _onSelectEnabled = false;
-    private _xrFrameObserver: Nullable<Observer<XRFrame>>;
-    private _attached: boolean = false;
+    }
 
     /**
      * Populated with the last native XR Hit Results

+ 7 - 1
src/Cameras/XR/features/WebXRPlaneDetector.ts

@@ -74,8 +74,14 @@ export class WebXRPlaneDetector implements IWebXRFeature {
      */
     public onPlaneUpdatedObservable: Observable<IWebXRPlane> = new Observable();
 
-    private _enabled: boolean = false;
     private _attached: boolean = false;
+    /**
+     * Is this feature attached
+     */
+    public get attached() {
+        return this._attached;
+    }
+    private _enabled: boolean = false;
     private _detectedPlanes: Array<IWebXRPlane> = [];
     private _lastFrameDetected: XRPlaneSet = new Set();
     private _observerTracked: Nullable<Observer<XRFrame>>;

+ 3 - 1
src/Cameras/XR/features/index.ts

@@ -1,4 +1,6 @@
 export * from "./WebXRHitTestLegacy";
 export * from "./WebXRAnchorSystem";
 export * from "./WebXRPlaneDetector";
-export * from "./WebXRBackgroundRemover";
+export * from "./WebXRBackgroundRemover";
+export * from "./WebXRControllerTeleportation";
+export * from "./WebXRControllerPointerSelection";

+ 0 - 2
src/Cameras/XR/index.ts

@@ -2,8 +2,6 @@ export * from "./webXRCamera";
 export * from "./webXREnterExitUI";
 export * from "./webXRExperienceHelper";
 export * from "./webXRInput";
-export * from "./webXRControllerTeleportation";
-export * from "./webXRControllerPointerSelection";
 export * from "./webXRController";
 export * from "./webXRManagedOutputCanvas";
 export * from "./webXRTypes";

+ 16 - 0
src/Cameras/XR/motionController/webXRControllerComponent.ts

@@ -57,6 +57,22 @@ export interface IWebXRMotionControllerComponentChanges {
 export class WebXRControllerComponent implements IDisposable {
 
     /**
+     * Thumbstick component type
+     */
+    public static THUMBSTICK = "xr-standard-thumbstick";
+    /**
+     * Touchpad component type
+     */
+    public static TOUCHPAD = "xr-standard-touchpad";
+    /**
+     * trigger component type
+     */
+    public static TRIGGER = "xr-standard-trigger";
+    /**
+     * squeeze component type
+     */
+    public static SQUEEZE = "xr-standard-squeeze";
+    /**
      * Observers registered here will be triggered when the state of a button changes
      * State change is either pressed / touched / value
      */

+ 16 - 3
src/Cameras/XR/webXRController.ts

@@ -5,8 +5,11 @@ import { Ray } from '../../Culling/ray';
 import { Scene } from '../../scene';
 import { WebXRAbstractMotionController } from './motionController/webXRAbstractController';
 import { WebXRMotionControllerManager } from './motionController/webXRMotionControllerManager';
+
+let idCount = 0;
+
 /**
- * Represents an XR input
+ * Represents an XR controller
  */
 export class WebXRController {
     /**
@@ -34,6 +37,7 @@ export class WebXRController {
     private _tmpQuaternion = new Quaternion();
     private _tmpVector = new Vector3();
 
+    private _uniqueId: string;
     /**
      * Creates the controller
      * @see https://doc.babylonjs.com/how_to/webxr
@@ -46,6 +50,8 @@ export class WebXRController {
         /** The underlying input source for the controller  */
         public inputSource: XRInputSource,
         controllerProfile?: string) {
+        this._uniqueId = `${idCount++}-${inputSource.targetRayMode}-${inputSource.handedness}`;
+
         this.pointer = new AbstractMesh("controllerPointer", scene);
         this.pointer.rotationQuaternion = new Quaternion();
 
@@ -65,6 +71,13 @@ export class WebXRController {
     }
 
     /**
+     * Get this controllers unique id
+     */
+    public get uniqueId() {
+        return this._uniqueId;
+    }
+
+    /**
      * Updates the controller pose based on the given XRFrame
      * @param xrFrame xr frame to update the pose with
      * @param referenceSpace reference space to use
@@ -108,11 +121,11 @@ export class WebXRController {
      */
     public getWorldPointerRayToRef(result: Ray) {
         // Force update to ensure picked point is synced with ray
-        let worldMatrix = this.pointer.computeWorldMatrix(true);
+        let worldMatrix = this.pointer.computeWorldMatrix();
         worldMatrix.decompose(undefined, this._tmpQuaternion, undefined);
         this._tmpVector.set(0, 0, 1);
         this._tmpVector.rotateByQuaternionToRef(this._tmpQuaternion, this._tmpVector);
-        result.origin = this.pointer.absolutePosition;
+        result.origin.copyFrom(this.pointer.absolutePosition);
         result.direction.copyFrom(this._tmpVector);
         result.length = 1000;
     }

+ 0 - 139
src/Cameras/XR/webXRControllerPointerSelection.ts

@@ -1,139 +0,0 @@
-import { Nullable } from "../../types";
-import { Vector3 } from '../../Maths/math.vector';
-import { Mesh } from '../../Meshes/mesh';
-import { Ray } from '../../Culling/ray';
-import { StandardMaterial } from '../../Materials/standardMaterial';
-import { WebXRInput } from './webXRInput';
-import { Color3 } from '../../Maths/math.color';
-import { Axis } from '../../Maths/math.axis';
-
-/**
- * Handles pointer input automatically for the pointer of XR controllers
- */
-export class WebXRControllerPointerSelection {
-    private static _idCounter = 0;
-    private _tmpRay = new Ray(new Vector3(), new Vector3());
-
-    /**
-     * Creates a WebXRControllerPointerSelection
-     * @param input input manager to setup pointer selection
-     */
-    constructor(input: WebXRInput) {
-        input.onControllerAddedObservable.add((controller) => {
-            let scene = controller.pointer.getScene();
-
-            let laserPointer: Mesh;
-            let cursorMesh: Mesh;
-            let triggerDown = false;
-            let id: number;
-            id = WebXRControllerPointerSelection._idCounter++;
-
-            // Create a laser pointer for the XR controller
-            laserPointer = Mesh.CreateCylinder("laserPointer", 1, 0.0002, 0.004, 20, 1, scene, false);
-            laserPointer.parent = controller.pointer;
-            let laserPointerMaterial = new StandardMaterial("laserPointerMat", scene);
-            laserPointerMaterial.emissiveColor = new Color3(0.7, 0.7, 0.7);
-            laserPointerMaterial.alpha = 0.6;
-            laserPointer.material = laserPointerMaterial;
-            laserPointer.rotation.x = Math.PI / 2;
-            this._updatePointerDistance(laserPointer, 1);
-            laserPointer.isPickable = false;
-
-            // Create a gaze tracker for the  XR controller
-            cursorMesh = Mesh.CreateTorus("gazeTracker", 0.0035 * 3, 0.0025 * 3, 20, scene, false);
-            cursorMesh.bakeCurrentTransformIntoVertices();
-            cursorMesh.isPickable = false;
-            cursorMesh.isVisible = false;
-            let targetMat = new StandardMaterial("targetMat", scene);
-            targetMat.specularColor = Color3.Black();
-            targetMat.emissiveColor = new Color3(0.7, 0.7, 0.7);
-            targetMat.backFaceCulling = false;
-            cursorMesh.material = targetMat;
-
-            let renderObserver = scene.onBeforeRenderObservable.add(() => {
-                // Every frame check collisions/input
-                controller.getWorldPointerRayToRef(this._tmpRay);
-                let pick = scene.pickWithRay(this._tmpRay);
-                if (pick) {
-                    let pressed = false;
-                    if (controller.inputSource.gamepad) {
-                        if (controller.inputSource.gamepad.buttons[0] && controller.inputSource.gamepad.buttons[0].value > 0.7) {
-                            pressed = true;
-                        } else if (controller.inputSource.gamepad.buttons[1] && controller.inputSource.gamepad.buttons[1].pressed) {
-                            pressed = true;
-                        }
-                    }
-                    // in screen mode - means finger is on the screen
-                    if (controller.inputSource.targetRayMode === 'screen') {
-                        pressed = true;
-                    }
-                    if (pressed) {
-                        if (!triggerDown) {
-                            scene.simulatePointerDown(pick, { pointerId: id });
-                        }
-                        triggerDown = true;
-                    } else {
-                        if (triggerDown) {
-                            scene.simulatePointerUp(pick, { pointerId: id });
-                        }
-                        triggerDown = false;
-                    }
-                    scene.simulatePointerMove(pick, { pointerId: id });
-                }
-
-                if (pick && pick.pickedPoint && pick.hit) {
-                    // Update laser state
-                    this._updatePointerDistance(laserPointer, pick.distance);
-
-                    // Update cursor state
-                    cursorMesh.position.copyFrom(pick.pickedPoint);
-                    cursorMesh.scaling.x = Math.sqrt(pick.distance);
-                    cursorMesh.scaling.y = Math.sqrt(pick.distance);
-                    cursorMesh.scaling.z = Math.sqrt(pick.distance);
-
-                    // To avoid z-fighting
-                    let pickNormal = this._convertNormalToDirectionOfRay(pick.getNormal(), this._tmpRay);
-                    let deltaFighting = 0.002;
-                    cursorMesh.position.copyFrom(pick.pickedPoint);
-                    if (pickNormal) {
-                        let axis1 = Vector3.Cross(Axis.Y, pickNormal);
-                        let axis2 = Vector3.Cross(pickNormal, axis1);
-                        Vector3.RotationFromAxisToRef(axis2, pickNormal, axis1, cursorMesh.rotation);
-                        cursorMesh.position.addInPlace(pickNormal.scale(deltaFighting));
-                    }
-                    cursorMesh.isVisible = true;
-                } else {
-                    cursorMesh.isVisible = false;
-                }
-            });
-
-            controller.onDisposeObservable.addOnce(() => {
-                laserPointer.dispose();
-                cursorMesh.dispose();
-                if (controller.inputSource.targetRayMode === 'screen') {
-                    controller.getWorldPointerRayToRef(this._tmpRay);
-                    let pick = scene.pickWithRay(this._tmpRay);
-                    if (pick) {
-                        scene.simulatePointerUp(pick, { pointerId: id });
-                    }
-                }
-                scene.onBeforeRenderObservable.remove(renderObserver);
-            });
-        });
-    }
-
-    private _convertNormalToDirectionOfRay(normal: Nullable<Vector3>, ray: Ray) {
-        if (normal) {
-            let angle = Math.acos(Vector3.Dot(normal, ray.direction));
-            if (angle < Math.PI / 2) {
-                normal.scaleInPlace(-1);
-            }
-        }
-        return normal;
-    }
-
-    private _updatePointerDistance(_laserPointer: Mesh, distance: number = 100) {
-        _laserPointer.scaling.y = distance;
-        _laserPointer.position.z = distance / 2;
-    }
-}

+ 0 - 189
src/Cameras/XR/webXRControllerTeleportation.ts

@@ -1,189 +0,0 @@
-import { AbstractMesh } from "../../Meshes/abstractMesh";
-import { Quaternion, Vector3 } from '../../Maths/math.vector';
-import { Mesh } from '../../Meshes/mesh';
-import { Ray } from '../../Culling/ray';
-import { StandardMaterial } from '../../Materials/standardMaterial';
-import { DynamicTexture } from '../../Materials/Textures/dynamicTexture';
-import { EasingFunction, SineEase } from '../../Animations/easing';
-import { Animation } from '../../Animations/animation';
-import { WebXRInput } from './webXRInput';
-
-/**
- * Enables teleportation
- */
-export class WebXRControllerTeleportation {
-    private _teleportationFillColor: string = "#444444";
-    private _teleportationBorderColor: string = "#FFFFFF";
-
-    private _tmpRay = new Ray(new Vector3(), new Vector3());
-    private _tmpVector = new Vector3();
-
-    /**
-     * when set to true (default) teleportation will wait for thumbstick changes.
-     * When set to false teleportation will be disabled.
-     *
-     * If set to false while teleporting results can be unexpected.
-     */
-    public enabled: boolean = true;
-
-    /**
-     * Creates a WebXRControllerTeleportation
-     * @param input input manager to add teleportation to
-     * @param floorMeshes floormeshes which can be teleported to
-     */
-    constructor(input: WebXRInput, floorMeshes: Array<AbstractMesh> = []) {
-        input.onControllerAddedObservable.add((c) => {
-            let scene = c.pointer.getScene();
-
-            let forwardReadyToTeleport = false;
-            let backwardReadyToTeleport = false;
-            let leftReadyToTeleport = false;
-            let rightReadyToTeleport = false;
-
-            // Teleport target abd it's animation
-            let teleportationTarget = Mesh.CreateGround("teleportationTarget", 2, 2, 2, scene);
-            teleportationTarget.isPickable = false;
-            let length = 512;
-            let dynamicTexture = new DynamicTexture("DynamicTexture", length, scene, true);
-            dynamicTexture.hasAlpha = true;
-            let context = dynamicTexture.getContext();
-            let centerX = length / 2;
-            let centerY = length / 2;
-            let radius = 200;
-            context.beginPath();
-            context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
-            context.fillStyle = this._teleportationFillColor;
-            context.fill();
-            context.lineWidth = 10;
-            context.strokeStyle = this._teleportationBorderColor;
-            context.stroke();
-            context.closePath();
-            dynamicTexture.update();
-            let teleportationCircleMaterial = new StandardMaterial("TextPlaneMaterial", scene);
-            teleportationCircleMaterial.diffuseTexture = dynamicTexture;
-            teleportationTarget.material = teleportationCircleMaterial;
-            let torus = Mesh.CreateTorus("torusTeleportation", 0.75, 0.1, 25, scene, false);
-            torus.isPickable = false;
-            torus.parent = teleportationTarget;
-            let animationInnerCircle = new Animation("animationInnerCircle", "position.y", 30, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CYCLE);
-            let keys = [];
-            keys.push({
-                frame: 0,
-                value: 0
-            });
-            keys.push({
-                frame: 30,
-                value: 0.4
-            });
-            keys.push({
-                frame: 60,
-                value: 0
-            });
-            animationInnerCircle.setKeys(keys);
-            let easingFunction = new SineEase();
-            easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);
-            animationInnerCircle.setEasingFunction(easingFunction);
-            torus.animations = [];
-            torus.animations.push(animationInnerCircle);
-            scene.beginAnimation(torus, 0, 60, true);
-
-            // Handle user input on every frame
-            let renderObserver = scene.onBeforeRenderObservable.add(() => {
-                if (!this.enabled) {
-                    return;
-                }
-                // Move the teleportationTarget to where the user is targeting to teleport to
-                if (forwardReadyToTeleport) {
-                    c.getWorldPointerRayToRef(this._tmpRay);
-                    let pick = scene.pickWithRay(this._tmpRay, (o) => {
-                        return floorMeshes.indexOf(o) !== -1;
-                    });
-                    if (pick && pick.pickedPoint) {
-                        // To avoid z-fighting
-                        teleportationTarget.position.copyFrom(pick.pickedPoint);
-                        teleportationTarget.position.y += 0.002;
-                    }
-                    teleportationTarget.isVisible = true;
-                    (<Mesh>teleportationTarget.getChildren()[0]).isVisible = true;
-                } else {
-                    teleportationTarget.isVisible = false;
-                    (<Mesh>teleportationTarget.getChildren()[0]).isVisible = false;
-                }
-
-                if (c.inputSource.gamepad) {
-                    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[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.xrCamera.position.y;
-                                input.xrCamera.position.copyFrom(this._tmpVector);
-                            }
-                            forwardReadyToTeleport = false;
-                        }
-
-                        // Backward teleportation
-                        if (c.inputSource.gamepad.axes[yIndex] > 0.7) {
-                            backwardReadyToTeleport = true;
-                        } else {
-                            if (backwardReadyToTeleport) {
-                                this._tmpVector.set(0, 0, -1);
-                                this._tmpVector.rotateByQuaternionToRef(input.xrCamera.rotationQuaternion, this._tmpVector);
-                                this._tmpVector.y = 0;
-                                this._tmpVector.normalize();
-                                this._tmpVector.y = -1.5;
-                                this._tmpVector.normalize();
-                                this._tmpRay.direction.copyFrom(this._tmpVector);
-                                let pick = scene.pickWithRay(this._tmpRay, (o) => {
-                                    return floorMeshes.indexOf(o) !== -1;
-                                });
-
-                                if (pick && pick.pickedPoint) {
-                                    // Teleport the users feet to where they targeted
-                                    this._tmpVector.copyFrom(pick.pickedPoint);
-                                    input.xrCamera.position.addInPlace(this._tmpVector);
-                                }
-                            }
-                            backwardReadyToTeleport = false;
-                        }
-                    }
-
-                    if (c.inputSource.gamepad.axes[xIndex] !== undefined) {
-                        if (c.inputSource.gamepad.axes[xIndex] < -0.7) {
-                            leftReadyToTeleport = true;
-                        } else {
-                            if (leftReadyToTeleport) {
-                                input.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, -Math.PI / 4, 0));
-                            }
-                            leftReadyToTeleport = false;
-                        }
-                        if (c.inputSource.gamepad.axes[xIndex] > 0.7) {
-                            rightReadyToTeleport = true;
-                        } else {
-                            if (rightReadyToTeleport) {
-                                input.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, Math.PI / 4, 0));
-                            }
-                            rightReadyToTeleport = false;
-                        }
-                    }
-
-                }
-            });
-
-            c.onDisposeObservable.addOnce(() => {
-                teleportationTarget.dispose();
-                dynamicTexture.dispose();
-                teleportationCircleMaterial.dispose();
-                torus.dispose();
-
-                scene.onBeforeRenderObservable.remove(renderObserver);
-            });
-        });
-    }
-
-}

+ 11 - 5
src/Cameras/XR/webXRDefaultExperience.ts

@@ -1,12 +1,12 @@
 import { WebXRExperienceHelper } from "./webXRExperienceHelper";
 import { Scene } from '../../scene';
 import { WebXRInput, IWebXRInputOptions } from './webXRInput';
-import { WebXRControllerPointerSelection } from './webXRControllerPointerSelection';
-import { WebXRControllerTeleportation } from './webXRControllerTeleportation';
+import { WebXRControllerPointerSelection } from './features/WebXRControllerPointerSelection';
 import { WebXRRenderTarget } from './webXRTypes';
 import { WebXREnterExitUI, WebXREnterExitUIOptions } from './webXREnterExitUI';
 import { AbstractMesh } from '../../Meshes/abstractMesh';
 import { WebXRManagedOutputCanvasOptions } from './webXRManagedOutputCanvas';
+import { WebXRMotionControllerTeleportation } from './features/WebXRControllerTeleportation';
 
 /**
  * Options for the default xr helper
@@ -57,7 +57,7 @@ export class WebXRDefaultExperience {
     /**
      * Enables teleportation
      */
-    public teleportation: WebXRControllerTeleportation;
+    public teleportation: WebXRMotionControllerTeleportation;
     /**
      * Enables ui for entering/exiting xr
      */
@@ -82,10 +82,16 @@ export class WebXRDefaultExperience {
 
             // Add controller support
             result.input = new WebXRInput(xrHelper.sessionManager, xrHelper.camera, options.inputOptions);
-            result.pointerSelection = new WebXRControllerPointerSelection(result.input);
+            result.pointerSelection = <WebXRControllerPointerSelection>result.baseExperience.featuresManager.enableFeature(WebXRControllerPointerSelection.Name, "latest", {
+                xrInput: result.input
+            });
 
             if (options.floorMeshes) {
-                result.teleportation = new WebXRControllerTeleportation(result.input, options.floorMeshes);
+                result.teleportation = <WebXRMotionControllerTeleportation>result.baseExperience.featuresManager.enableFeature(WebXRMotionControllerTeleportation.Name, "latest", {
+                    floorMeshes: options.floorMeshes,
+                    xrInput: result.input
+                });
+                result.teleportation.setSelectionFeature(result.pointerSelection);
             }
 
             // Create the WebXR output target

+ 10 - 10
src/Cameras/XR/webXRFeaturesManager.ts

@@ -6,6 +6,10 @@ import { IDisposable } from '../../scene';
  */
 export interface IWebXRFeature extends IDisposable {
     /**
+     * Is this feature attached
+     */
+    attached: boolean;
+    /**
      * Attach the feature to the session
      * Will usually be called by the features manager
      *
@@ -121,8 +125,7 @@ export class WebXRFeaturesManager implements IDisposable {
         [name: string]: {
             featureImplementation: IWebXRFeature,
             version: number,
-            enabled: boolean,
-            attached: boolean
+            enabled: boolean
         }
     } = {};
 
@@ -136,7 +139,7 @@ export class WebXRFeaturesManager implements IDisposable {
         this._xrSessionManager.onXRSessionInit.add(() => {
             this.getEnabledFeatures().forEach((featureName) => {
                 const feature = this._features[featureName];
-                if (feature.enabled && !feature.attached) {
+                if (feature.enabled && !feature.featureImplementation.attached) {
                     this.attachFeature(featureName);
                 }
             });
@@ -146,7 +149,7 @@ export class WebXRFeaturesManager implements IDisposable {
         this._xrSessionManager.onXRSessionEnded.add(() => {
             this.getEnabledFeatures().forEach((featureName) => {
                 const feature = this._features[featureName];
-                if (feature.enabled && feature.attached) {
+                if (feature.enabled && feature.featureImplementation.attached) {
                     // detach, but don't disable!
                     this.detachFeature(featureName);
                 }
@@ -193,7 +196,6 @@ export class WebXRFeaturesManager implements IDisposable {
 
             this._features[name] = {
                 featureImplementation: constructFunction(),
-                attached: false,
                 enabled: true,
                 version: versionToLoad
             };
@@ -203,7 +205,7 @@ export class WebXRFeaturesManager implements IDisposable {
         }
 
         // if session started already, request and enable
-        if (this._xrSessionManager.session && !feature.attached && attachIfPossible) {
+        if (this._xrSessionManager.session && !feature.featureImplementation.attached && attachIfPossible) {
             // enable feature
             this.attachFeature(name);
         }
@@ -234,9 +236,8 @@ export class WebXRFeaturesManager implements IDisposable {
      */
     public attachFeature(featureName: string) {
         const feature = this._features[featureName];
-        if (feature && feature.enabled && !feature.attached) {
+        if (feature && feature.enabled && !feature.featureImplementation.attached) {
             feature.featureImplementation.attach();
-            feature.attached = true;
         }
     }
 
@@ -246,9 +247,8 @@ export class WebXRFeaturesManager implements IDisposable {
      */
     public detachFeature(featureName: string) {
         const feature = this._features[featureName];
-        if (feature && feature.attached) {
+        if (feature && feature.featureImplementation.attached) {
             feature.featureImplementation.detach();
-            feature.attached = false;
         }
     }
 

+ 16 - 1
src/Cameras/XR/webXRSessionManager.ts

@@ -42,6 +42,11 @@ export class WebXRSessionManager implements IDisposable {
     public onXRSessionInit: Observable<XRSession> = new Observable<XRSession>();
 
     /**
+     * Fires when the reference space changed
+     */
+    public onXRReferenceSpaceChanged: Observable<XRReferenceSpace> = new Observable();
+
+    /**
      * Underlying xr session
      */
     public session: XRSession;
@@ -52,13 +57,23 @@ export class WebXRSessionManager implements IDisposable {
      */
     public viewerReferenceSpace: XRReferenceSpace;
 
+    private _referenceSpace: 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;
+    public get referenceSpace(): XRReferenceSpace {
+        return this._referenceSpace;
+    }
 
     /**
+     * Set a new reference space and triggers the observable
+     */
+    public set referenceSpace(newReferenceSpace: XRReferenceSpace) {
+        this._referenceSpace = newReferenceSpace;
+        this.onXRReferenceSpaceChanged.notifyObservers(this._referenceSpace);
+    }
+    /**
      * The base reference space from which the session started. good if you want to reset your
      * reference space
      */

+ 0 - 25
src/Shaders/ShadersInclude/shadowsFragmentFunctions.fx

@@ -120,11 +120,6 @@
             vec2 uv = 0.5 * clipSpace.xy + vec2(0.5);
             vec3 uvLayer = vec3(uv.x, uv.y, layer);
 
-            if (uv.x < 0. || uv.x > 1.0 || uv.y < 0. || uv.y > 1.0)
-            {
-                return 1.0;
-            }
-
             float shadowPixelDepth = clamp(depthMetric, 0., 1.0);
 
             #ifndef SHADOWFLOAT
@@ -258,11 +253,6 @@
             vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
             vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
 
-            if (uvDepth.x < 0. || uvDepth.x > 1.0 || uvDepth.y < 0. || uvDepth.y > 1.0)
-            {
-                return 1.0;
-            }
-
             uvDepth.z = clamp(uvDepth.z, 0., GREATEST_LESS_THAN_ONE);
 
             vec4 uvDepthLayer = vec4(uvDepth.x, uvDepth.y, layer, uvDepth.z);
@@ -280,11 +270,6 @@
             vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
             vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
 
-            if (uvDepth.x < 0. || uvDepth.x > 1.0 || uvDepth.y < 0. || uvDepth.y > 1.0)
-            {
-                return 1.0;
-            }
-
             uvDepth.z = clamp(uvDepth.z, 0., GREATEST_LESS_THAN_ONE);
 
             vec2 uv = uvDepth.xy * shadowMapSizeAndInverse.x;	// uv in texel units
@@ -321,11 +306,6 @@
             vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
             vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
 
-            if (uvDepth.x < 0. || uvDepth.x > 1.0 || uvDepth.y < 0. || uvDepth.y > 1.0)
-            {
-                return 1.0;
-            }
-
             uvDepth.z = clamp(uvDepth.z, 0., GREATEST_LESS_THAN_ONE);
 
             vec2 uv = uvDepth.xy * shadowMapSizeAndInverse.x;	// uv in texel units
@@ -600,11 +580,6 @@
             vec3 clipSpace = vPositionFromLight.xyz / vPositionFromLight.w;
             vec3 uvDepth = vec3(0.5 * clipSpace.xyz + vec3(0.5));
 
-            if (uvDepth.x < 0. || uvDepth.x > 1.0 || uvDepth.y < 0. || uvDepth.y > 1.0)
-            {
-                return 1.0;
-            }
-
             uvDepth.z = clamp(uvDepth.z, 0., GREATEST_LESS_THAN_ONE);
 
             vec4 uvDepthLayer = vec4(uvDepth.x, uvDepth.y, layer, uvDepth.z);