소스 검색

Merge pull request #7507 from RaananW/motion-controller-profiles

Profiles motion controller and the input repository
David Catuhe 5 년 전
부모
커밋
c27f390d4e

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

@@ -202,6 +202,7 @@
 - WebXR camera is initialized on the first frame, including copying transformation from native camera (except for in AR) ([#7389](https://github.com/BabylonJS/Babylon.js/issues/7389)) ([RaananW](https://github.com/RaananW/))
 - Selection has gaze mode (which can be forced) and touch-screen support ([#7395](https://github.com/BabylonJS/Babylon.js/issues/7395)) ([RaananW](https://github.com/RaananW/))
 - Laser pointers can be excluded from lighting influence so that they are always visible in both WebXR and WebVR ([#7323](https://github.com/BabylonJS/Babylon.js/issues/7323)) ([RaananW](https://github.com/RaananW/))
+- Full support for the online motion controller repository ([#7323](https://github.com/BabylonJS/Babylon.js/issues/7323)) ([RaananW](https://github.com/RaananW/))
 
 ### Ray
 

+ 36 - 38
src/Cameras/XR/features/WebXRControllerPointerSelection.ts

@@ -357,53 +357,51 @@ export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
     }
 
     private _attachTrackedPointerRayMode(xrController: WebXRController) {
-        if (!xrController.motionController) {
-            return;
-        }
-
-        if (this._options.forceGazeMode) {
-            return this._attachGazeMode(xrController);
-        }
-
-        const controllerData = this._controllers[xrController.uniqueId];
-
-        if (this._options.overrideButtonId) {
-            controllerData.selectionComponent = xrController.motionController.getComponent(this._options.overrideButtonId);
-        }
-        if (!controllerData.selectionComponent) {
-            controllerData.selectionComponent = xrController.motionController.getMainComponent();
-        }
-
-        controllerData.onFrameObserver = this._xrSessionManager.onXRFrameObservable.add(() => {
-            if (controllerData.selectionComponent && controllerData.selectionComponent.pressed) {
-                (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshPickedColor;
-                (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.laserPointerPickedColor;
-            } else {
-                (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshDefaultColor;
-                (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.lasterPointerDefaultColor;
+        xrController.onMotionControllerProfileLoaded.add((motionController) => {
+            if (this._options.forceGazeMode) {
+                return this._attachGazeMode(xrController);
             }
 
-            controllerData.laserPointer.isVisible = this.displayLaserPointer;
-            (<StandardMaterial>controllerData.laserPointer.material).disableLighting = this.disablePointerLighting;
-            (<StandardMaterial>controllerData.selectionMesh.material).disableLighting = this.disableSelectionMeshLighting;
+            const controllerData = this._controllers[xrController.uniqueId];
 
-            if (controllerData.pick) {
-                this._scene.simulatePointerMove(controllerData.pick, { pointerId: controllerData.id });
+            if (this._options.overrideButtonId) {
+                controllerData.selectionComponent = motionController.getComponent(this._options.overrideButtonId);
+            }
+            if (!controllerData.selectionComponent) {
+                controllerData.selectionComponent = motionController.getMainComponent();
             }
-        });
 
-        controllerData.onButtonChangedObserver = controllerData.selectionComponent.onButtonStateChanged.add((component) => {
-            if (component.changes.pressed) {
-                const pressed = component.changes.pressed.current;
+            controllerData.onFrameObserver = this._xrSessionManager.onXRFrameObservable.add(() => {
+                if (controllerData.selectionComponent && controllerData.selectionComponent.pressed) {
+                    (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshPickedColor;
+                    (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.laserPointerPickedColor;
+                } else {
+                    (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshDefaultColor;
+                    (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.lasterPointerDefaultColor;
+                }
+                controllerData.laserPointer.isVisible = this.displayLaserPointer;
+                (<StandardMaterial>controllerData.laserPointer.material).disableLighting = this.disablePointerLighting;
+                (<StandardMaterial>controllerData.selectionMesh.material).disableLighting = this.disableSelectionMeshLighting;
+
                 if (controllerData.pick) {
-                    if (pressed) {
-                        this._scene.simulatePointerDown(controllerData.pick, { pointerId: controllerData.id });
-                    } else {
-                        this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
+                    this._scene.simulatePointerMove(controllerData.pick, { pointerId: controllerData.id });
+                }
+            });
+
+            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 });
+                        } else {
+                            this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
+                        }
                     }
                 }
-            }
+            });
         });
+
     }
 
     private _detachController(xrControllerUniqueId: string) {

+ 99 - 97
src/Cameras/XR/features/WebXRControllerTeleportation.ts

@@ -316,112 +316,114 @@ export class WebXRMotionControllerTeleportation extends WebXRAbstractFeature {
         };
         const controllerData = this._controllers[xrController.uniqueId];
         // motion controller support
-        if (xrController.motionController) {
-            const movementController = xrController.motionController.getComponent(WebXRControllerComponent.THUMBSTICK) || xrController.motionController.getComponent(WebXRControllerComponent.TOUCHPAD);
-            if (!movementController || this._options.useMainComponentOnly) {
-                // use trigger to move on long press
-                const mainComponent = xrController.motionController.getMainComponent();
-                if (!mainComponent) {
-                    return;
-                }
-                controllerData.onButtonChangedObserver = mainComponent.onButtonStateChanged.add(() => {
-                    // did "pressed" changed?
-                    if (mainComponent.changes.pressed) {
-                        if (mainComponent.changes.pressed.current) {
-                            // simulate "forward" thumbstick push
+        xrController.onMotionControllerProfileLoaded.addOnce(() => {
+            if (xrController.motionController) {
+                const movementController = xrController.motionController.getComponentOfType(WebXRControllerComponent.THUMBSTICK) || xrController.motionController.getComponentOfType(WebXRControllerComponent.TOUCHPAD);
+                if (!movementController || this._options.useMainComponentOnly) {
+                    // use trigger to move on long press
+                    const mainComponent = xrController.motionController.getMainComponent();
+                    if (!mainComponent) {
+                        return;
+                    }
+                    controllerData.onButtonChangedObserver = mainComponent.onButtonStateChanged.add(() => {
+                        // did "pressed" changed?
+                        if (mainComponent.changes.pressed) {
+                            if (mainComponent.changes.pressed.current) {
+                                // simulate "forward" thumbstick push
+                                controllerData.teleportationState.forward = true;
+                                this._currentTeleportationControllerId = controllerData.xrController.uniqueId;
+                                controllerData.teleportationState.baseRotation = this._options.xrInput.xrCamera.rotationQuaternion.toEulerAngles().y;
+                                controllerData.teleportationState.currentRotation = 0;
+                                const timeToSelect = this._options.timeToTeleport || 3000;
+                                let timer = 0;
+                                const observer = this._xrSessionManager.onXRFrameObservable.add(() => {
+                                    if (!mainComponent.pressed) {
+                                        this._xrSessionManager.onXRFrameObservable.remove(observer);
+                                        return;
+                                    }
+                                    timer += this._xrSessionManager.scene.getEngine().getDeltaTime();
+                                    if (timer >= timeToSelect && this._currentTeleportationControllerId === controllerData.xrController.uniqueId && controllerData.teleportationState.forward) {
+                                        this._teleportForward(xrController.uniqueId);
+                                    }
+
+                                    // failsafe
+                                    if (timer >= timeToSelect) {
+                                        this._xrSessionManager.onXRFrameObservable.remove(observer);
+                                    }
+                                });
+                            } else {
+                                controllerData.teleportationState.forward = false;
+                                this._currentTeleportationControllerId = "";
+                            }
+                        }
+                    });
+                } else {
+                    controllerData.onButtonChangedObserver = movementController.onButtonStateChanged.add(() => {
+                        if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId && controllerData.teleportationState.forward && !movementController.touched) {
+                            this._teleportForward(xrController.uniqueId);
+                        }
+                    });
+                    // 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._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;
-                            controllerData.teleportationState.currentRotation = 0;
-                            const timeToSelect = this._options.timeToTeleport || 3000;
-                            let timer = 0;
-                            const observer = this._xrSessionManager.onXRFrameObservable.add(() => {
-                                if (!mainComponent.pressed) {
-                                    this._xrSessionManager.onXRFrameObservable.remove(observer);
-                                    return;
-                                }
-                                timer += this._xrSessionManager.scene.getEngine().getDeltaTime();
-                                if (timer >= timeToSelect && this._currentTeleportationControllerId === controllerData.xrController.uniqueId && controllerData.teleportationState.forward) {
-                                    this._teleportForward(xrController.uniqueId);
+                        }
+                        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));
                                 }
-
-                                // failsafe
-                                if (timer >= timeToSelect) {
-                                    this._xrSessionManager.onXRFrameObservable.remove(observer);
+                            } 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.currentRotation = 0;
+                                    }
                                 }
-                            });
-                        } else {
-                            controllerData.teleportationState.forward = false;
-                            this._currentTeleportationControllerId = "";
-                        }
-                    }
-                });
-            } else {
-                controllerData.onButtonChangedObserver = movementController.onButtonStateChanged.add(() => {
-                    if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId && controllerData.teleportationState.forward && !movementController.touched) {
-                        this._teleportForward(xrController.uniqueId);
-                    }
-                });
-                // 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._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.currentRotation = 0;
-                                }
-                            }
+                            controllerData.teleportationState.rotating = false;
                         }
-                    } else {
-                        controllerData.teleportationState.rotating = false;
-                    }
-                });
+                    });
+                }
             }
-        }
+        });
     }
 
     private _teleportForward(controllerId: string) {

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

@@ -4,4 +4,5 @@ export * from "./webXRGenericMotionController";
 export * from "./webXRMicrosoftMixedRealityController";
 export * from "./webXRMotionControllerManager";
 export * from "./webXROculusTouchMotionController";
-export * from "./webXRHTCViveMotionController";
+export * from "./webXRHTCViveMotionController";
+export * from "./webXRProfiledMotionController";

+ 123 - 86
src/Cameras/XR/motionController/webXRAbstractController.ts

@@ -11,7 +11,7 @@ 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";
+export type MotionControllerHandness = "none" | "left" | "right";
 /**
  * The type of components available in motion controllers.
  * This is not the name of the component.
@@ -19,6 +19,11 @@ export type MotionControllerHandness = "none" | "left" | "right" | "left-right"
 export type MotionControllerComponentType = "trigger" | "squeeze" | "touchpad" | "thumbstick" | "button";
 
 /**
+ * The state of a controller component
+ */
+export type MotionControllerComponentStateType = "default" | "touched" | "pressed";
+
+/**
  * The schema of motion controller layout.
  * No object will be initialized using this interface
  * This is used just to define the profile.
@@ -40,38 +45,76 @@ export interface IMotionControllerLayout {
              * 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
+             * The indices of this component in the gamepad object
+             */
+            gamepadIndices: {
+                /**
+                 * Index of button
+                 */
+                button?: number;
+                /**
+                 * If available, index of x-axis
+                 */
+                xAxis?: number;
+                /**
+                 * If available, index of y-axis
+                 */
+                yAxis?: number;
+            };
+            /**
+             * The mesh's root node name
              */
-            componentId: string;
+            rootNodeName: string;
             /**
-             * X or Y Axis
+             * Animation definitions for this model
              */
-            axis: "x-axis" | "y-axis";
-        } | null>;
+            visualResponses: {
+                [stateKey: string]: {
+                    /**
+                     * What property will be animated
+                     */
+                    componentProperty: "xAxis" | "yAxis" | "button" | "state";
+                    /**
+                     * What states influence this visual reponse
+                     */
+                    states: MotionControllerComponentStateType[];
+                    /**
+                     * Type of animation - movement or visibility
+                     */
+                    valueNodeProperty: "transform" | "visibility";
+                    /**
+                     * Base node name to move. Its position will be calculated according to the min and max nodes
+                     */
+                    valueNodeName?: string;
+                    /**
+                     * Minimum movement node
+                     */
+                    minNodeName?: string;
+                    /**
+                     * Max movement node
+                     */
+                    maxNodeName?: string;
+                }
+            }
+            /**
+             * If touch enabled, what is the name of node to display user feedback
+             */
+            touchPointNodeName?: string;
+        }
     };
+    /**
+     * Is it xr standard mapping or not
+     */
+    gamepadMapping: "" | "xr-standard";
+    /**
+     * Base root node of this entire model
+     */
+    rootNodeName: string;
+    /**
+     * Path to load the assets. Usually relative to the base path
+     */
+    assetPath: string;
 }
 
 /**
@@ -131,7 +174,7 @@ export interface IMotionControllerButtonMeshMap {
  * 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 {
+export interface IMotionControllerMeshMap {
     /**
      * The mesh that will be changed when axis value changes
      */
@@ -139,11 +182,11 @@ export interface IMotionControllerAxisMeshMap {
     /**
      * the mesh that defines the minimum value mesh position.
      */
-    minMesh: AbstractMesh;
+    minMesh?: AbstractMesh;
     /**
      * the mesh that defines the maximum value mesh position.
      */
-    maxMesh: AbstractMesh;
+    maxMesh?: AbstractMesh;
 }
 
 /**
@@ -181,17 +224,6 @@ export interface IMinimalMotionControllerObject {
 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;
@@ -214,6 +246,11 @@ export abstract class WebXRAbstractMotionController implements IDisposable {
      */
     public rootMesh: Nullable<AbstractMesh>;
 
+    /**
+     * Disable the model's animation. Can be set at any time.
+     */
+    public disableAnimation: boolean = false;
+
     private _modelReady: boolean = false;
 
     /**
@@ -235,27 +272,23 @@ export abstract class WebXRAbstractMotionController implements IDisposable {
         public handness: MotionControllerHandness,
         _doNotLoadControllerMesh: boolean = false) {
         // initialize the components
-        if (layout.gamepad) {
-            layout.gamepad.buttons.forEach(this._initComponent);
+        if (layout.components) {
+            Object.keys(layout.components).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);
+    private _initComponent = (id: string) => {
+        if (!id) { return; }
+        const componentDef = this.layout.components[id];
+        const type = componentDef.type;
+        const buttonIndex = componentDef.gamepadIndices.button;
         // 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;
-                }
-            }
-        });
+        if (componentDef.gamepadIndices.xAxis !== undefined && componentDef.gamepadIndices.yAxis !== undefined) {
+            axes.push(componentDef.gamepadIndices.xAxis, componentDef.gamepadIndices.yAxis);
+        }
+
         this.components[id] = new WebXRControllerComponent(id, type, buttonIndex, axes);
     }
 
@@ -264,7 +297,7 @@ export abstract class WebXRAbstractMotionController implements IDisposable {
      * @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.getComponentIds().forEach((id) => this.getComponent(id).update(this.gamepadObject));
         this.updateModel(xrFrame);
     }
 
@@ -272,7 +305,7 @@ export abstract class WebXRAbstractMotionController implements IDisposable {
      * Get the list of components available in this motion controller
      * @returns an array of strings correlating to available components
      */
-    public getComponentTypes(): string[] {
+    public getComponentIds(): string[] {
         return Object.keys(this.components);
     }
 
@@ -294,6 +327,24 @@ export abstract class WebXRAbstractMotionController implements IDisposable {
     }
 
     /**
+     * Get the first component of specific type
+     * @param type type of component to find
+     * @return a controller component or null if not found
+     */
+    public getComponentOfType(type: MotionControllerComponentType): Nullable<WebXRControllerComponent> {
+        return this.getAllComponentsOfType(type)[0] || null;
+    }
+
+    /**
+     * Returns all components of specific type
+     * @param type the type to search for
+     * @return an array of components with this type
+     */
+    public getAllComponentsOfType(type: MotionControllerComponentType): WebXRControllerComponent[] {
+        return this.getComponentIds().map((id) => this.components[id]).filter((component) => component.type === type);
+    }
+
+    /**
      * 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
@@ -343,14 +394,17 @@ export abstract class WebXRAbstractMotionController implements IDisposable {
      * @param axisValue the value of the axis which determines the meshes new position
      * @hidden
      */
-    protected _lerpAxisTransform(axisMap: IMotionControllerAxisMeshMap, axisValue: number): void {
+    protected _lerpTransform(axisMap: IMotionControllerMeshMap, axisValue: number, fixValueCoordinates?: boolean): void {
+        if (!axisMap.minMesh || !axisMap.maxMesh) {
+            return;
+        }
 
         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;
+        let lerpValue = fixValueCoordinates ? axisValue * 0.5 + 0.5 : axisValue;
         Quaternion.SlerpToRef(
             axisMap.minMesh.rotationQuaternion,
             axisMap.maxMesh.rotationQuaternion,
@@ -363,30 +417,13 @@ export abstract class WebXRAbstractMotionController implements IDisposable {
             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);
+    // Look through all children recursively. This will return null if no mesh exists with the given name.
+    protected _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.
+    protected _getImmediateChildByName(node: AbstractMesh, name: string): AbstractMesh {
+        return <AbstractMesh>node.getChildren((n) => n.name == name, true)[0];
     }
 
     private _getGenericFilenameAndPath(): { filename: string, path: string } {
@@ -442,7 +479,7 @@ export abstract class WebXRAbstractMotionController implements IDisposable {
      * Dispose this controller, the model mesh and all its components
      */
     public dispose(): void {
-        this.getComponentTypes().forEach((id) => this.getComponent(id).dispose());
+        this.getComponentIds().forEach((id) => this.getComponent(id).dispose());
         if (this.rootMesh) {
             this.rootMesh.dispose();
         }

+ 22 - 4
src/Cameras/XR/motionController/webXRControllerComponent.ts

@@ -59,19 +59,23 @@ export class WebXRControllerComponent implements IDisposable {
     /**
      * Thumbstick component type
      */
-    public static THUMBSTICK = "xr-standard-thumbstick";
+    public static THUMBSTICK: MotionControllerComponentType = "thumbstick";
     /**
      * Touchpad component type
      */
-    public static TOUCHPAD = "xr-standard-touchpad";
+    public static TOUCHPAD: MotionControllerComponentType = "touchpad";
     /**
      * trigger component type
      */
-    public static TRIGGER = "xr-standard-trigger";
+    public static TRIGGER: MotionControllerComponentType = "trigger";
     /**
      * squeeze component type
      */
-    public static SQUEEZE = "xr-standard-squeeze";
+    public static SQUEEZE: MotionControllerComponentType = "squeeze";
+    /**
+     * button component type
+     */
+    public static BUTTON: MotionControllerComponentType = "button";
     /**
      * Observers registered here will be triggered when the state of a button changes
      * State change is either pressed / touched / value
@@ -91,6 +95,13 @@ export class WebXRControllerComponent implements IDisposable {
         y: 0
     };
     private _changes: IWebXRMotionControllerComponentChanges = {};
+    private _hasChanges: boolean = false;
+    /**
+     * Return whether or not the component changed the last frame
+     */
+    public get hasChanges(): boolean {
+        return this._hasChanges;
+    }
 
     /**
      * Creates a new component for a motion controller.
@@ -173,10 +184,15 @@ export class WebXRControllerComponent implements IDisposable {
     public update(nativeController: IMinimalMotionControllerObject) {
         let buttonUpdated = false;
         let axesUpdate = false;
+        this._hasChanges = false;
         this._changes = {};
 
         if (this.isButton()) {
             const button = nativeController.buttons[this._buttonIndex];
+            // defensive, in case a profile was forced
+            if (!button) {
+                return;
+            }
             if (this._currentValue !== button.value) {
                 this.changes.value = {
                     current: button.value,
@@ -240,9 +256,11 @@ export class WebXRControllerComponent implements IDisposable {
         }
 
         if (buttonUpdated) {
+            this._hasChanges = true;
             this.onButtonStateChanged.notifyObservers(this);
         }
         if (axesUpdate) {
+            this._hasChanges = true;
             this.onAxisValueChanged.notifyObservers(this._axes);
         }
     }

+ 54 - 23
src/Cameras/XR/motionController/webXRGenericMotionController.ts

@@ -9,27 +9,6 @@ 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
  */
@@ -42,7 +21,7 @@ export class WebXRGenericTriggerMotionController extends WebXRAbstractMotionCont
     public profileId = WebXRGenericTriggerMotionController.ProfileId;
 
     constructor(scene: Scene, gamepadObject: IMinimalMotionControllerObject, handness: MotionControllerHandness) {
-        super(scene, GenericTriggerLayout["left-right-none"], gamepadObject, handness);
+        super(scene, GenericTriggerLayout[handness], gamepadObject, handness);
     }
 
     protected _processLoadedModel(meshes: AbstractMesh[]): void {
@@ -77,4 +56,56 @@ export class WebXRGenericTriggerMotionController extends WebXRAbstractMotionCont
         return true;
     }
 
-}
+}
+
+// https://github.com/immersive-web/webxr-input-profiles/blob/master/packages/registry/profiles/generic/generic-trigger-touchpad-thumbstick.json
+const GenericTriggerLayout: IMotionControllerLayoutMap = {
+    "left": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {}
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "generic-trigger-left",
+        "assetPath": "left.glb"
+    },
+    "right": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {}
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "generic-trigger-right",
+        "assetPath": "right.glb"
+    },
+    "none": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {}
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "generic-trigger-none",
+        "assetPath": "none.glb"
+    }
+};

+ 168 - 69
src/Cameras/XR/motionController/webXRHTCViveMotionController.ts

@@ -10,57 +10,6 @@ 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
  */
@@ -78,22 +27,27 @@ export class WebXRHTCViveMotionController extends WebXRAbstractMotionController
 
     private _modelRootNode: AbstractMesh;
 
+    /**
+     * Create a new Vive motion controller object
+     * @param scene the scene to use to create this controller
+     * @param gamepadObject the corresponding gamepad object
+     * @param handness the handness of the controller
+     */
     constructor(scene: Scene,
         gamepadObject: IMinimalMotionControllerObject,
-        handness: MotionControllerHandness,
-        legacyMapping: boolean = false) {
-        super(scene, legacyMapping ? HTCViveLegacyLayout["left-right-none"] : HTCViveLayout["left-right-none"], gamepadObject, handness);
+        handness: MotionControllerHandness) {
+        super(scene, HTCViveLayout[handness], gamepadObject, handness);
     }
 
     protected _processLoadedModel(_meshes: AbstractMesh[]): void {
-        this.layout.gamepad!.buttons.forEach((buttonName) => {
-            const comp = buttonName && this.getComponent(buttonName);
+        this.getComponentIds().forEach((id) => {
+            const comp = id && this.getComponent(id);
             if (comp) {
                 comp.onButtonStateChanged.add((component) => {
 
-                    if (!this.rootMesh) { return; }
+                    if (!this.rootMesh || this.disableAnimation) { return; }
 
-                    switch (buttonName) {
+                    switch (id) {
                         case "xr-standard-trigger":
                             (<AbstractMesh>(this._modelRootNode.getChildren()[6])).rotation.x = -component.value * 0.15;
                             return;
@@ -101,14 +55,6 @@ export class WebXRHTCViveMotionController extends WebXRAbstractMotionController
                             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);
             }
@@ -149,6 +95,159 @@ WebXRMotionControllerManager.RegisterController("htc-vive", (xrInput: XRInputSou
     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);
-});
+// WebXRMotionControllerManager.RegisterController("htc-vive-legacy", (xrInput: XRInputSource, scene: Scene) => {
+//     return new WebXRHTCViveMotionController(scene, <any>(xrInput.gamepad), xrInput.handedness, true);
+// });
+
+const HTCViveLayout: IMotionControllerLayoutMap = {
+    "left": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-squeeze": {
+                "type": "squeeze",
+                "gamepadIndices": {
+                    "button": 1
+                },
+                "rootNodeName": "xr_standard_squeeze",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-touchpad": {
+                "type": "touchpad",
+                "gamepadIndices": {
+                    "button": 2,
+                    "xAxis": 0,
+                    "yAxis": 1
+                },
+                "rootNodeName": "xr_standard_touchpad",
+                "visualResponses": {
+
+                },
+            },
+            "menu": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 4
+                },
+                "rootNodeName": "menu",
+                "visualResponses": {
+
+                }
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "htc_vive_none",
+        "assetPath": "none.glb"
+    },
+    "right": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-squeeze": {
+                "type": "squeeze",
+                "gamepadIndices": {
+                    "button": 1
+                },
+                "rootNodeName": "xr_standard_squeeze",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-touchpad": {
+                "type": "touchpad",
+                "gamepadIndices": {
+                    "button": 2,
+                    "xAxis": 0,
+                    "yAxis": 1
+                },
+                "rootNodeName": "xr_standard_touchpad",
+                "visualResponses": {
+
+                },
+            },
+            "menu": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 4
+                },
+                "rootNodeName": "menu",
+                "visualResponses": {
+
+                }
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "htc_vive_none",
+        "assetPath": "none.glb"
+    },
+    "none": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-squeeze": {
+                "type": "squeeze",
+                "gamepadIndices": {
+                    "button": 1
+                },
+                "rootNodeName": "xr_standard_squeeze",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-touchpad": {
+                "type": "touchpad",
+                "gamepadIndices": {
+                    "button": 2,
+                    "xAxis": 0,
+                    "yAxis": 1
+                },
+                "rootNodeName": "xr_standard_touchpad",
+                "visualResponses": {
+
+                },
+            },
+            "menu": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 4
+                },
+                "rootNodeName": "menu",
+                "visualResponses": {
+
+                }
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "htc-vive-none",
+        "assetPath": "none.glb"
+    }
+};

+ 402 - 75
src/Cameras/XR/motionController/webXRMicrosoftMixedRealityController.ts

@@ -7,38 +7,10 @@ import {
 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" }
-        },
-        "gamepad": {
-            "mapping": "xr-standard",
-            "buttons": [
-                "xr-standard-trigger",
-                "xr-standard-squeeze",
-                "xr-standard-touchpad",
-                "xr-standard-thumbstick"
-            ],
-            "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" }
-            ]
-        }
-    }
-};
+import { Logger } from '../../../Misc/logger';
 
 /**
  * The motion controller class for all microsoft mixed reality controllers
@@ -121,20 +93,22 @@ export class WebXRMicrosoftMixedRealityController extends WebXRAbstractMotionCon
         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];
+        this.getComponentIds().forEach((id, i) => {
+            if (this.disableAnimation) {
+                return;
+            }
+            if (id && this.rootMesh) {
+                const buttonMap = (<any>this._mapping.buttons)[id];
                 const buttonMeshName = buttonMap.rootNodeName;
                 if (!buttonMeshName) {
-                    Logger.Log('Skipping unknown button at index: ' + i + ' with mapped name: ' + buttonName);
-                    continue;
+                    Logger.Log('Skipping unknown button at index: ' + i + ' with mapped name: ' + id);
+                    return;
                 }
 
                 var buttonMesh = this._getChildByName(this.rootMesh, buttonMeshName);
                 if (!buttonMesh) {
                     Logger.Warn('Missing button mesh with name: ' + buttonMeshName);
-                    continue;
+                    return;
                 }
 
                 buttonMap.valueMesh = this._getImmediateChildByName(buttonMesh, this._mapping.defaultButton.valueNodeName);
@@ -142,10 +116,10 @@ export class WebXRMicrosoftMixedRealityController extends WebXRAbstractMotionCon
                 buttonMap.unpressedMesh = this._getImmediateChildByName(buttonMesh, this._mapping.defaultButton.unpressedNodeName);
 
                 if (buttonMap.valueMesh && buttonMap.pressedMesh && buttonMap.unpressedMesh) {
-                    const comp = this.getComponent(buttonName);
+                    const comp = this.getComponent(id);
                     if (comp) {
                         comp.onButtonStateChanged.add((component) => {
-                            this._lerpButtonTransform(buttonMap, component.value);
+                            this._lerpTransform(buttonMap, component.value);
                         }, undefined, true);
                     }
                 } else {
@@ -154,51 +128,44 @@ export class WebXRMicrosoftMixedRealityController extends WebXRAbstractMotionCon
                 }
             }
 
-        }
+        });
 
         // 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;
+        this.getComponentIds().forEach((id, i) => {
+            const comp = this.getComponent(id);
+            if (!comp.isAxes()) {
+                return;
             }
 
-            const axisMap = (<any>this._mapping.axes)[axisData.componentId][axisData.axis];
+            ["x-axis", "y-axis"].forEach((axis) => {
+                if (!this.rootMesh) { return; }
+                const axisMap = (<any>this._mapping.axes)[id][axis];
 
-            var axisMesh = this._getChildByName(this.rootMesh, axisMap.rootNodeName);
-            if (!axisMesh) {
-                Logger.Warn('Missing axis mesh with name: ' + axisMap.rootNodeName);
-                continue;
-            }
+                var axisMesh = this._getChildByName(this.rootMesh, axisMap.rootNodeName);
+                if (!axisMesh) {
+                    Logger.Warn('Missing axis mesh with name: ' + axisMap.rootNodeName);
+                    return;
+                }
 
-            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);
+                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);
-                }
+                if (axisMap.valueMesh && axisMap.minMesh && axisMap.maxMesh) {
+                    if (comp) {
+                        comp.onAxisValueChanged.add((axisValues) => {
+                            const value = axis === "x-axis" ? axisValues.x : axisValues.y;
+                            this._lerpTransform(axisMap, value, true);
+                        }, 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);
-            }
-        }
-    }
+                } 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; } {
@@ -254,4 +221,364 @@ export class WebXRMicrosoftMixedRealityController extends WebXRAbstractMotionCon
 // register the profile
 WebXRMotionControllerManager.RegisterController("windows-mixed-reality", (xrInput: XRInputSource, scene: Scene) => {
     return new WebXRMicrosoftMixedRealityController(scene, <any>(xrInput.gamepad), xrInput.handedness);
-});
+});
+
+// https://github.com/immersive-web/webxr-input-profiles/blob/master/packages/registry/profiles/microsoft/microsoft-mixed-reality.json
+const MixedRealityProfile: IMotionControllerLayoutMap = {
+    "left": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {
+                    "xr_standard_trigger_pressed": {
+                        "componentProperty": "button",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_trigger_pressed_value",
+                        "minNodeName": "xr_standard_trigger_pressed_min",
+                        "maxNodeName": "xr_standard_trigger_pressed_max"
+                    }
+                }
+            },
+            "xr-standard-squeeze": {
+                "type": "squeeze",
+                "gamepadIndices": {
+                    "button": 1
+                },
+                "rootNodeName": "xr_standard_squeeze",
+                "visualResponses": {
+                    "xr_standard_squeeze_pressed": {
+                        "componentProperty": "button",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_squeeze_pressed_value",
+                        "minNodeName": "xr_standard_squeeze_pressed_min",
+                        "maxNodeName": "xr_standard_squeeze_pressed_max"
+                    }
+                }
+            },
+            "xr-standard-touchpad": {
+                "type": "touchpad",
+                "gamepadIndices": {
+                    "button": 2,
+                    "xAxis": 0,
+                    "yAxis": 1
+                },
+                "rootNodeName": "xr_standard_touchpad",
+                "visualResponses": {
+                    "xr_standard_touchpad_pressed": {
+                        "componentProperty": "button",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_pressed_value",
+                        "minNodeName": "xr_standard_touchpad_pressed_min",
+                        "maxNodeName": "xr_standard_touchpad_pressed_max"
+                    },
+                    "xr_standard_touchpad_xaxis_pressed": {
+                        "componentProperty": "xAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_xaxis_pressed_value",
+                        "minNodeName": "xr_standard_touchpad_xaxis_pressed_min",
+                        "maxNodeName": "xr_standard_touchpad_xaxis_pressed_max"
+                    },
+                    "xr_standard_touchpad_yaxis_pressed": {
+                        "componentProperty": "yAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_yaxis_pressed_value",
+                        "minNodeName": "xr_standard_touchpad_yaxis_pressed_min",
+                        "maxNodeName": "xr_standard_touchpad_yaxis_pressed_max"
+                    },
+                    "xr_standard_touchpad_xaxis_touched": {
+                        "componentProperty": "xAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_xaxis_touched_value",
+                        "minNodeName": "xr_standard_touchpad_xaxis_touched_min",
+                        "maxNodeName": "xr_standard_touchpad_xaxis_touched_max"
+                    },
+                    "xr_standard_touchpad_yaxis_touched": {
+                        "componentProperty": "yAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_yaxis_touched_value",
+                        "minNodeName": "xr_standard_touchpad_yaxis_touched_min",
+                        "maxNodeName": "xr_standard_touchpad_yaxis_touched_max"
+                    },
+                    "xr_standard_touchpad_axes_touched": {
+                        "componentProperty": "state",
+                        "states": [
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "visibility",
+                        "valueNodeName": "xr_standard_touchpad_axes_touched_value"
+                    }
+                },
+                "touchPointNodeName": "xr_standard_touchpad_axes_touched_value"
+            },
+            "xr-standard-thumbstick": {
+                "type": "thumbstick",
+                "gamepadIndices": {
+                    "button": 3,
+                    "xAxis": 2,
+                    "yAxis": 3
+                },
+                "rootNodeName": "xr_standard_thumbstick",
+                "visualResponses": {
+                    "xr_standard_thumbstick_pressed": {
+                        "componentProperty": "button",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_thumbstick_pressed_value",
+                        "minNodeName": "xr_standard_thumbstick_pressed_min",
+                        "maxNodeName": "xr_standard_thumbstick_pressed_max"
+                    },
+                    "xr_standard_thumbstick_xaxis_pressed": {
+                        "componentProperty": "xAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_thumbstick_xaxis_pressed_value",
+                        "minNodeName": "xr_standard_thumbstick_xaxis_pressed_min",
+                        "maxNodeName": "xr_standard_thumbstick_xaxis_pressed_max"
+                    },
+                    "xr_standard_thumbstick_yaxis_pressed": {
+                        "componentProperty": "yAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_thumbstick_yaxis_pressed_value",
+                        "minNodeName": "xr_standard_thumbstick_yaxis_pressed_min",
+                        "maxNodeName": "xr_standard_thumbstick_yaxis_pressed_max"
+                    }
+                }
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "microsoft-mixed-reality-left",
+        "assetPath": "left.glb"
+    },
+    "right": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {
+                    "xr_standard_trigger_pressed": {
+                        "componentProperty": "button",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_trigger_pressed_value",
+                        "minNodeName": "xr_standard_trigger_pressed_min",
+                        "maxNodeName": "xr_standard_trigger_pressed_max"
+                    }
+                }
+            },
+            "xr-standard-squeeze": {
+                "type": "squeeze",
+                "gamepadIndices": {
+                    "button": 1
+                },
+                "rootNodeName": "xr_standard_squeeze",
+                "visualResponses": {
+                    "xr_standard_squeeze_pressed": {
+                        "componentProperty": "button",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_squeeze_pressed_value",
+                        "minNodeName": "xr_standard_squeeze_pressed_min",
+                        "maxNodeName": "xr_standard_squeeze_pressed_max"
+                    }
+                }
+            },
+            "xr-standard-touchpad": {
+                "type": "touchpad",
+                "gamepadIndices": {
+                    "button": 2,
+                    "xAxis": 0,
+                    "yAxis": 1
+                },
+                "rootNodeName": "xr_standard_touchpad",
+                "visualResponses": {
+                    "xr_standard_touchpad_pressed": {
+                        "componentProperty": "button",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_pressed_value",
+                        "minNodeName": "xr_standard_touchpad_pressed_min",
+                        "maxNodeName": "xr_standard_touchpad_pressed_max"
+                    },
+                    "xr_standard_touchpad_xaxis_pressed": {
+                        "componentProperty": "xAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_xaxis_pressed_value",
+                        "minNodeName": "xr_standard_touchpad_xaxis_pressed_min",
+                        "maxNodeName": "xr_standard_touchpad_xaxis_pressed_max"
+                    },
+                    "xr_standard_touchpad_yaxis_pressed": {
+                        "componentProperty": "yAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_yaxis_pressed_value",
+                        "minNodeName": "xr_standard_touchpad_yaxis_pressed_min",
+                        "maxNodeName": "xr_standard_touchpad_yaxis_pressed_max"
+                    },
+                    "xr_standard_touchpad_xaxis_touched": {
+                        "componentProperty": "xAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_xaxis_touched_value",
+                        "minNodeName": "xr_standard_touchpad_xaxis_touched_min",
+                        "maxNodeName": "xr_standard_touchpad_xaxis_touched_max"
+                    },
+                    "xr_standard_touchpad_yaxis_touched": {
+                        "componentProperty": "yAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_touchpad_yaxis_touched_value",
+                        "minNodeName": "xr_standard_touchpad_yaxis_touched_min",
+                        "maxNodeName": "xr_standard_touchpad_yaxis_touched_max"
+                    },
+                    "xr_standard_touchpad_axes_touched": {
+                        "componentProperty": "state",
+                        "states": [
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "visibility",
+                        "valueNodeName": "xr_standard_touchpad_axes_touched_value"
+                    }
+                },
+                "touchPointNodeName": "xr_standard_touchpad_axes_touched_value"
+            },
+            "xr-standard-thumbstick": {
+                "type": "thumbstick",
+                "gamepadIndices": {
+                    "button": 3,
+                    "xAxis": 2,
+                    "yAxis": 3
+                },
+                "rootNodeName": "xr_standard_thumbstick",
+                "visualResponses": {
+                    "xr_standard_thumbstick_pressed": {
+                        "componentProperty": "button",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_thumbstick_pressed_value",
+                        "minNodeName": "xr_standard_thumbstick_pressed_min",
+                        "maxNodeName": "xr_standard_thumbstick_pressed_max"
+                    },
+                    "xr_standard_thumbstick_xaxis_pressed": {
+                        "componentProperty": "xAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_thumbstick_xaxis_pressed_value",
+                        "minNodeName": "xr_standard_thumbstick_xaxis_pressed_min",
+                        "maxNodeName": "xr_standard_thumbstick_xaxis_pressed_max"
+                    },
+                    "xr_standard_thumbstick_yaxis_pressed": {
+                        "componentProperty": "yAxis",
+                        "states": [
+                            "default",
+                            "touched",
+                            "pressed"
+                        ],
+                        "valueNodeProperty": "transform",
+                        "valueNodeName": "xr_standard_thumbstick_yaxis_pressed_value",
+                        "minNodeName": "xr_standard_thumbstick_yaxis_pressed_min",
+                        "maxNodeName": "xr_standard_thumbstick_yaxis_pressed_max"
+                    }
+                }
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "microsoft-mixed-reality-right",
+        "assetPath": "right.glb"
+    }
+};

+ 124 - 28
src/Cameras/XR/motionController/webXRMotionControllerManager.ts

@@ -1,8 +1,10 @@
 import {
-    WebXRAbstractMotionController,
+    WebXRAbstractMotionController, IMotionControllerProfile,
 } from './webXRAbstractController';
 import { WebXRGenericTriggerMotionController } from './webXRGenericMotionController';
 import { Scene } from '../../../scene';
+import { Tools } from '../../../Misc/tools';
+import { WebXRProfiledMotionController } from './webXRProfiledMotionController';
 
 /**
  * A construction function type to create a new controller based on an xrInput object
@@ -18,6 +20,18 @@ export type MotionControllerConstructor = (xrInput: XRInputSource, scene: Scene)
  * 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 {
+    /**
+     * The base URL of the online controller repository. Can be changed at any time.
+     */
+    public static BaseRepositoryUrl = "https://immersive-web.github.io/webxr-input-profiles/packages/viewer/dist";
+    /**
+     * Use the online repository, or use only locally-defined controllers
+     */
+    public static UseOnlineRepository: boolean = true;
+    /**
+     * Which repository gets priority - local or online
+     */
+    public static PrioritizeOnlineRepository: boolean = true;
     private static _AvailableControllers: { [type: string]: MotionControllerConstructor } = {};
     private static _Fallbacks: { [profileId: string]: string[] } = {};
 
@@ -45,49 +59,128 @@ export class WebXRMotionControllerManager {
      * @param xrInput the xrInput to which a new controller is initialized
      * @param scene the scene to which the model will be added
      * @param forceProfile force a certain profile for this controller
-     * @return the motion controller class for this profile id or the generic standard class if none was found
+     * @return A promise that fulfils with the motion controller class for this profile id or the generic standard class if none was found
      */
-    public static GetMotionControllerWithXRInput(xrInput: XRInputSource, scene: Scene, forceProfile?: string): WebXRAbstractMotionController {
-        //if a type was forced, try creating a controller using it. Continue if not found.
+    public static GetMotionControllerWithXRInput(xrInput: XRInputSource, scene: Scene, forceProfile?: string): Promise<WebXRAbstractMotionController> {
+        const profileArray: string[] = [];
         if (forceProfile) {
-            const constructionFunction = this._AvailableControllers[forceProfile];
-            if (constructionFunction) {
-                return constructionFunction(xrInput, scene);
-            }
+            profileArray.push(forceProfile);
         }
+        profileArray.push(...(xrInput.profiles || []));
 
-        for (let i = 0; i < xrInput.profiles.length; ++i) {
-            const constructionFunction = this._AvailableControllers[xrInput.profiles[i]];
-            if (constructionFunction) {
-                return constructionFunction(xrInput, scene);
-            }
+        // emulator support
+        if (profileArray.length && !profileArray[0]) {
+            // remove the first "undefined" that the emulator is adding
+            profileArray.pop();
         }
-        // try using the gamepad id
+
+        // legacy support - 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);
+                    // oculus in gamepad id
+                    profileArray.push("oculus-touch-v2");
+                    break;
             }
         }
+
+        // make sure microsoft/windows mixed reality works correctly
+        const windowsMRIdx = profileArray.indexOf("windows-mixed-reality");
+        if (windowsMRIdx !== -1) {
+            profileArray.splice(windowsMRIdx, 0, "microsoft-mixed-reality");
+        }
+
+        if (!profileArray.length) {
+            profileArray.push("generic-trigger");
+        }
+
+        if (this.UseOnlineRepository) {
+            const firstFunction = this.PrioritizeOnlineRepository ? this._LoadProfileFromRepository : this._LoadProfilesFromAvailableControllers;
+            const secondFunction = this.PrioritizeOnlineRepository ? this._LoadProfilesFromAvailableControllers : this._LoadProfileFromRepository;
+
+            return firstFunction.call(this, profileArray, xrInput, scene).catch(() => {
+                return secondFunction.call(this, profileArray, xrInput, scene);
+            });
+
+        } else {
+            // use only available functions
+            return this._LoadProfilesFromAvailableControllers(profileArray, xrInput, scene);
+        }
+    }
+
+    private static _LoadProfilesFromAvailableControllers(profileArray: string[], xrInput: XRInputSource, scene: Scene) {
         // check fallbacks
-        for (let i = 0; i < xrInput.profiles.length; ++i) {
-            const fallbacks = this.FindFallbackWithProfileId(xrInput.profiles[i]);
+        for (let i = 0; i < profileArray.length; ++i) {
+            // defensive
+            if (!profileArray[i]) {
+                continue;
+            }
+            const fallbacks = this.FindFallbackWithProfileId(profileArray[i]);
             for (let j = 0; j < fallbacks.length; ++j) {
                 const constructionFunction = this._AvailableControllers[fallbacks[j]];
                 if (constructionFunction) {
-                    return constructionFunction(xrInput, scene);
+                    return Promise.resolve(constructionFunction(xrInput, scene));
                 }
             }
         }
-        // return the most generic thing we have
-        return this._AvailableControllers[WebXRGenericTriggerMotionController.ProfileId](xrInput, scene);
+
+        throw new Error(`no controller requested was found in the available controllers list`);
+    }
+
+    private static _ProfilesList: Promise<{ [profile: string]: string }>;
+
+    // cache for loading
+    private static _ProfileLoadingPromises: { [profileName: string]: Promise<IMotionControllerProfile> } = {};
+
+    private static _LoadProfileFromRepository(profileArray: string[], xrInput: XRInputSource, scene: Scene): Promise<WebXRAbstractMotionController> {
+        return Promise.resolve().then(() => {
+            if (!this._ProfilesList) {
+                return this.UpdateProfilesList();
+            } else {
+                return this._ProfilesList;
+            }
+        }).then((profilesList: { [profile: string]: string }) => {
+            // load the right profile
+            for (let i = 0; i < profileArray.length; ++i) {
+                // defensive
+                if (!profileArray[i]) {
+                    continue;
+                }
+                if (profilesList[profileArray[i]]) {
+                    return profileArray[i];
+                }
+            }
+
+            throw new Error(`neither controller ${profileArray[0]} nor all fallbacks were found in the repository,`);
+        }).then((profileToLoad: string) => {
+            // load the profile
+            if (!this._ProfileLoadingPromises[profileToLoad]) {
+                this._ProfileLoadingPromises[profileToLoad] = Tools.LoadFileAsync(`${this.BaseRepositoryUrl}/profiles/${profileToLoad}/profile.json`, false).then((data) => <IMotionControllerProfile>JSON.parse(data as string));
+            }
+            return this._ProfileLoadingPromises[profileToLoad];
+        }).then((profile: IMotionControllerProfile) => {
+            return new WebXRProfiledMotionController(scene, xrInput, profile, this.BaseRepositoryUrl);
+        });
+
+    }
+
+    /**
+     * Clear the cache used for profile loading and reload when requested again
+     */
+    public static ClearProfilesCache() {
+        delete this._ProfilesList;
+        this._ProfileLoadingPromises = {};
+    }
+
+    /**
+     * Will update the list of profiles available in the repository
+     * @return a promise that resolves to a map of profiles available online
+     */
+    public static UpdateProfilesList() {
+        this._ProfilesList = Tools.LoadFileAsync(this.BaseRepositoryUrl + '/profiles/profilesList.json', false).then((data) => {
+            return JSON.parse(data.toString());
+        });
+        return this._ProfilesList;
     }
 
     /**
@@ -96,7 +189,10 @@ export class WebXRMotionControllerManager {
      * @return an array with corresponding fallback profiles
      */
     public static FindFallbackWithProfileId(profileId: string): string[] {
-        return this._Fallbacks[profileId] || [];
+        const returnArray = this._Fallbacks[profileId] || [];
+
+        returnArray.unshift(profileId);
+        return returnArray;
     }
 
     /**

+ 291 - 122
src/Cameras/XR/motionController/webXROculusTouchMotionController.ts

@@ -10,123 +10,6 @@ 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
@@ -167,14 +50,14 @@ export class WebXROculusTouchMotionController extends WebXRAbstractMotionControl
         const isQuest = this._isQuest();
         const triggerDirection = this.handness === 'right' ? -1 : 1;
 
-        this.layout.gamepad!.buttons.forEach((buttonName) => {
-            const comp = buttonName && this.getComponent(buttonName);
+        this.getComponentIds().forEach((id) => {
+            const comp = id && this.getComponent(id);
             if (comp) {
                 comp.onButtonStateChanged.add((component) => {
 
-                    if (!this.rootMesh) { return; }
+                    if (!this.rootMesh || this.disableAnimation) { return; }
 
-                    switch (buttonName) {
+                    switch (id) {
                         case "xr-standard-trigger": // index trigger
                             if (!isQuest) {
                                 (<AbstractMesh>(this._modelRootNode.getChildren()[3])).rotation.x = -component.value * 0.20;
@@ -274,4 +157,290 @@ WebXRMotionControllerManager.RegisterController("oculus-touch", (xrInput: XRInpu
 
 WebXRMotionControllerManager.RegisterController("oculus-touch-legacy", (xrInput: XRInputSource, scene: Scene) => {
     return new WebXROculusTouchMotionController(scene, <any>(xrInput.gamepad), xrInput.handedness, true);
-});
+});
+
+const OculusTouchLayouts: IMotionControllerLayoutMap = {
+    "left": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-squeeze": {
+                "type": "squeeze",
+                "gamepadIndices": {
+                    "button": 1
+                },
+                "rootNodeName": "xr_standard_squeeze",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-thumbstick": {
+                "type": "thumbstick",
+                "gamepadIndices": {
+                    "button": 3,
+                    "xAxis": 2,
+                    "yAxis": 3
+                },
+                "rootNodeName": "xr_standard_thumbstick",
+                "visualResponses": {
+
+                }
+            },
+            "x-button": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 4
+                },
+                "rootNodeName": "x_button",
+                "visualResponses": {
+
+                }
+            },
+            "y-button": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 5
+                },
+                "rootNodeName": "y_button",
+                "visualResponses": {
+
+                }
+            },
+            "thumbrest": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 6
+                },
+                "rootNodeName": "thumbrest",
+                "visualResponses": {
+
+                }
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "oculus-touch-v2-left",
+        "assetPath": "left.glb"
+    },
+    "right": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 0
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-squeeze": {
+                "type": "squeeze",
+                "gamepadIndices": {
+                    "button": 1
+                },
+                "rootNodeName": "xr_standard_squeeze",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-thumbstick": {
+                "type": "thumbstick",
+                "gamepadIndices": {
+                    "button": 3,
+                    "xAxis": 2,
+                    "yAxis": 3
+                },
+                "rootNodeName": "xr_standard_thumbstick",
+                "visualResponses": {
+
+                }
+            },
+            "a-button": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 4
+                },
+                "rootNodeName": "a_button",
+                "visualResponses": {
+
+                }
+            },
+            "b-button": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 5
+                },
+                "rootNodeName": "b_button",
+                "visualResponses": {
+
+                }
+            },
+            "thumbrest": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 6
+                },
+                "rootNodeName": "thumbrest",
+                "visualResponses": {
+
+                }
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "oculus-touch-v2-right",
+        "assetPath": "right.glb"
+    }
+};
+
+const OculusTouchLegacyLayouts: IMotionControllerLayoutMap = {
+    "left": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 1
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-squeeze": {
+                "type": "squeeze",
+                "gamepadIndices": {
+                    "button": 2
+                },
+                "rootNodeName": "xr_standard_squeeze",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-thumbstick": {
+                "type": "thumbstick",
+                "gamepadIndices": {
+                    "button": 0,
+                    "xAxis": 0,
+                    "yAxis": 1
+                },
+                "rootNodeName": "xr_standard_thumbstick",
+                "visualResponses": {
+
+                }
+            },
+            "x-button": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 3
+                },
+                "rootNodeName": "x_button",
+                "visualResponses": {
+
+                }
+            },
+            "y-button": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 4
+                },
+                "rootNodeName": "y_button",
+                "visualResponses": {
+
+                }
+            },
+            "thumbrest": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 5
+                },
+                "rootNodeName": "thumbrest",
+                "visualResponses": {
+
+                }
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "oculus-touch-v2-left",
+        "assetPath": "left.glb"
+    },
+    "right": {
+        "selectComponentId": "xr-standard-trigger",
+        "components": {
+            "xr-standard-trigger": {
+                "type": "trigger",
+                "gamepadIndices": {
+                    "button": 1
+                },
+                "rootNodeName": "xr_standard_trigger",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-squeeze": {
+                "type": "squeeze",
+                "gamepadIndices": {
+                    "button": 2
+                },
+                "rootNodeName": "xr_standard_squeeze",
+                "visualResponses": {
+
+                }
+            },
+            "xr-standard-thumbstick": {
+                "type": "thumbstick",
+                "gamepadIndices": {
+                    "button": 0,
+                    "xAxis": 0,
+                    "yAxis": 1
+                },
+                "rootNodeName": "xr_standard_thumbstick",
+                "visualResponses": {
+
+                }
+            },
+            "a-button": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 3
+                },
+                "rootNodeName": "a_button",
+                "visualResponses": {
+
+                }
+            },
+            "b-button": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 4
+                },
+                "rootNodeName": "b_button",
+                "visualResponses": {
+
+                }
+            },
+            "thumbrest": {
+                "type": "button",
+                "gamepadIndices": {
+                    "button": 5
+                },
+                "rootNodeName": "thumbrest",
+                "visualResponses": {
+
+                }
+            }
+        },
+        "gamepadMapping": "xr-standard",
+        "rootNodeName": "oculus-touch-v2-right",
+        "assetPath": "right.glb"
+    }
+};

+ 143 - 0
src/Cameras/XR/motionController/webXRProfiledMotionController.ts

@@ -0,0 +1,143 @@
+import { AbstractMesh } from '../../../Meshes/abstractMesh';
+import { WebXRAbstractMotionController, IMotionControllerProfile, IMotionControllerMeshMap } from './webXRAbstractController';
+import { Scene } from '../../../scene';
+import { SceneLoader } from '../../../Loading/sceneLoader';
+import { Mesh } from '../../../Meshes/mesh';
+import { Axis, Space } from '../../../Maths/math.axis';
+import { Color3 } from '../../../Maths/math.color';
+import { WebXRControllerComponent } from './webXRControllerComponent';
+import { SphereBuilder } from '../../../Meshes/Builders/sphereBuilder';
+import { StandardMaterial } from '../../../Materials/standardMaterial';
+
+/**
+ * A profiled motion controller has its profile loaded from an online repository.
+ * The class is responsible of loading the model, mapping the keys and enabling model-animations
+ */
+export class WebXRProfiledMotionController extends WebXRAbstractMotionController {
+    /**
+     * The profile ID of this controller. Will be populated when the controller initializes.
+     */
+    public profileId: string;
+
+    private _buttonMeshMapping: {
+        [buttonName: string]: {
+            mainMesh: AbstractMesh;
+            states: {
+                [state: string]: IMotionControllerMeshMap
+            }
+        }
+    } = {};
+    constructor(scene: Scene, xrInput: XRInputSource, _profile: IMotionControllerProfile, private _repositoryUrl: string) {
+        super(scene, _profile.layouts[xrInput.handedness || "none"], xrInput.gamepad as any, xrInput.handedness);
+        this.profileId = _profile.profileId;
+    }
+
+    protected _getFilenameAndPath(): { filename: string; path: string; } {
+        return {
+            filename: this.layout.assetPath,
+            path: `${this._repositoryUrl}/profiles/${this.profileId}/`
+        };
+    }
+    private _touchDots: { [visKey: string]: AbstractMesh } = {};
+
+    protected _processLoadedModel(_meshes: AbstractMesh[]): void {
+        this.getComponentIds().forEach((type) => {
+            const componentInLayout = this.layout.components[type];
+            this._buttonMeshMapping[type] = {
+                mainMesh: this._getChildByName(this.rootMesh!, componentInLayout.rootNodeName),
+                states: {}
+            };
+            Object.keys(componentInLayout.visualResponses).forEach((visualResponseKey) => {
+                const visResponse = componentInLayout.visualResponses[visualResponseKey];
+                if (visResponse.valueNodeProperty === "transform") {
+                    this._buttonMeshMapping[type].states[visualResponseKey] = {
+                        valueMesh: this._getChildByName(this.rootMesh!, visResponse.valueNodeName!),
+                        minMesh: this._getChildByName(this.rootMesh!, visResponse.minNodeName!),
+                        maxMesh: this._getChildByName(this.rootMesh!, visResponse.maxNodeName!)
+                    };
+                } else {
+                    // visibility, usually for touchpads
+                    const nameOfMesh = (componentInLayout.type === WebXRControllerComponent.TOUCHPAD && componentInLayout.touchPointNodeName)
+                        ? componentInLayout.touchPointNodeName : visResponse.valueNodeName!;
+                    this._buttonMeshMapping[type].states[visualResponseKey] = {
+                        valueMesh: this._getChildByName(this.rootMesh!, nameOfMesh)
+                    };
+                    if (componentInLayout.type === WebXRControllerComponent.TOUCHPAD && !this._touchDots[visualResponseKey]) {
+                        const dot = SphereBuilder.CreateSphere(visualResponseKey + 'dot', {
+                            diameter: 0.0015,
+                            segments: 8
+                        }, this.scene);
+                        dot.material = new StandardMaterial(visualResponseKey + 'mat', this.scene);
+                        (<StandardMaterial>dot.material).diffuseColor = Color3.Red();
+                        dot.parent = this._buttonMeshMapping[type].states[visualResponseKey].valueMesh;
+                        dot.isVisible = false;
+                        this._touchDots[visualResponseKey] = dot;
+                    }
+                }
+            });
+        });
+    }
+
+    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.rotate(Axis.Y, Math.PI, Space.WORLD);
+    }
+    protected _updateModel(_xrFrame: XRFrame): void {
+        if (this.disableAnimation) {
+            return;
+        }
+        this.getComponentIds().forEach((id) => {
+            const component = this.getComponent(id);
+            if (!component.hasChanges) { return; }
+            const meshes = this._buttonMeshMapping[id];
+            const componentInLayout = this.layout.components[id];
+            Object.keys(componentInLayout.visualResponses).forEach((visualResponseKey) => {
+                const visResponse = componentInLayout.visualResponses[visualResponseKey];
+                let value = component.value;
+                if (visResponse.componentProperty === "xAxis") {
+                    value = component.axes.x;
+                } else if (visResponse.componentProperty === "yAxis") {
+                    value = component.axes.y;
+                }
+                if (visResponse.valueNodeProperty === "transform") {
+                    this._lerpTransform(meshes.states[visualResponseKey], value, visResponse.componentProperty !== "button");
+                } else {
+                    // visibility
+                    meshes.states[visualResponseKey].valueMesh.isVisible = component.touched || component.pressed;
+                    if (this._touchDots[visualResponseKey]) {
+                        this._touchDots[visualResponseKey].isVisible = component.touched || component.pressed;
+                    }
+                }
+            });
+        });
+    }
+    protected _getModelLoadingConstraints(): boolean {
+        return SceneLoader.IsPluginForExtensionAvailable(".glb");
+    }
+
+    public dispose() {
+        super.dispose();
+        Object.keys(this._touchDots).forEach((visResKey) => {
+            this._touchDots[visResKey].dispose();
+        });
+    }
+
+}

+ 29 - 4
src/Cameras/XR/webXRController.ts

@@ -17,6 +17,17 @@ export interface IWebXRControllerOptions {
      * This can be used when creating your own profile or when testing different controllers
      */
     forceControllerProfile?: string;
+
+    /**
+     * Do not load the controller mesh, in case a different mesh needs to be loaded.
+     */
+    doNotLoadControllerMesh?: boolean;
+
+    /**
+     * Should the controller mesh be animated when a user interacts with it
+     * The pressed buttons / thumbstick and touchpad animations will be disabled
+     */
+    disableMotionControllerAnimation?: boolean;
 }
 
 /**
@@ -39,6 +50,11 @@ export class WebXRController {
     public motionController?: WebXRAbstractMotionController;
 
     /**
+     * Observers registered here will trigger when a motion controller profile was assigned to this xr controller
+     */
+    public onMotionControllerProfileLoaded = new Observable<WebXRAbstractMotionController>();
+
+    /**
      * Event that fires when the controller is removed/disposed
      */
     public onDisposeObservable = new Observable<{}>();
@@ -71,10 +87,18 @@ export class WebXRController {
 
         // for now only load motion controllers if gamepad available
         if (this.inputSource.gamepad) {
-            this.motionController = WebXRMotionControllerManager.GetMotionControllerWithXRInput(inputSource, _scene, this._options.forceControllerProfile);
-            // if the model is loaded, do your thing
-            this.motionController.onModelLoadedObservable.addOnce(() => {
-                this.motionController!.rootMesh!.parent = this.pointer;
+            WebXRMotionControllerManager.GetMotionControllerWithXRInput(inputSource, _scene, this._options.forceControllerProfile).then((motionController) => {
+                this.motionController = motionController;
+                this.onMotionControllerProfileLoaded.notifyObservers(motionController);
+                // should the model be loaded?
+                if (!this._options.doNotLoadControllerMesh) {
+                    this.motionController.loadModel().then((success) => {
+                        if (success) {
+                            this.motionController!.rootMesh!.parent = this.grip || this.pointer;
+                            this.motionController!.disableAnimation = !!this._options.disableMotionControllerAnimation;
+                        }
+                    });
+                }
             });
         }
     }
@@ -148,6 +172,7 @@ export class WebXRController {
         if (this.motionController) {
             this.motionController.dispose();
         }
+        this.onMotionControllerProfileLoaded.clear();
         this.pointer.dispose();
         this.onDisposeObservable.notifyObservers({});
     }

+ 35 - 4
src/Cameras/XR/webXRInput.ts

@@ -4,6 +4,7 @@ import { IDisposable } from "../../scene";
 import { WebXRController } from './webXRController';
 import { WebXRSessionManager } from './webXRSessionManager';
 import { WebXRCamera } from './webXRCamera';
+import { WebXRMotionControllerManager } from './motionController/webXRMotionControllerManager';
 
 /**
  * The schema for initialization options of the XR Input class
@@ -20,6 +21,23 @@ export interface IWebXRInputOptions {
      * Profiles are defined here - https://github.com/immersive-web/webxr-input-profiles/
      */
     forceInputProfile?: string;
+
+    /**
+     * Do not send a request to the controlle repository to load the profile.
+     *
+     * Instead, use the controllers available in babylon itself.
+     */
+    disableOnlineControllerRepository?: boolean;
+
+    /**
+     * A custom URL for the controllers repository
+     */
+    customControllersRepositoryURL?: string;
+
+    /**
+     * Should the controller model's components not move according to the user input
+     */
+    disableControllerAnimation?: boolean;
 }
 /**
  * XR input used to track XR inputs such as controllers/rays
@@ -73,6 +91,18 @@ export class WebXRInput implements IDisposable {
                 controller.updateFromXRFrame(frame, this.xrSessionManager.referenceSpace);
             });
         });
+
+        if (this.options.customControllersRepositoryURL) {
+            WebXRMotionControllerManager.BaseRepositoryUrl = this.options.customControllersRepositoryURL;
+        }
+
+        if (!this.options.disableOnlineControllerRepository) {
+            WebXRMotionControllerManager.UseOnlineRepository = true;
+            // pre-load the profiles list to load the controllers quicker afterwards
+            WebXRMotionControllerManager.UpdateProfilesList();
+        } else {
+            WebXRMotionControllerManager.UseOnlineRepository = false;
+        }
     }
 
     private _onInputSourcesChange = (event: XRInputSourceChangeEvent) => {
@@ -84,11 +114,12 @@ export class WebXRInput implements IDisposable {
         let sources = this.controllers.map((c) => { return c.inputSource; });
         for (let input of addInputs) {
             if (sources.indexOf(input) === -1) {
-                let controller = new WebXRController(this.xrSessionManager.scene, input, { forceControllerProfile: this.options.forceInputProfile });
+                let controller = new WebXRController(this.xrSessionManager.scene, input, {
+                    forceControllerProfile: this.options.forceInputProfile,
+                    doNotLoadControllerMesh: this.options.doNotLoadControllerMeshes,
+                    disableMotionControllerAnimation: this.options.disableControllerAnimation
+                });
                 this.controllers.push(controller);
-                if (!this.options.doNotLoadControllerMeshes && controller.motionController) {
-                    controller.motionController.loadModel();
-                }
                 this.onControllerAddedObservable.notifyObservers(controller);
             }
         }

+ 2 - 2
src/Meshes/Compression/dracoCompression.ts

@@ -280,7 +280,7 @@ export class DracoCompression implements IDisposable {
     constructor(numWorkers = DracoCompression.DefaultNumWorkers) {
         const decoder = DracoCompression.Configuration.decoder;
 
-        const decoderInfo: { url: string | undefined, wasmBinaryPromise: Promise<ArrayBuffer | undefined> } =
+        const decoderInfo: { url: string | undefined, wasmBinaryPromise: Promise<ArrayBuffer | string | undefined> } =
             (decoder.wasmUrl && decoder.wasmBinaryUrl && typeof WebAssembly === "object") ? {
                 url: decoder.wasmUrl,
                 wasmBinaryPromise: Tools.LoadFileAsync(getAbsoluteUrl(decoder.wasmBinaryUrl))
@@ -336,7 +336,7 @@ export class DracoCompression implements IDisposable {
                 }
 
                 return Tools.LoadScriptAsync(decoderInfo.url).then(() => {
-                    return createDecoderAsync(decoderWasmBinary);
+                    return createDecoderAsync(decoderWasmBinary as ArrayBuffer);
                 });
             });
         }

+ 6 - 5
src/Misc/tools.ts

@@ -350,13 +350,14 @@ export class Tools {
     /**
      * Loads a file from a url
      * @param url the file url to load
-     * @returns a promise containing an ArrayBuffer corrisponding to the loaded file
+     * @param useArrayBuffer defines a boolean indicating that date must be returned as ArrayBuffer
+     * @returns a promise containing an ArrayBuffer corresponding to the loaded file
      */
-    public static LoadFileAsync(url: string): Promise<ArrayBuffer> {
+    public static LoadFileAsync(url: string, useArrayBuffer: boolean = true): Promise<ArrayBuffer | string> {
         return new Promise((resolve, reject) => {
             FileTools.LoadFile(url, (data) => {
-                resolve(data as ArrayBuffer);
-            }, undefined, undefined, true, (request, exception) => {
+                resolve(data);
+            }, undefined, undefined, useArrayBuffer, (request, exception) => {
                 reject(exception);
             });
         });
@@ -1103,7 +1104,7 @@ export class Tools {
      * Utility function to detect if the current user agent is Safari
      * @returns whether or not the current user agent is safari
      */
-    public static IsSafari() : boolean {
+    public static IsSafari(): boolean {
         return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
     }
 }