Browse Source

Merge pull request #7361 from RaananW/xr-model-loading

WebXR Motion Controllers
David Catuhe 5 years ago
parent
commit
f6550057ac

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

@@ -182,6 +182,7 @@
 - New Features Manager for WebXR features ([RaananW](https://github.com/RaananW/))
 - New features - Plane detection, Hit Test, Background remover ([RaananW](https://github.com/RaananW/))
 - Camera's API works as expected (position, rotationQuaternion, world matrix etc') ([#7239](https://github.com/BabylonJS/Babylon.js/issues/7239)) ([RaananW](https://github.com/RaananW/))
+- 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/))
 
 ### Ray
 

+ 12 - 12
src/Cameras/VR/vrExperienceHelper.ts

@@ -844,18 +844,18 @@ export class VRExperienceHelper {
                         });
 
                         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._leftController.webVRController != webVRController)) {
-                                    this._rightController = localController;
-                                } else {
-                                    this._leftController = localController;
-                                }
-
-                                this._tryEnableInteractionOnController(localController);
-                            }
+                            // 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 {

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

@@ -4,11 +4,11 @@ export * from "./webXRExperienceHelper";
 export * from "./webXRInput";
 export * from "./webXRControllerTeleportation";
 export * from "./webXRControllerPointerSelection";
-export * from "./webXRControllerModelLoader";
 export * from "./webXRController";
 export * from "./webXRManagedOutputCanvas";
 export * from "./webXRTypes";
 export * from "./webXRSessionManager";
 export * from "./webXRDefaultExperience";
 export * from "./webXRFeaturesManager";
-export * from "./features/index";
+export * from "./features/index";
+export * from "./motionController/index";

+ 7 - 0
src/Cameras/XR/motionController/index.ts

@@ -0,0 +1,7 @@
+export * from "./webXRAbstractController";
+export * from "./webXRControllerComponent";
+export * from "./webXRGenericMotionController";
+export * from "./webXRMicrosoftMixedRealityController";
+export * from "./webXRMotionControllerManager";
+export * from "./webXROculusTouchMotionController";
+export * from "./webXRHTCViveMotionController";

+ 450 - 0
src/Cameras/XR/motionController/webXRAbstractController.ts

@@ -0,0 +1,450 @@
+import { IDisposable, Scene } from '../../../scene';
+import { WebXRControllerComponent } from './webXRControllerComponent';
+import { Observable } from '../../../Misc/observable';
+import { Logger } from '../../../Misc/logger';
+import { SceneLoader } from '../../../Loading/sceneLoader';
+import { AbstractMesh } from '../../../Meshes/abstractMesh';
+import { Nullable } from '../../../types';
+import { Quaternion, Vector3 } from '../../../Maths/math.vector';
+import { Mesh } from '../../../Meshes/mesh';
+
+/**
+ * Handness type in xrInput profiles. These can be used to define layouts in the Layout Map.
+ */
+export type MotionControllerHandness = "none" | "left" | "right" | "left-right" | "left-right-none";
+/**
+ * The type of components available in motion controllers.
+ * This is not the name of the component.
+ */
+export type MotionControllerComponentType = "trigger" | "squeeze" | "touchpad" | "thumbstick" | "button";
+
+/**
+ * The schema of motion controller layout.
+ * No object will be initialized using this interface
+ * This is used just to define the profile.
+ */
+export interface IMotionControllerLayout {
+    /**
+     * Defines the main button component id
+     */
+    selectComponentId: string;
+    /**
+     * Available components (unsorted)
+     */
+    components: {
+        /**
+         * A map of component Ids
+         */
+        [componentId: string]: {
+            /**
+             * The type of input the component outputs
+             */
+            type: MotionControllerComponentType;
+        }
+    };
+    /**
+     * An optional gamepad object. If no gamepad object is not defined, no models will be loaded
+     */
+    gamepad?: {
+        /**
+         * Is the mapping based on the xr-standard defined here:
+         * https://www.w3.org/TR/webxr-gamepads-module-1/#xr-standard-gamepad-mapping
+         */
+        mapping: "" | "xr-standard";
+        /**
+         * The buttons available in this input in the right order
+         * index of this button will be the index in the gamepadObject.buttons array
+         * correlates to the componentId in components
+         */
+        buttons: Array<string | null>;
+        /**
+         * Definition of the axes of the gamepad input, sorted
+         * Correlates to componentIds in the components map
+         */
+        axes: Array<{
+            /**
+             * The component id that the axis correlates to
+             */
+            componentId: string;
+            /**
+             * X or Y Axis
+             */
+            axis: "x-axis" | "y-axis";
+        } | null>;
+    };
+}
+
+/**
+ * A definition for the layout map in the input profile
+ */
+export interface IMotionControllerLayoutMap {
+    /**
+     * Layouts with handness type as a key
+     */
+    [handness: string /* handness */]: IMotionControllerLayout;
+}
+
+/**
+ * The XR Input profile schema
+ * Profiles can be found here:
+ * https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/registry/profiles
+ */
+export interface IMotionControllerProfile {
+    /**
+     * The id of this profile
+     * correlates to the profile(s) in the xrInput.profiles array
+     */
+    profileId: string;
+    /**
+     * fallback profiles for this profileId
+     */
+    fallbackProfileIds: string[];
+    /**
+     * The layout map, with handness as key
+     */
+    layouts: IMotionControllerLayoutMap;
+}
+
+/**
+ * A helper-interface for the 3 meshes needed for controller button animation
+ * The meshes are provided to the _lerpButtonTransform function to calculate the current position of the value mesh
+ */
+export interface IMotionControllerButtonMeshMap {
+    /**
+     * The mesh that will be changed when value changes
+     */
+    valueMesh: AbstractMesh;
+    /**
+     * the mesh that defines the pressed value mesh position.
+     * This is used to find the max-position of this button
+     */
+    pressedMesh: AbstractMesh;
+    /**
+     * the mesh that defines the unpressed value mesh position.
+     * This is used to find the min (or initial) position of this button
+     */
+    unpressedMesh: AbstractMesh;
+}
+
+/**
+ * A helper-interface for the 3 meshes needed for controller axis animation.
+ * This will be expanded when touchpad animations are fully supported
+ * The meshes are provided to the _lerpAxisTransform function to calculate the current position of the value mesh
+ */
+export interface IMotionControllerAxisMeshMap {
+    /**
+     * The mesh that will be changed when axis value changes
+     */
+    valueMesh: AbstractMesh;
+    /**
+     * the mesh that defines the minimum value mesh position.
+     */
+    minMesh: AbstractMesh;
+    /**
+     * the mesh that defines the maximum value mesh position.
+     */
+    maxMesh: AbstractMesh;
+}
+
+/**
+ * The elements needed for change-detection of the gamepad objects in motion controllers
+ */
+export interface IMinimalMotionControllerObject {
+    /**
+     * An array of available buttons
+     */
+    buttons: Array<{
+        /**
+        * Value of the button/trigger
+        */
+        value: number;
+        /**
+         * If the button/trigger is currently touched
+         */
+        touched: boolean;
+        /**
+         * If the button/trigger is currently pressed
+         */
+        pressed: boolean;
+    }>;
+    /**
+     * Available axes of this controller
+     */
+    axes: number[];
+}
+
+/**
+ * An Abstract Motion controller
+ * This class receives an xrInput and a profile layout and uses those to initialize the components
+ * Each component has an observable to check for changes in value and state
+ */
+export abstract class WebXRAbstractMotionController implements IDisposable {
+
+    /**
+     * Component type map
+     */
+    public static ComponentType = {
+        TRIGGER: "trigger",
+        SQUEEZE: "squeeze",
+        TOUCHPAD: "touchpad",
+        THUMBSTICK: "thumbstick",
+        BUTTON: "button"
+    };
+
+    /**
+     * The profile id of this motion controller
+     */
+    public abstract profileId: string;
+
+    /**
+     * A map of components (WebXRControllerComponent) in this motion controller
+     * Components have a ComponentType and can also have both button and axis definitions
+     */
+    public readonly components: {
+        [id: string]: WebXRControllerComponent
+    } = {};
+
+    /**
+     * Observers registered here will be triggered when the model of this controller is done loading
+     */
+    public onModelLoadedObservable: Observable<WebXRAbstractMotionController> = new Observable();
+
+    /**
+     * The root mesh of the model. It is null if the model was not yet initialized
+     */
+    public rootMesh: Nullable<AbstractMesh>;
+
+    private _modelReady: boolean = false;
+
+    /**
+     * constructs a new abstract motion controller
+     * @param scene the scene to which the model of the controller will be added
+     * @param layout The profile layout to load
+     * @param gamepadObject The gamepad object correlating to this controller
+     * @param handness handness (left/right/none) of this controller
+     * @param _doNotLoadControllerMesh set this flag to ignore the mesh loading
+     */
+    constructor(protected scene: Scene, protected layout: IMotionControllerLayout,
+        /**
+         * The gamepad object correlating to this controller
+         */
+        public gamepadObject: IMinimalMotionControllerObject,
+        /**
+         * handness (left/right/none) of this controller
+         */
+        public handness: MotionControllerHandness,
+        _doNotLoadControllerMesh: boolean = false) {
+        // initialize the components
+        if (layout.gamepad) {
+            layout.gamepad.buttons.forEach(this._initComponent);
+        }
+        // Model is loaded in WebXRInput
+    }
+
+    private _initComponent = (id: string | null) => {
+        if (!this.layout.gamepad || !id) { return; }
+        const type = this.layout.components[id].type;
+        const buttonIndex = this.layout.gamepad.buttons.indexOf(id);
+        // search for axes
+        let axes: number[] = [];
+        this.layout.gamepad.axes.forEach((axis, index) => {
+            if (axis && axis.componentId === id) {
+                if (axis.axis === "x-axis") {
+                    axes[0] = index;
+                } else {
+                    axes[1] = index;
+                }
+            }
+        });
+        this.components[id] = new WebXRControllerComponent(id, type, buttonIndex, axes);
+    }
+
+    /**
+     * Update this model using the current XRFrame
+     * @param xrFrame the current xr frame to use and update the model
+     */
+    public updateFromXRFrame(xrFrame: XRFrame): void {
+        this.getComponentTypes().forEach((id) => this.getComponent(id).update(this.gamepadObject));
+        this.updateModel(xrFrame);
+    }
+
+    /**
+     * Get the list of components available in this motion controller
+     * @returns an array of strings correlating to available components
+     */
+    public getComponentTypes(): string[] {
+        return Object.keys(this.components);
+    }
+
+    /**
+     * Get the main (Select) component of this controller as defined in the layout
+     * @returns the main component of this controller
+     */
+    public getMainComponent(): WebXRControllerComponent {
+        return this.getComponent(this.layout.selectComponentId);
+    }
+
+    /**
+     * get a component based an its component id as defined in layout.components
+     * @param id the id of the component
+     * @returns the component correlates to the id or undefined if not found
+     */
+    public getComponent(id: string): WebXRControllerComponent {
+        return this.components[id];
+    }
+
+    /**
+     * Loads the model correlating to this controller
+     * When the mesh is loaded, the onModelLoadedObservable will be triggered
+     * @returns A promise fulfilled with the result of the model loading
+     */
+    public async loadModel(): Promise<boolean> {
+        let useGeneric = !this._getModelLoadingConstraints();
+        let loadingParams = this._getGenericFilenameAndPath();
+        // Checking if GLB loader is present
+        if (useGeneric) {
+            Logger.Warn("You need to reference GLTF loader to load Windows Motion Controllers model. Falling back to generic models");
+        } else {
+            loadingParams = this._getFilenameAndPath();
+        }
+        return new Promise((resolve, reject) => {
+            SceneLoader.ImportMesh("", loadingParams.path, loadingParams.filename, this.scene, (meshes: AbstractMesh[]) => {
+                if (useGeneric) {
+                    this._getGenericParentMesh(meshes);
+                } else {
+                    this._setRootMesh(meshes);
+                }
+                this._processLoadedModel(meshes);
+                this._modelReady = true;
+                this.onModelLoadedObservable.notifyObservers(this);
+                resolve(true);
+            }, null, (_scene: Scene, message: string) => {
+                Logger.Log(message);
+                Logger.Warn(`Failed to retrieve controller model of type ${this.profileId} from the remote server: ${loadingParams.path}${loadingParams.filename}`);
+                reject(message);
+            });
+        });
+    }
+
+    /**
+     * Update the model itself with the current frame data
+     * @param xrFrame the frame to use for updating the model mesh
+     */
+    protected updateModel(xrFrame: XRFrame): void {
+        if (!this._modelReady) {
+            return;
+        }
+        this._updateModel(xrFrame);
+    }
+
+    /**
+     * Moves the axis on the controller mesh based on its current state
+     * @param axis the index of the axis
+     * @param axisValue the value of the axis which determines the meshes new position
+     * @hidden
+     */
+    protected _lerpAxisTransform(axisMap: IMotionControllerAxisMeshMap, axisValue: number): void {
+
+        if (!axisMap.minMesh.rotationQuaternion || !axisMap.maxMesh.rotationQuaternion || !axisMap.valueMesh.rotationQuaternion) {
+            return;
+        }
+
+        // Convert from gamepad value range (-1 to +1) to lerp range (0 to 1)
+        let lerpValue = axisValue * 0.5 + 0.5;
+        Quaternion.SlerpToRef(
+            axisMap.minMesh.rotationQuaternion,
+            axisMap.maxMesh.rotationQuaternion,
+            lerpValue,
+            axisMap.valueMesh.rotationQuaternion);
+        Vector3.LerpToRef(
+            axisMap.minMesh.position,
+            axisMap.maxMesh.position,
+            lerpValue,
+            axisMap.valueMesh.position);
+    }
+
+    /**
+     * Moves the buttons on the controller mesh based on their current state
+     * @param buttonName the name of the button to move
+     * @param buttonValue the value of the button which determines the buttons new position
+     */
+    protected _lerpButtonTransform(buttonMap: IMotionControllerButtonMeshMap, buttonValue: number): void {
+
+        if (!buttonMap
+            || !buttonMap.unpressedMesh.rotationQuaternion
+            || !buttonMap.pressedMesh.rotationQuaternion
+            || !buttonMap.valueMesh.rotationQuaternion) {
+            return;
+        }
+
+        Quaternion.SlerpToRef(
+            buttonMap.unpressedMesh.rotationQuaternion,
+            buttonMap.pressedMesh.rotationQuaternion,
+            buttonValue,
+            buttonMap.valueMesh.rotationQuaternion);
+        Vector3.LerpToRef(
+            buttonMap.unpressedMesh.position,
+            buttonMap.pressedMesh.position,
+            buttonValue,
+            buttonMap.valueMesh.position);
+    }
+
+    private _getGenericFilenameAndPath(): { filename: string, path: string } {
+        return {
+            filename: "generic.babylon",
+            path: "https://controllers.babylonjs.com/generic/"
+        };
+    }
+
+    private _getGenericParentMesh(meshes: AbstractMesh[]): void {
+        this.rootMesh = new Mesh(this.profileId + " " + this.handness, this.scene);
+
+        meshes.forEach((mesh) => {
+            if (!mesh.parent) {
+                mesh.isPickable = false;
+                mesh.setParent(this.rootMesh);
+            }
+        });
+
+        this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
+    }
+
+    /**
+     * Get the filename and path for this controller's model
+     * @returns a map of filename and path
+     */
+    protected abstract _getFilenameAndPath(): { filename: string, path: string };
+    /**
+     * This function will be called after the model was successfully loaded and can be used
+     * for mesh transformations before it is available for the user
+     * @param meshes the loaded meshes
+     */
+    protected abstract _processLoadedModel(meshes: AbstractMesh[]): void;
+    /**
+     * Set the root mesh for this controller. Important for the WebXR controller class
+     * @param meshes the loaded meshes
+     */
+    protected abstract _setRootMesh(meshes: AbstractMesh[]): void;
+    /**
+     * A function executed each frame that updates the mesh (if needed)
+     * @param xrFrame the current xrFrame
+     */
+    protected abstract _updateModel(xrFrame: XRFrame): void;
+    /**
+     * This function is called before the mesh is loaded. It checks for loading constraints.
+     * For example, this function can check if the GLB loader is available
+     * If this function returns false, the generic controller will be loaded instead
+     * @returns Is the client ready to load the mesh
+     */
+    protected abstract _getModelLoadingConstraints(): boolean;
+
+    /**
+     * Dispose this controller, the model mesh and all its components
+     */
+    public dispose(): void {
+        this.getComponentTypes().forEach((id) => this.getComponent(id).dispose());
+        if (this.rootMesh) {
+            this.rootMesh.dispose();
+        }
+    }
+}

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

@@ -0,0 +1,241 @@
+import { IMinimalMotionControllerObject, MotionControllerComponentType } from "./webXRAbstractController";
+import { Observable } from '../../../Misc/observable';
+import { IDisposable } from '../../../scene';
+
+/**
+ * X-Y values for axes in WebXR
+ */
+export interface IWebXRMotionControllerAxesValue {
+    /**
+     * The value of the x axis
+     */
+    x: number;
+    /**
+     * The value of the y-axis
+     */
+    y: number;
+}
+
+/**
+ * changed / previous values for the values of this component
+ */
+export interface IWebXRMotionControllerComponentChangesValues<T> {
+    /**
+     * current (this frame) value
+     */
+    current: T;
+    /**
+     * previous (last change) value
+     */
+    previous: T;
+}
+
+/**
+ * Represents changes in the component between current frame and last values recorded
+ */
+export interface IWebXRMotionControllerComponentChanges {
+    /**
+     * will be populated with previous and current values if touched changed
+     */
+    touched?: IWebXRMotionControllerComponentChangesValues<boolean>;
+    /**
+     * will be populated with previous and current values if pressed changed
+     */
+    pressed?: IWebXRMotionControllerComponentChangesValues<boolean>;
+    /**
+     * will be populated with previous and current values if value changed
+     */
+    value?: IWebXRMotionControllerComponentChangesValues<number>;
+    /**
+     * will be populated with previous and current values if axes changed
+     */
+    axes?: IWebXRMotionControllerComponentChangesValues<IWebXRMotionControllerAxesValue>;
+}
+/**
+ * This class represents a single component (for example button or thumbstick) of a motion controller
+ */
+export class WebXRControllerComponent implements IDisposable {
+
+    /**
+     * Observers registered here will be triggered when the state of a button changes
+     * State change is either pressed / touched / value
+     */
+    public onButtonStateChanged: Observable<WebXRControllerComponent> = new Observable();
+    /**
+     * If axes are available for this component (like a touchpad or thumbstick) the observers will be notified when
+     * the axes data changes
+     */
+    public onAxisValueChanged: Observable<{ x: number, y: number }> = new Observable();
+
+    private _currentValue: number = 0;
+    private _touched: boolean = false;
+    private _pressed: boolean = false;
+    private _axes: IWebXRMotionControllerAxesValue = {
+        x: 0,
+        y: 0
+    };
+    private _changes: IWebXRMotionControllerComponentChanges = {};
+
+    /**
+     * Creates a new component for a motion controller.
+     * It is created by the motion controller itself
+     *
+     * @param id the id of this component
+     * @param type the type of the component
+     * @param _buttonIndex index in the buttons array of the gamepad
+     * @param _axesIndices indices of the values in the axes array of the gamepad
+     */
+    constructor(
+        /**
+         * the id of this component
+         */
+        public id: string,
+        /**
+         * the type of the component
+         */
+        public type: MotionControllerComponentType,
+        private _buttonIndex: number = -1,
+        private _axesIndices: number[] = []) {
+
+    }
+
+    /**
+     * Get the current value of this component
+     */
+    public get value(): number {
+        return this._currentValue;
+    }
+
+    /**
+     * is the button currently pressed
+     */
+    public get pressed(): boolean {
+        return this._pressed;
+    }
+
+    /**
+     * is the button currently touched
+     */
+    public get touched(): boolean {
+        return this._touched;
+    }
+
+    /**
+     * The current axes data. If this component has no axes it will still return an object { x: 0, y: 0 }
+     */
+    public get axes(): IWebXRMotionControllerAxesValue {
+        return this._axes;
+    }
+
+    /**
+     * Get the changes. Elements will be populated only if they changed with their previous and current value
+     */
+    public get changes(): IWebXRMotionControllerComponentChanges {
+        return this._changes;
+    }
+
+    /**
+     * Is this component a button (hence - pressable)
+     * @returns true if can be pressed
+     */
+    public isButton(): boolean {
+        return this._buttonIndex !== -1;
+    }
+
+    /**
+     * Are there axes correlating to this component
+     * @return true is axes data is available
+     */
+    public isAxes(): boolean {
+        return this._axesIndices.length !== 0;
+    }
+
+    /**
+     * update this component using the gamepad object it is in. Called on every frame
+     * @param nativeController the native gamepad controller object
+     */
+    public update(nativeController: IMinimalMotionControllerObject) {
+        let buttonUpdated = false;
+        let axesUpdate = false;
+        this._changes = {};
+
+        if (this.isButton()) {
+            const button = nativeController.buttons[this._buttonIndex];
+            if (this._currentValue !== button.value) {
+                this.changes.value = {
+                    current: button.value,
+                    previous: this._currentValue
+                };
+                buttonUpdated = true;
+                this._currentValue = button.value;
+            }
+            if (this._touched !== button.touched) {
+                this.changes.touched = {
+                    current: button.touched,
+                    previous: this._touched
+                };
+                buttonUpdated = true;
+                this._touched = button.touched;
+            }
+            if (this._pressed !== button.pressed) {
+                this.changes.pressed = {
+                    current: button.pressed,
+                    previous: this._pressed
+                };
+                buttonUpdated = true;
+                this._pressed = button.pressed;
+            }
+        }
+
+        if (this.isAxes()) {
+            if (this._axes.x !== nativeController.axes[this._axesIndices[0]]) {
+                this.changes.axes = {
+                    current: {
+                        x: nativeController.axes[this._axesIndices[0]],
+                        y: this._axes.y
+                    },
+                    previous: {
+                        x: this._axes.x,
+                        y: this._axes.y
+                    }
+                };
+                this._axes.x = nativeController.axes[this._axesIndices[0]];
+                axesUpdate = true;
+            }
+
+            if (this._axes.y !== nativeController.axes[this._axesIndices[1]]) {
+                if (this.changes.axes) {
+                    this.changes.axes.current.y = nativeController.axes[this._axesIndices[1]];
+                } else {
+                    this.changes.axes = {
+                        current: {
+                            x: this._axes.x,
+                            y: nativeController.axes[this._axesIndices[1]]
+                        },
+                        previous: {
+                            x: this._axes.x,
+                            y: this._axes.y
+                        }
+                    };
+                }
+                this._axes.y = nativeController.axes[this._axesIndices[1]];
+                axesUpdate = true;
+            }
+        }
+
+        if (buttonUpdated) {
+            this.onButtonStateChanged.notifyObservers(this);
+        }
+        if (axesUpdate) {
+            this.onAxisValueChanged.notifyObservers(this._axes);
+        }
+    }
+
+    /**
+     * Dispose this component
+     */
+    public dispose(): void {
+        this.onAxisValueChanged.clear();
+        this.onButtonStateChanged.clear();
+    }
+}

+ 80 - 0
src/Cameras/XR/motionController/webXRGenericMotionController.ts

@@ -0,0 +1,80 @@
+import {
+    WebXRAbstractMotionController,
+    IMinimalMotionControllerObject,
+    MotionControllerHandness,
+    IMotionControllerLayoutMap
+} from "./webXRAbstractController";
+import { AbstractMesh } from '../../../Meshes/abstractMesh';
+import { Scene } from '../../../scene';
+import { Mesh } from '../../../Meshes/mesh';
+import { Quaternion } from '../../../Maths/math.vector';
+
+// https://github.com/immersive-web/webxr-input-profiles/blob/master/packages/registry/profiles/generic/generic-trigger-touchpad-thumbstick.json
+const GenericTriggerLayout: IMotionControllerLayoutMap = {
+    "left-right-none": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": { "type": "trigger" }
+        },
+        "gamepad": {
+            "mapping": "xr-standard",
+            "buttons": [
+                "xr-standard-trigger"
+            ],
+            "axes": []
+        }
+    }
+
+};
+
+// TODO support all generic models with xr-standard mapping at:
+// https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/registry/profiles/generic
+
+/**
+ * A generic trigger-only motion controller for WebXR
+ */
+export class WebXRGenericTriggerMotionController extends WebXRAbstractMotionController {
+    /**
+     * Static version of the profile id of this controller
+     */
+    public static ProfileId = "generic-trigger";
+
+    public profileId = WebXRGenericTriggerMotionController.ProfileId;
+
+    constructor(scene: Scene, gamepadObject: IMinimalMotionControllerObject, handness: MotionControllerHandness) {
+        super(scene, GenericTriggerLayout["left-right-none"], gamepadObject, handness);
+    }
+
+    protected _processLoadedModel(meshes: AbstractMesh[]): void {
+        // nothing to do
+    }
+
+    protected _updateModel(): void {
+        // no-op
+    }
+
+    protected _getFilenameAndPath(): { filename: string; path: string; } {
+        return {
+            filename: "generic.babylon",
+            path: "https://controllers.babylonjs.com/generic/"
+        };
+    }
+
+    protected _setRootMesh(meshes: AbstractMesh[]): void {
+        this.rootMesh = new Mesh(this.profileId + " " + this.handness, this.scene);
+
+        meshes.forEach((mesh) => {
+            mesh.isPickable = false;
+            if (!mesh.parent) {
+                mesh.setParent(this.rootMesh);
+            }
+        });
+
+        this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
+    }
+
+    protected _getModelLoadingConstraints(): boolean {
+        return true;
+    }
+
+}

+ 154 - 0
src/Cameras/XR/motionController/webXRHTCViveMotionController.ts

@@ -0,0 +1,154 @@
+import {
+    IMotionControllerLayoutMap,
+    IMinimalMotionControllerObject,
+    MotionControllerHandness,
+    WebXRAbstractMotionController
+} from "./webXRAbstractController";
+import { Scene } from '../../../scene';
+import { AbstractMesh } from '../../../Meshes/abstractMesh';
+import { Mesh } from '../../../Meshes/mesh';
+import { Quaternion } from '../../../Maths/math.vector';
+import { WebXRMotionControllerManager } from './webXRMotionControllerManager';
+
+const HTCViveLayout: IMotionControllerLayoutMap = {
+    "left-right-none": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": { "type": "trigger" },
+            "xr-standard-squeeze": { "type": "squeeze" },
+            "xr-standard-touchpad": { "type": "touchpad" },
+            "menu": { "type": "button" }
+        },
+        "gamepad": {
+            "mapping": "xr-standard",
+            "buttons": [
+                "xr-standard-trigger",
+                "xr-standard-squeeze",
+                "xr-standard-touchpad",
+                null,
+                "menu"
+            ],
+            "axes": [
+                { "componentId": "xr-standard-touchpad", "axis": "x-axis" },
+                { "componentId": "xr-standard-touchpad", "axis": "y-axis" }
+            ]
+        }
+    }
+};
+
+const HTCViveLegacyLayout: IMotionControllerLayoutMap = {
+    "left-right-none": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": { "type": "trigger" },
+            "xr-standard-squeeze": { "type": "squeeze" },
+            "xr-standard-touchpad": { "type": "touchpad" },
+            "menu": { "type": "button" }
+        },
+        "gamepad": {
+            "mapping": "",
+            "buttons": [
+                "xr-standard-touchpad",
+                "xr-standard-trigger",
+                "xr-standard-squeeze",
+                "menu"
+            ],
+            "axes": [
+                { "componentId": "xr-standard-touchpad", "axis": "x-axis" },
+                { "componentId": "xr-standard-touchpad", "axis": "y-axis" }
+            ]
+        }
+    }
+};
+
+/**
+ * The motion controller class for the standard HTC-Vive controllers
+ */
+export class WebXRHTCViveMotionController extends WebXRAbstractMotionController {
+    /**
+     * The base url used to load the left and right controller models
+     */
+    public static MODEL_BASE_URL: string = 'https://controllers.babylonjs.com/vive/';
+    /**
+     * File name for the controller model.
+     */
+    public static MODEL_FILENAME: string = 'wand.babylon';
+
+    public profileId = "htc-vive";
+
+    private _modelRootNode: AbstractMesh;
+
+    constructor(scene: Scene,
+        gamepadObject: IMinimalMotionControllerObject,
+        handness: MotionControllerHandness,
+        legacyMapping: boolean = false) {
+        super(scene, legacyMapping ? HTCViveLegacyLayout["left-right-none"] : HTCViveLayout["left-right-none"], gamepadObject, handness);
+    }
+
+    protected _processLoadedModel(_meshes: AbstractMesh[]): void {
+        this.layout.gamepad!.buttons.forEach((buttonName) => {
+            const comp = buttonName && this.getComponent(buttonName);
+            if (comp) {
+                comp.onButtonStateChanged.add((component) => {
+
+                    if (!this.rootMesh) { return; }
+
+                    switch (buttonName) {
+                        case "xr-standard-trigger":
+                            (<AbstractMesh>(this._modelRootNode.getChildren()[6])).rotation.x = -component.value * 0.15;
+                            return;
+                        case "xr-standard-touchpad":
+                            return;
+                        case "xr-standard-squeeze":
+                            return;
+                        case "menu":
+                            if (component.pressed) {
+                                (<AbstractMesh>(this._modelRootNode.getChildren()[2])).position.y = -0.001;
+                            }
+                            else {
+                                (<AbstractMesh>(this._modelRootNode.getChildren()[2])).position.y = 0;
+                            }
+                            return;
+                    }
+                }, undefined, true);
+            }
+        });
+    }
+
+    protected _getFilenameAndPath(): { filename: string; path: string; } {
+        let filename = WebXRHTCViveMotionController.MODEL_FILENAME;
+        let path = WebXRHTCViveMotionController.MODEL_BASE_URL;
+
+        return {
+            filename,
+            path
+        };
+    }
+
+    protected _updateModel(): void {
+        // no-op. model is updated using observables.
+    }
+
+    protected _getModelLoadingConstraints(): boolean {
+        return true;
+    }
+
+    protected _setRootMesh(meshes: AbstractMesh[]): void {
+        this.rootMesh = new Mesh(this.profileId + " " + this.handness, this.scene);
+
+        meshes.forEach((mesh) => { mesh.isPickable = false; });
+        this._modelRootNode = meshes[1];
+        this._modelRootNode.parent = this.rootMesh;
+        this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
+    }
+
+}
+
+// register the profile
+WebXRMotionControllerManager.RegisterController("htc-vive", (xrInput: XRInputSource, scene: Scene) => {
+    return new WebXRHTCViveMotionController(scene, <any>(xrInput.gamepad), xrInput.handedness);
+});
+
+WebXRMotionControllerManager.RegisterController("htc-vive-legacy", (xrInput: XRInputSource, scene: Scene) => {
+    return new WebXRHTCViveMotionController(scene, <any>(xrInput.gamepad), xrInput.handedness, true);
+});

+ 264 - 0
src/Cameras/XR/motionController/webXRMicrosoftMixedRealityController.ts

@@ -0,0 +1,264 @@
+import {
+    WebXRAbstractMotionController,
+    IMinimalMotionControllerObject,
+    MotionControllerHandness,
+    IMotionControllerLayoutMap
+} from "./webXRAbstractController";
+import { WebXRMotionControllerManager } from './webXRMotionControllerManager';
+import { AbstractMesh } from '../../../Meshes/abstractMesh';
+import { Scene } from '../../../scene';
+import { Logger } from '../../../Misc/logger';
+import { Mesh } from '../../../Meshes/mesh';
+import { Quaternion } from '../../../Maths/math.vector';
+import { SceneLoader } from '../../../Loading/sceneLoader';
+
+// https://github.com/immersive-web/webxr-input-profiles/blob/master/packages/registry/profiles/microsoft/microsoft-mixed-reality.json
+const MixedRealityProfile: IMotionControllerLayoutMap = {
+    "left-right": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": { "type": "trigger" },
+            "xr-standard-squeeze": { "type": "squeeze" },
+            "xr-standard-touchpad": { "type": "touchpad" },
+            "xr-standard-thumbstick": { "type": "thumbstick" },
+            "menu": { "type": "button" }
+        },
+        "gamepad": {
+            "mapping": "xr-standard",
+            "buttons": [
+                "xr-standard-trigger",
+                "xr-standard-squeeze",
+                "xr-standard-touchpad",
+                "xr-standard-thumbstick",
+                "menu"
+            ],
+            "axes": [
+                { "componentId": "xr-standard-touchpad", "axis": "x-axis" },
+                { "componentId": "xr-standard-touchpad", "axis": "y-axis" },
+                { "componentId": "xr-standard-thumbstick", "axis": "x-axis" },
+                { "componentId": "xr-standard-thumbstick", "axis": "y-axis" }
+            ]
+        }
+    }
+};
+
+/**
+ * The motion controller class for all microsoft mixed reality controllers
+ */
+export class WebXRMicrosoftMixedRealityController extends WebXRAbstractMotionController {
+    /**
+     * The base url used to load the left and right controller models
+     */
+    public static MODEL_BASE_URL: string = 'https://controllers.babylonjs.com/microsoft/';
+    /**
+     * The name of the left controller model file
+     */
+    public static MODEL_LEFT_FILENAME: string = 'left.glb';
+    /**
+     * The name of the right controller model file
+     */
+    public static MODEL_RIGHT_FILENAME: string = 'right.glb';
+
+    public profileId = "microsoft-mixed-reality";
+
+    // use this in the future - https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/assets/profiles/microsoft
+    protected readonly _mapping = {
+        defaultButton: {
+            "valueNodeName": "VALUE",
+            "unpressedNodeName": "UNPRESSED",
+            "pressedNodeName": "PRESSED"
+        },
+        defaultAxis: {
+            "valueNodeName": "VALUE",
+            "minNodeName": "MIN",
+            "maxNodeName": "MAX"
+        },
+        buttons: {
+            "xr-standard-trigger": {
+                "rootNodeName": "SELECT",
+                "componentProperty": "button",
+                "states": ["default", "touched", "pressed"]
+            },
+            "xr-standard-squeeze": {
+                "rootNodeName": "GRASP",
+                "componentProperty": "state",
+                "states": ["pressed"]
+            },
+            "xr-standard-touchpad": {
+                "rootNodeName": "TOUCHPAD_PRESS",
+                "labelAnchorNodeName": "squeeze-label",
+                "touchPointNodeName": "TOUCH" // TODO - use this for visual feedback
+            },
+            "xr-standard-thumbstick": {
+                "rootNodeName": "THUMBSTICK_PRESS",
+                "componentProperty": "state",
+                "states": ["pressed"],
+            },
+            "menu": {
+                "rootNodeName": "MENU",
+                "componentProperty": "state",
+                "states": ["pressed"]
+            }
+        },
+        axes: {
+            "xr-standard-touchpad": {
+                "x-axis": {
+                    "rootNodeName": "TOUCHPAD_TOUCH_X"
+                },
+                "y-axis": {
+                    "rootNodeName": "TOUCHPAD_TOUCH_Y"
+                }
+            },
+            "xr-standard-thumbstick": {
+                "x-axis": {
+                    "rootNodeName": "THUMBSTICK_X"
+                },
+                "y-axis": {
+                    "rootNodeName": "THUMBSTICK_Y"
+                }
+            }
+        }
+    };
+
+    constructor(scene: Scene, gamepadObject: IMinimalMotionControllerObject, handness: MotionControllerHandness) {
+        super(scene, MixedRealityProfile["left-right"], gamepadObject, handness);
+    }
+
+    protected _processLoadedModel(_meshes: AbstractMesh[]): void {
+        if (!this.rootMesh) { return; }
+
+        // Button Meshes
+        for (let i = 0; i < this.layout.gamepad!.buttons.length; i++) {
+            const buttonName = this.layout.gamepad!.buttons[i];
+            if (buttonName) {
+                const buttonMap = (<any>this._mapping.buttons)[buttonName];
+                const buttonMeshName = buttonMap.rootNodeName;
+                if (!buttonMeshName) {
+                    Logger.Log('Skipping unknown button at index: ' + i + ' with mapped name: ' + buttonName);
+                    continue;
+                }
+
+                var buttonMesh = this._getChildByName(this.rootMesh, buttonMeshName);
+                if (!buttonMesh) {
+                    Logger.Warn('Missing button mesh with name: ' + buttonMeshName);
+                    continue;
+                }
+
+                buttonMap.valueMesh = this._getImmediateChildByName(buttonMesh, this._mapping.defaultButton.valueNodeName);
+                buttonMap.pressedMesh = this._getImmediateChildByName(buttonMesh, this._mapping.defaultButton.pressedNodeName);
+                buttonMap.unpressedMesh = this._getImmediateChildByName(buttonMesh, this._mapping.defaultButton.unpressedNodeName);
+
+                if (buttonMap.valueMesh && buttonMap.pressedMesh && buttonMap.unpressedMesh) {
+                    const comp = this.getComponent(buttonName);
+                    if (comp) {
+                        comp.onButtonStateChanged.add((component) => {
+                            this._lerpButtonTransform(buttonMap, component.value);
+                        }, undefined, true);
+                    }
+                } else {
+                    // If we didn't find the mesh, it simply means this button won't have transforms applied as mapped button value changes.
+                    Logger.Warn('Missing button submesh under mesh with name: ' + buttonMeshName);
+                }
+            }
+
+        }
+
+        // Axis Meshes
+        for (let i = 0; i < this.layout.gamepad!.axes.length; ++i) {
+            const axisData = this.layout.gamepad!.axes[i];
+            if (!axisData) {
+                Logger.Log('Skipping unknown axis at index: ' + i);
+                continue;
+            }
+
+            const axisMap = (<any>this._mapping.axes)[axisData.componentId][axisData.axis];
+
+            var axisMesh = this._getChildByName(this.rootMesh, axisMap.rootNodeName);
+            if (!axisMesh) {
+                Logger.Warn('Missing axis mesh with name: ' + axisMap.rootNodeName);
+                continue;
+            }
+
+            axisMap.valueMesh = this._getImmediateChildByName(axisMesh, this._mapping.defaultAxis.valueNodeName);
+            axisMap.minMesh = this._getImmediateChildByName(axisMesh, this._mapping.defaultAxis.minNodeName);
+            axisMap.maxMesh = this._getImmediateChildByName(axisMesh, this._mapping.defaultAxis.maxNodeName);
+
+            if (axisMap.valueMesh && axisMap.minMesh && axisMap.maxMesh) {
+                const comp = this.getComponent(axisData.componentId);
+                if (comp) {
+                    comp.onAxisValueChanged.add((axisValues) => {
+                        const value = axisData.axis === "x-axis" ? axisValues.x : axisValues.y;
+                        this._lerpAxisTransform(axisMap, value);
+                    }, undefined, true);
+                }
+
+            } else {
+                // If we didn't find the mesh, it simply means this button won't have transforms applied as mapped button value changes.
+                Logger.Warn('Missing axis submesh under mesh with name: ' + axisMap.rootNodeName);
+            }
+        }
+    }
+
+    // Look through all children recursively. This will return null if no mesh exists with the given name.
+    private _getChildByName(node: AbstractMesh, name: string): AbstractMesh {
+        return <AbstractMesh>node.getChildren((n) => n.name === name, false)[0];
+    }
+    // Look through only immediate children. This will return null if no mesh exists with the given name.
+    private _getImmediateChildByName(node: AbstractMesh, name: string): AbstractMesh {
+        return <AbstractMesh>node.getChildren((n) => n.name == name, true)[0];
+    }
+
+    protected _getFilenameAndPath(): { filename: string; path: string; } {
+        let filename = "";
+        if (this.handness === 'left') {
+            filename = WebXRMicrosoftMixedRealityController.MODEL_LEFT_FILENAME;
+        }
+        else { // Right is the default if no hand is specified
+            filename = WebXRMicrosoftMixedRealityController.MODEL_RIGHT_FILENAME;
+        }
+
+        const device = 'default';
+        let path = WebXRMicrosoftMixedRealityController.MODEL_BASE_URL + device + '/';
+        return {
+            filename,
+            path
+        };
+    }
+
+    protected _updateModel(): void {
+        // no-op. model is updated using observables.
+    }
+
+    protected _getModelLoadingConstraints(): boolean {
+        return SceneLoader.IsPluginForExtensionAvailable(".glb");
+    }
+
+    protected _setRootMesh(meshes: AbstractMesh[]): void {
+        this.rootMesh = new Mesh(this.profileId + " " + this.handness, this.scene);
+        this.rootMesh.isPickable = false;
+        let rootMesh;
+        // Find the root node in the loaded glTF scene, and attach it as a child of 'parentMesh'
+        for (let i = 0; i < meshes.length; i++) {
+            let mesh = meshes[i];
+
+            mesh.isPickable = false;
+
+            if (!mesh.parent) {
+                // Handle root node, attach to the new parentMesh
+                rootMesh = mesh;
+            }
+        }
+
+        if (rootMesh) {
+            rootMesh.setParent(this.rootMesh);
+        }
+
+        this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
+    }
+
+}
+
+// register the profile
+WebXRMotionControllerManager.RegisterController("microsoft-mixed-reality", (xrInput: XRInputSource, scene: Scene) => {
+    return new WebXRMicrosoftMixedRealityController(scene, <any>(xrInput.gamepad), xrInput.handedness);
+});

+ 131 - 0
src/Cameras/XR/motionController/webXRMotionControllerManager.ts

@@ -0,0 +1,131 @@
+import {
+    WebXRAbstractMotionController,
+} from './webXRAbstractController';
+import { WebXRGenericTriggerMotionController } from './webXRGenericMotionController';
+import { Scene } from '../../../scene';
+
+/**
+ * A construction function type to create a new controller based on an xrInput object
+ */
+export type MotionControllerConstructor = (xrInput: XRInputSource, scene: Scene) => WebXRAbstractMotionController;
+
+/**
+ * The MotionController Manager manages all registered motion controllers and loads the right one when needed.
+ *
+ * When this repository is complete: https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/assets
+ * it should be replaced with auto-loaded controllers.
+ *
+ * When using a model try to stay as generic as possible. Eventually there will be no need in any of the controller classes
+ */
+export class WebXRMotionControllerManager {
+    private static _AvailableControllers: { [type: string]: MotionControllerConstructor } = {};
+    private static _Fallbacks: { [profileId: string]: string[] } = {};
+
+    /**
+     * Register a new controller based on its profile. This function will be called by the controller classes themselves.
+     *
+     * If you are missing a profile, make sure it is imported in your source, otherwise it will not register.
+     *
+     * @param type the profile type to register
+     * @param constructFunction the function to be called when loading this profile
+     */
+    public static RegisterController(type: string, constructFunction: MotionControllerConstructor) {
+        this._AvailableControllers[type] = constructFunction;
+    }
+
+    /**
+     * When acquiring a new xrInput object (usually by the WebXRInput class), match it with the correct profile.
+     * The order of search:
+     *
+     * 1) Iterate the profiles array of the xr input and try finding a corresponding motion controller
+     * 2) (If not found) search in the gamepad id and try using it (legacy versions only)
+     * 3) search for registered fallbacks (should be redundant, nonetheless it makes sense to check)
+     * 4) return the generic trigger controller if none were found
+     *
+     * @param xrInput the xrInput to which a new controller is initialized
+     * @param scene the scene to which the model will be added
+     * @return the motion controller class for this profile id or the generic standard class if none was found
+     */
+    public static GetMotionControllerWithXRInput(xrInput: XRInputSource, scene: Scene): WebXRAbstractMotionController {
+        for (let i = 0; i < xrInput.profiles.length; ++i) {
+            const constructionFunction = this._AvailableControllers[xrInput.profiles[i]];
+            if (constructionFunction) {
+                return constructionFunction(xrInput, scene);
+            }
+        }
+        // try using the gamepad id
+        if (xrInput.gamepad && xrInput.gamepad.id) {
+            switch (xrInput.gamepad.id) {
+                case (xrInput.gamepad.id.match(/oculus touch/gi) ? xrInput.gamepad.id : undefined):
+                    // oculus in gamepad id - legacy mapping
+                    return this._AvailableControllers["oculus-touch-legacy"](xrInput, scene);
+                case (xrInput.gamepad.id.match(/Spatial Controller/gi) ? xrInput.gamepad.id : undefined):
+                    // oculus in gamepad id - legacy mapping
+                    return this._AvailableControllers["microsoft-mixed-reality"](xrInput, scene);
+                case (xrInput.gamepad.id.match(/openvr/gi) ? xrInput.gamepad.id : undefined):
+                    // oculus in gamepad id - legacy mapping
+                    return this._AvailableControllers["htc-vive-legacy"](xrInput, scene);
+            }
+        }
+        // check fallbacks
+        for (let i = 0; i < xrInput.profiles.length; ++i) {
+            const fallbacks = this.FindFallbackWithProfileId(xrInput.profiles[i]);
+            for (let j = 0; j < fallbacks.length; ++j) {
+                const constructionFunction = this._AvailableControllers[fallbacks[j]];
+                if (constructionFunction) {
+                    return constructionFunction(xrInput, scene);
+                }
+            }
+        }
+        // return the most generic thing we have
+        return this._AvailableControllers[WebXRGenericTriggerMotionController.ProfileId](xrInput, scene);
+    }
+
+    /**
+     * Find a fallback profile if the profile was not found. There are a few predefined generic profiles.
+     * @param profileId the profile to which a fallback needs to be found
+     * @return an array with corresponding fallback profiles
+     */
+    public static FindFallbackWithProfileId(profileId: string): string[] {
+        return this._Fallbacks[profileId] || [];
+    }
+
+    /**
+     * Register a fallback to a specific profile.
+     * @param profileId the profileId that will receive the fallbacks
+     * @param fallbacks A list of fallback profiles
+     */
+    public static RegisterFallbacksForProfileId(profileId: string, fallbacks: string[]): void {
+        if (this._Fallbacks[profileId]) {
+            this._Fallbacks[profileId].push(...fallbacks);
+        } else {
+            this._Fallbacks[profileId] = fallbacks;
+        }
+    }
+
+    /**
+     * Register the default fallbacks.
+     * This function is called automatically when this file is imported.
+     */
+    public static DefaultFallbacks() {
+        this.RegisterFallbacksForProfileId("google-daydream", ["generic-touchpad"]);
+        this.RegisterFallbacksForProfileId("htc-vive-focus", ["generic-trigger-touchpad"]);
+        this.RegisterFallbacksForProfileId("htc-vive", ["generic-trigger-squeeze-touchpad"]);
+        this.RegisterFallbacksForProfileId("magicleap-one", ["generic-trigger-squeeze-touchpad"]);
+        this.RegisterFallbacksForProfileId("microsoft-mixed-reality", ["generic-trigger-squeeze-touchpad-thumbstick"]);
+        this.RegisterFallbacksForProfileId("oculus-go", ["generic-trigger-touchpad"]);
+        this.RegisterFallbacksForProfileId("oculus-touch-v2", ["oculus-touch", "generic-trigger-squeeze-thumbstick"]);
+        this.RegisterFallbacksForProfileId("oculus-touch", ["generic-trigger-squeeze-thumbstick"]);
+        this.RegisterFallbacksForProfileId("samsung-gearvr", ["microsoft-mixed-reality", "generic-trigger-squeeze-touchpad-thumbstick"]);
+        this.RegisterFallbacksForProfileId("samsung-odyssey", ["generic-touchpad"]);
+        this.RegisterFallbacksForProfileId("valve-index", ["generic-trigger-squeeze-touchpad-thumbstick"]);
+    }
+}
+
+// register the generic profile(s) here so we will at least have them
+WebXRMotionControllerManager.RegisterController(WebXRGenericTriggerMotionController.ProfileId, (xrInput: XRInputSource, scene: Scene) => {
+    return new WebXRGenericTriggerMotionController(scene, <any>(xrInput.gamepad), xrInput.handedness);
+});
+
+// register fallbacks
+WebXRMotionControllerManager.DefaultFallbacks();

+ 278 - 0
src/Cameras/XR/motionController/webXROculusTouchMotionController.ts

@@ -0,0 +1,278 @@
+import {
+    WebXRAbstractMotionController,
+    IMinimalMotionControllerObject,
+    MotionControllerHandness,
+    IMotionControllerLayoutMap
+} from "./webXRAbstractController";
+import { WebXRMotionControllerManager } from './webXRMotionControllerManager';
+import { AbstractMesh } from '../../../Meshes/abstractMesh';
+import { Scene } from '../../../scene';
+import { Mesh } from '../../../Meshes/mesh';
+import { Quaternion } from '../../../Maths/math.vector';
+
+// https://github.com/immersive-web/webxr-input-profiles/blob/master/packages/registry/profiles/microsoft/microsoft-mixed-reality.json
+const OculusTouchLayouts: IMotionControllerLayoutMap = {
+    "left": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": { "type": "trigger" },
+            "xr-standard-squeeze": { "type": "squeeze" },
+            "xr-standard-thumbstick": { "type": "thumbstick" },
+            "a-button": { "type": "button" },
+            "b-button": { "type": "button" },
+            "thumbrest": { "type": "button" }
+        },
+        "gamepad": {
+            "mapping": "xr-standard",
+            "buttons": [
+                "xr-standard-trigger",
+                "xr-standard-squeeze",
+                null,
+                "xr-standard-thumbstick",
+                "a-button",
+                "b-button",
+                "thumbrest"
+            ],
+            "axes": [
+                null,
+                null,
+                { "componentId": "xr-standard-thumbstick", "axis": "x-axis" },
+                { "componentId": "xr-standard-thumbstick", "axis": "y-axis" }
+            ]
+        }
+    },
+    "right": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": { "type": "trigger" },
+            "xr-standard-squeeze": { "type": "squeeze" },
+            "xr-standard-thumbstick": { "type": "thumbstick" },
+            "x-button": { "type": "button" },
+            "y-button": { "type": "button" },
+            "thumbrest": { "type": "button" }
+        },
+        "gamepad": {
+            "mapping": "xr-standard",
+            "buttons": [
+                "xr-standard-trigger",
+                "xr-standard-squeeze",
+                null,
+                "xr-standard-thumbstick",
+                "x-button",
+                "y-button",
+                "thumbrest"
+            ],
+            "axes": [
+                null,
+                null,
+                { "componentId": "xr-standard-thumbstick", "axis": "x-axis" },
+                { "componentId": "xr-standard-thumbstick", "axis": "y-axis" }
+            ]
+        }
+    }
+};
+
+const OculusTouchLegacyLayouts: IMotionControllerLayoutMap = {
+    "left": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": { "type": "trigger" },
+            "xr-standard-squeeze": { "type": "squeeze" },
+            "xr-standard-thumbstick": { "type": "thumbstick" },
+            "a-button": { "type": "button" },
+            "b-button": { "type": "button" },
+            "thumbrest": { "type": "button" }
+        },
+        "gamepad": {
+            "mapping": "",
+            "buttons": [
+                "xr-standard-thumbstick",
+                "xr-standard-trigger",
+                "xr-standard-squeeze",
+                "a-button",
+                "b-button",
+                "thumbrest"
+            ],
+            "axes": [
+                { "componentId": "xr-standard-thumbstick", "axis": "x-axis" },
+                { "componentId": "xr-standard-thumbstick", "axis": "y-axis" }
+            ]
+        }
+    },
+    "right": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": { "type": "trigger" },
+            "xr-standard-squeeze": { "type": "squeeze" },
+            "xr-standard-thumbstick": { "type": "thumbstick" },
+            "x-button": { "type": "button" },
+            "y-button": { "type": "button" },
+            "thumbrest": { "type": "button" }
+        },
+        "gamepad": {
+            "mapping": "",
+            "buttons": [
+                "xr-standard-thumbstick",
+                "xr-standard-trigger",
+                "xr-standard-squeeze",
+                "x-button",
+                "y-button",
+                "thumbrest"
+            ],
+            "axes": [
+                { "componentId": "xr-standard-thumbstick", "axis": "x-axis" },
+                { "componentId": "xr-standard-thumbstick", "axis": "y-axis" }
+            ]
+        }
+    }
+};
+
+/**
+ * The motion controller class for oculus touch (quest, rift).
+ * This class supports legacy mapping as well the standard xr mapping
+ */
+export class WebXROculusTouchMotionController extends WebXRAbstractMotionController {
+    /**
+     * The base url used to load the left and right controller models
+     */
+    public static MODEL_BASE_URL: string = 'https://controllers.babylonjs.com/oculus/';
+    /**
+     * The name of the left controller model file
+     */
+    public static MODEL_LEFT_FILENAME: string = 'left.babylon';
+    /**
+     * The name of the right controller model file
+     */
+    public static MODEL_RIGHT_FILENAME: string = 'right.babylon';
+
+    /**
+     * Base Url for the Quest controller model.
+     */
+    public static QUEST_MODEL_BASE_URL: string = 'https://controllers.babylonjs.com/oculusQuest/';
+
+    public profileId = "oculus-touch";
+
+    private _modelRootNode: AbstractMesh;
+
+    constructor(scene: Scene,
+        gamepadObject: IMinimalMotionControllerObject,
+        handness: MotionControllerHandness,
+        legacyMapping: boolean = false,
+        private _forceLegacyControllers: boolean = false) {
+        super(scene, legacyMapping ? OculusTouchLegacyLayouts[handness] : OculusTouchLayouts[handness], gamepadObject, handness);
+    }
+
+    protected _processLoadedModel(_meshes: AbstractMesh[]): void {
+
+        const isQuest = this._isQuest();
+        const triggerDirection = this.handness === 'right' ? -1 : 1;
+
+        this.layout.gamepad!.buttons.forEach((buttonName) => {
+            const comp = buttonName && this.getComponent(buttonName);
+            if (comp) {
+                comp.onButtonStateChanged.add((component) => {
+
+                    if (!this.rootMesh) { return; }
+
+                    switch (buttonName) {
+                        case "xr-standard-trigger": // index trigger
+                            if (!isQuest) {
+                                (<AbstractMesh>(this._modelRootNode.getChildren()[3])).rotation.x = -component.value * 0.20;
+                                (<AbstractMesh>(this._modelRootNode.getChildren()[3])).position.y = -component.value * 0.005;
+                                (<AbstractMesh>(this._modelRootNode.getChildren()[3])).position.z = -component.value * 0.005;
+                            }
+                            return;
+                        case "xr-standard-squeeze":  // secondary trigger
+                            if (!isQuest) {
+                                (<AbstractMesh>(this._modelRootNode.getChildren()[4])).position.x = triggerDirection * component.value * 0.0035;
+                            }
+                            return;
+                        case "xr-standard-thumbstick": // thumbstick
+                            return;
+                        case "a-button":
+                        case "x-button":
+                            if (!isQuest) {
+                                if (component.pressed) {
+                                    (<AbstractMesh>(this._modelRootNode.getChildren()[1])).position.y = -0.001;
+                                }
+                                else {
+                                    (<AbstractMesh>(this._modelRootNode.getChildren()[1])).position.y = 0;
+                                }
+                            }
+                            return;
+                        case "b-button":
+                        case "y-button":
+                            if (!isQuest) {
+                                if (component.pressed) {
+                                    (<AbstractMesh>(this._modelRootNode.getChildren()[2])).position.y = -0.001;
+                                }
+                                else {
+                                    (<AbstractMesh>(this._modelRootNode.getChildren()[2])).position.y = 0;
+                                }
+                            }
+                            return;
+                    }
+                }, undefined, true);
+            }
+        });
+    }
+
+    protected _getFilenameAndPath(): { filename: string; path: string; } {
+        let filename = "";
+        if (this.handness === 'left') {
+            filename = WebXROculusTouchMotionController.MODEL_LEFT_FILENAME;
+        }
+        else { // Right is the default if no hand is specified
+            filename = WebXROculusTouchMotionController.MODEL_RIGHT_FILENAME;
+        }
+
+        let path = this._isQuest() ? WebXROculusTouchMotionController.QUEST_MODEL_BASE_URL : WebXROculusTouchMotionController.MODEL_BASE_URL;
+        return {
+            filename,
+            path
+        };
+    }
+
+    /**
+     * Is this the new type of oculus touch. At the moment both have the same profile and it is impossible to differentiate
+     * between the touch and touch 2.
+     */
+    private _isQuest() {
+        // this is SADLY the only way to currently check. Until proper profiles will be available.
+        return !!navigator.userAgent.match(/Quest/gi) && !this._forceLegacyControllers;
+    }
+
+    protected _updateModel(): void {
+        // no-op. model is updated using observables.
+    }
+
+    protected _getModelLoadingConstraints(): boolean {
+        return true;
+    }
+
+    protected _setRootMesh(meshes: AbstractMesh[]): void {
+        this.rootMesh = new Mesh(this.profileId + " " + this.handness, this.scene);
+
+        meshes.forEach((mesh) => { mesh.isPickable = false; });
+        if (this._isQuest()) {
+            this._modelRootNode = meshes[0];
+            this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
+            this.rootMesh.position.y = 0.034;
+            this.rootMesh.position.z = 0.052;
+        } else {
+            this._modelRootNode = meshes[1];
+            this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(Math.PI / -4, Math.PI, 0);
+        }
+        this._modelRootNode.parent = this.rootMesh;
+    }
+
+}
+
+// register the profile
+WebXRMotionControllerManager.RegisterController("oculus-touch", (xrInput: XRInputSource, scene: Scene) => {
+    return new WebXROculusTouchMotionController(scene, <any>(xrInput.gamepad), xrInput.handedness);
+});
+
+WebXRMotionControllerManager.RegisterController("oculus-touch-legacy", (xrInput: XRInputSource, scene: Scene) => {
+    return new WebXROculusTouchMotionController(scene, <any>(xrInput.gamepad), xrInput.handedness, true);
+});

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

@@ -1,10 +1,10 @@
-import { Nullable } from "../../types";
 import { Observable } from "../../Misc/observable";
 import { AbstractMesh } from "../../Meshes/abstractMesh";
 import { Quaternion, Vector3 } from '../../Maths/math.vector';
 import { Ray } from '../../Culling/ray';
 import { Scene } from '../../scene';
-import { WebVRController } from '../../Gamepads/Controllers/webVRController';
+import { WebXRAbstractMotionController } from './motionController/webXRAbstractController';
+import { WebXRMotionControllerManager } from './motionController/webXRMotionControllerManager';
 /**
  * Represents an XR input
  */
@@ -24,7 +24,7 @@ export class WebXRController {
      * Using this object it is possible to get click events and trackpad changes of the
      * webxr controller that is currently being used.
      */
-    public gamepadController?: WebVRController;
+    public gamepadController?: WebXRAbstractMotionController;
 
     /**
      * Event that fires when the controller is removed/disposed
@@ -44,22 +44,22 @@ export class WebXRController {
     constructor(
         private scene: Scene,
         /** The underlying input source for the controller  */
-        public inputSource: XRInputSource,
-        private parentContainer: Nullable<AbstractMesh> = null) {
+        public inputSource: XRInputSource) {
         this.pointer = new AbstractMesh("controllerPointer", scene);
         this.pointer.rotationQuaternion = new Quaternion();
-        if (parentContainer) {
-            parentContainer.addChild(this.pointer);
-        }
 
         if (this.inputSource.gripSpace) {
             this.grip = new AbstractMesh("controllerGrip", this.scene);
             this.grip.rotationQuaternion = new Quaternion();
-            if (this.parentContainer) {
-                this.parentContainer.addChild(this.grip);
-            }
-        } else if (this.inputSource.gamepad) {
-            this._gamepadMode = true;
+        }
+
+        // for now only load motion controllers if gamepad available
+        if (this.inputSource.gamepad) {
+            this.gamepadController = WebXRMotionControllerManager.GetMotionControllerWithXRInput(inputSource, scene);
+            // if the model is loaded, do your thing
+            this.gamepadController.onModelLoadedObservable.addOnce(() => {
+                this.gamepadController!.rootMesh!.parent = this.pointer;
+            });
         }
     }
 
@@ -97,9 +97,7 @@ export class WebXRController {
         }
         if (this.gamepadController) {
             // either update buttons only or also position, if in gamepad mode
-            this.gamepadController.isXR = !this._gamepadMode;
-            this.gamepadController.update();
-            this.gamepadController.isXR = true;
+            this.gamepadController.updateFromXRFrame(xrFrame);
         }
     }
 

+ 0 - 77
src/Cameras/XR/webXRControllerModelLoader.ts

@@ -1,77 +0,0 @@
-import { Quaternion, Vector3 } from '../../Maths/math.vector';
-import { XRWindowsMotionController } from '../../Gamepads/Controllers/windowsMotionController';
-import { OculusTouchController } from '../../Gamepads/Controllers/oculusTouchController';
-import { WebXRInput } from './webXRInput';
-import { ViveController } from '../../Gamepads/Controllers/viveController';
-import { WebVRController } from '../../Gamepads/Controllers/webVRController';
-import { Observable } from '../../Misc/observable';
-import { WebXRController } from './webXRController';
-
-/**
- * Loads a controller model and adds it as a child of the WebXRControllers grip when the controller is created
- */
-export class WebXRControllerModelLoader {
-
-    /**
-     * an observable that triggers when a new model (the mesh itself) was initialized.
-     * To know when the mesh was loaded use the controller's own modelLoaded() method
-     */
-    public onControllerModelLoaded = new Observable<WebXRController>();
-    /**
-     * Creates the WebXRControllerModelLoader
-     * @param input xr input that creates the controllers
-     */
-    constructor(input: WebXRInput) {
-        input.onControllerAddedObservable.add((c) => {
-            if (!c.inputSource.gamepad) {
-                return;
-            }
-
-            let controllerModel: WebVRController;
-
-            let rotation: Quaternion;
-            const position = new Vector3();
-
-            switch (c.inputSource.gamepad.id) {
-                case "htc-vive": {
-                    controllerModel = new ViveController(c.inputSource.gamepad);
-                    rotation = Quaternion.FromEulerAngles(0, Math.PI, 0);
-                    break;
-                }
-                case "oculus-touch": {
-                    controllerModel = new OculusTouchController(c.inputSource.gamepad);
-                    rotation = Quaternion.FromEulerAngles(0, Math.PI, 0);
-                    position.y = 0.034;
-                    position.z = 0.052;
-                    break;
-                }
-                case "oculus-quest": {
-                    OculusTouchController._IsQuest = true;
-                    controllerModel = new OculusTouchController(c.inputSource.gamepad);
-                    rotation = Quaternion.FromEulerAngles(Math.PI / -4, Math.PI, 0);
-                    break;
-                }
-                default: {
-                    controllerModel = new XRWindowsMotionController(c.inputSource.gamepad);
-                    rotation = Quaternion.FromEulerAngles(0, Math.PI, 0);
-                    break;
-                }
-            }
-
-            controllerModel.hand = c.inputSource.handedness;
-            controllerModel.isXR = true;
-            controllerModel.initControllerMesh(c.getScene(), (m) => {
-                controllerModel.mesh!.parent = c.grip || null;
-                controllerModel.mesh!.rotationQuaternion = rotation;
-                controllerModel.mesh!.position = position;
-                m.isPickable = false;
-                m.getChildMeshes(false).forEach((m) => {
-                    m.isPickable = false;
-                });
-                this.onControllerModelLoaded.notifyObservers(c);
-            });
-
-            c.gamepadController = controllerModel;
-        });
-    }
-}

+ 6 - 6
src/Cameras/XR/webXRControllerTeleportation.ts

@@ -121,8 +121,8 @@ export class WebXRControllerTeleportation {
                             if (forwardReadyToTeleport) {
                                 // Teleport the users feet to where they targeted
                                 this._tmpVector.copyFrom(teleportationTarget.position);
-                                this._tmpVector.y += input.baseExperience.camera.position.y;
-                                input.baseExperience.camera.position.copyFrom(this._tmpVector);
+                                this._tmpVector.y += input.xrCamera.position.y;
+                                input.xrCamera.position.copyFrom(this._tmpVector);
                             }
                             forwardReadyToTeleport = false;
                         }
@@ -133,7 +133,7 @@ export class WebXRControllerTeleportation {
                         } else {
                             if (backwardReadyToTeleport) {
                                 this._tmpVector.set(0, 0, -1);
-                                this._tmpVector.rotateByQuaternionToRef(input.baseExperience.camera.rotationQuaternion, this._tmpVector);
+                                this._tmpVector.rotateByQuaternionToRef(input.xrCamera.rotationQuaternion, this._tmpVector);
                                 this._tmpVector.y = 0;
                                 this._tmpVector.normalize();
                                 this._tmpVector.y = -1.5;
@@ -146,7 +146,7 @@ export class WebXRControllerTeleportation {
                                 if (pick && pick.pickedPoint) {
                                     // Teleport the users feet to where they targeted
                                     this._tmpVector.copyFrom(pick.pickedPoint);
-                                    input.baseExperience.camera.position.addInPlace(this._tmpVector);
+                                    input.xrCamera.position.addInPlace(this._tmpVector);
                                 }
                             }
                             backwardReadyToTeleport = false;
@@ -158,7 +158,7 @@ export class WebXRControllerTeleportation {
                             leftReadyToTeleport = true;
                         } else {
                             if (leftReadyToTeleport) {
-                                input.baseExperience.camera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, -Math.PI / 4, 0));
+                                input.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, -Math.PI / 4, 0));
                             }
                             leftReadyToTeleport = false;
                         }
@@ -166,7 +166,7 @@ export class WebXRControllerTeleportation {
                             rightReadyToTeleport = true;
                         } else {
                             if (rightReadyToTeleport) {
-                                input.baseExperience.camera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, Math.PI / 4, 0));
+                                input.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, Math.PI / 4, 0));
                             }
                             rightReadyToTeleport = false;
                         }

+ 9 - 10
src/Cameras/XR/webXRDefaultExperience.ts

@@ -1,7 +1,6 @@
 import { WebXRExperienceHelper } from "./webXRExperienceHelper";
 import { Scene } from '../../scene';
-import { WebXRInput } from './webXRInput';
-import { WebXRControllerModelLoader } from './webXRControllerModelLoader';
+import { WebXRInput, IWebXRInputOptions } from './webXRInput';
 import { WebXRControllerPointerSelection } from './webXRControllerPointerSelection';
 import { WebXRControllerTeleportation } from './webXRControllerTeleportation';
 import { WebXRRenderTarget } from './webXRTypes';
@@ -16,7 +15,7 @@ export class WebXRDefaultExperienceOptions {
     /**
      * Floor meshes that should be used for teleporting
      */
-    public floorMeshes: Array<AbstractMesh>;
+    public floorMeshes?: Array<AbstractMesh>;
 
     /**
      * Enable or disable default UI to enter XR
@@ -32,6 +31,11 @@ export class WebXRDefaultExperienceOptions {
      * optional UI options. This can be used among other to change session mode and reference space type
      */
     public uiOptions?: WebXREnterExitUIOptions;
+
+    /**
+     * Disable the controller mesh-loading. Can be used if you want to load your own meshes
+     */
+    public inputOptions?: IWebXRInputOptions;
 }
 
 /**
@@ -47,10 +51,6 @@ export class WebXRDefaultExperience {
      */
     public input: WebXRInput;
     /**
-     * Loads the controller models
-     */
-    public controllerModelLoader: WebXRControllerModelLoader;
-    /**
      * Enables laser pointer and selection
      */
     public pointerSelection: WebXRControllerPointerSelection;
@@ -73,7 +73,7 @@ export class WebXRDefaultExperience {
      * @param options options for basic configuration
      * @returns resulting WebXRDefaultExperience
      */
-    public static CreateAsync(scene: Scene, options: WebXRDefaultExperienceOptions) {
+    public static CreateAsync(scene: Scene, options: WebXRDefaultExperienceOptions = {}) {
         var result = new WebXRDefaultExperience();
 
         // Create base experience
@@ -81,8 +81,7 @@ export class WebXRDefaultExperience {
             result.baseExperience = xrHelper;
 
             // Add controller support
-            result.input = new WebXRInput(xrHelper);
-            result.controllerModelLoader = new WebXRControllerModelLoader(result.input);
+            result.input = new WebXRInput(xrHelper.sessionManager, xrHelper.camera, options.inputOptions);
             result.pointerSelection = new WebXRControllerPointerSelection(result.input);
 
             if (options.floorMeshes) {

+ 39 - 27
src/Cameras/XR/webXRInput.ts

@@ -1,11 +1,20 @@
 import { Nullable } from "../../types";
 import { Observer, Observable } from "../../Misc/observable";
 import { IDisposable } from "../../scene";
-import { WebXRExperienceHelper } from "./webXRExperienceHelper";
 import { WebXRController } from './webXRController';
-import { WebXRState } from './webXRTypes';
+import { WebXRSessionManager } from './webXRSessionManager';
+import { WebXRCamera } from './webXRCamera';
 
 /**
+ * The schema for initialization options of the XR Input class
+ */
+export interface IWebXRInputOptions {
+    /**
+     * If set to true no model will be automatically loaded
+     */
+    doNotLoadControllerMeshes?: boolean;
+}
+/**
  * XR input used to track XR inputs such as controllers/rays
  */
 export class WebXRInput implements IDisposable {
@@ -14,7 +23,8 @@ export class WebXRInput implements IDisposable {
      */
     public controllers: Array<WebXRController> = [];
     private _frameObserver: Nullable<Observer<any>>;
-    private _stateObserver: Nullable<Observer<any>>;
+    private _sessionEndedObserver: Nullable<Observer<any>>;
+    private _sessionInitObserver: Nullable<Observer<any>>;
     /**
      * Event when a controller has been connected/added
      */
@@ -26,37 +36,35 @@ export class WebXRInput implements IDisposable {
 
     /**
      * Initializes the WebXRInput
-     * @param baseExperience experience helper which the input should be created for
+     * @param xrSessionManager the xr session manager for this session
+     * @param xrCamera the WebXR camera for this session. Mainly used for teleportation
+     * @param options = initialization options for this xr input
      */
     public constructor(
         /**
-         * Base experience the input listens to
+         * the xr session manager for this session
+         */
+        public xrSessionManager: WebXRSessionManager,
+        /**
+         * the WebXR camera for this session. Mainly used for teleportation
          */
-        public baseExperience: WebXRExperienceHelper
+        public xrCamera: WebXRCamera,
+        private readonly options: IWebXRInputOptions = {}
     ) {
         // Remove controllers when exiting XR
-        this._stateObserver = baseExperience.onStateChangedObservable.add((s) => {
-            if (s === WebXRState.NOT_IN_XR) {
-                this._addAndRemoveControllers([], this.controllers.map((c) => {return c.inputSource; }));
-            }
+        this._sessionEndedObserver = this.xrSessionManager.onXRSessionEnded.add(() => {
+            this._addAndRemoveControllers([], this.controllers.map((c) => { return c.inputSource; }));
         });
 
-        this._frameObserver = baseExperience.sessionManager.onXRFrameObservable.add(() => {
-            if (!baseExperience.sessionManager.currentFrame) {
-                return;
-            }
-
-            // Start listing to input add/remove event
-            if (this.controllers.length == 0 && baseExperience.sessionManager.session.inputSources && baseExperience.sessionManager.session.inputSources.length > 0) {
-                this._addAndRemoveControllers(baseExperience.sessionManager.session.inputSources, []);
-                baseExperience.sessionManager.session.addEventListener("inputsourceschange", this._onInputSourcesChange);
-            }
+        this._sessionInitObserver = this.xrSessionManager.onXRSessionInit.add((session) => {
+            session.addEventListener("inputsourceschange", this._onInputSourcesChange);
+        });
 
+        this._frameObserver = this.xrSessionManager.onXRFrameObservable.add((frame) => {
             // Update controller pose info
             this.controllers.forEach((controller) => {
-                controller.updateFromXRFrame(baseExperience.sessionManager.currentFrame!, baseExperience.sessionManager.referenceSpace);
+                controller.updateFromXRFrame(frame, this.xrSessionManager.referenceSpace);
             });
-
         });
     }
 
@@ -66,11 +74,14 @@ export class WebXRInput implements IDisposable {
 
     private _addAndRemoveControllers(addInputs: Array<XRInputSource>, removeInputs: Array<XRInputSource>) {
         // Add controllers if they don't already exist
-        let sources = this.controllers.map((c) => {return c.inputSource; });
+        let sources = this.controllers.map((c) => { return c.inputSource; });
         for (let input of addInputs) {
             if (sources.indexOf(input) === -1) {
-                let controller = new WebXRController(this.baseExperience.camera._scene, input);
+                let controller = new WebXRController(this.xrSessionManager.scene, input);
                 this.controllers.push(controller);
+                if (!this.options.doNotLoadControllerMeshes && controller.gamepadController) {
+                    controller.gamepadController.loadModel();
+                }
                 this.onControllerAddedObservable.notifyObservers(controller);
             }
         }
@@ -81,7 +92,7 @@ export class WebXRInput implements IDisposable {
         this.controllers.forEach((c) => {
             if (removeInputs.indexOf(c.inputSource) === -1) {
                 keepControllers.push(c);
-            }else {
+            } else {
                 removedControllers.push(c);
             }
         });
@@ -100,7 +111,8 @@ export class WebXRInput implements IDisposable {
         this.controllers.forEach((c) => {
             c.dispose();
         });
-        this.baseExperience.sessionManager.onXRFrameObservable.remove(this._frameObserver);
-        this.baseExperience.onStateChangedObservable.remove(this._stateObserver);
+        this.xrSessionManager.onXRFrameObservable.remove(this._frameObserver);
+        this.xrSessionManager.onXRSessionInit.remove(this._sessionInitObserver);
+        this.xrSessionManager.onXRSessionEnded.remove(this._sessionEndedObserver);
     }
 }