Selaa lähdekoodia

a new controller API, separated from VR

Raanan Weber 5 vuotta sitten
vanhempi
commit
7922b141ba

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

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

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

@@ -0,0 +1,245 @@
+import { IDisposable, Scene } from '../../../scene';
+import { WebXRControllerComponent } from './webXRControllerComponent';
+import { Observable } from '../../../Misc/observable';
+import { Logger } from '../../../Misc/logger';
+import { SceneLoader } from '../../../Loading/sceneLoader';
+import { AbstractMesh } from '../../../Meshes/abstractMesh';
+import { Nullable } from '../../../types';
+import { Quaternion, Vector3 } from '../../../Maths/math.vector';
+
+export type MotionControllerHandness = "none" | "left" | "right" | "left-right" | "left-right-none";
+export type MotionControllerComponentType = "trigger" | "squeeze" | "touchpad" | "thumbstick" | "button";
+
+// Profiels can be found here - https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/registry/profiles
+export interface IMotionControllerLayout {
+    selectComponentId: string;
+    components: {
+        [componentId: string]: {
+            type: MotionControllerComponentType;
+        }
+    };
+    gamepad?: {
+        mapping: "" | "xr-standard";
+        buttons: Array<string | null>; /* correlates to the componentId in components */
+        axes: Array<{
+            componentId: string;
+            axis: "x-axis" | "y-axis";
+        }>;
+    };
+}
+
+export interface IMotionControllerLayoutMap {
+    [handness: string /* handness */]: IMotionControllerLayout;
+}
+export interface IMotionControllerProfile {
+    profileId: string;
+    fallbackProfileIds: string[];
+    layouts: IMotionControllerLayoutMap;
+}
+
+export interface IMotionControllerButtonMeshMap {
+    valueMesh: AbstractMesh;
+    pressedMesh: AbstractMesh;
+    unpressedMesh: AbstractMesh;
+}
+
+export interface IMotionControllerAxisMeshMap {
+    valueMesh: AbstractMesh;
+    minMesh: AbstractMesh;
+    maxMesh: AbstractMesh;
+}
+
+export interface IMinimalMotionControllerObject {
+    id?: string;
+    buttons: Array<{
+        /**
+        * Value of the button/trigger
+        */
+        value: number;
+        /**
+         * If the button/trigger is currently touched
+         */
+        touched: boolean;
+        /**
+         * If the button/trigger is currently pressed
+         */
+        pressed: boolean;
+    }>;
+    axes: number[];
+}
+
+export abstract class WebXRAbstractMotionController implements IDisposable {
+
+    // constants
+    public static ComponentType = {
+        TRIGGER: "trigger",
+        SQUEEZE: "squeeze",
+        TOUCHPAD: "touchpad",
+        THUMBSTICK: "thumbstick",
+        BUTTON: "button"
+    };
+
+    public abstract profileId: string;
+
+    public readonly components: {
+        [id: string]: WebXRControllerComponent
+    } = {};
+
+    public onModelLoadedObservable: Observable<WebXRAbstractMotionController> = new Observable();
+    public rootMesh: Nullable<AbstractMesh>;
+    private _modelReady: boolean = false;
+
+    constructor(protected scene: Scene, protected layout: IMotionControllerLayout,
+        protected gamepadObject: IMinimalMotionControllerObject,
+        public handness: MotionControllerHandness,
+        _doNotLoadControllerMesh: boolean = false) {
+        // initialize the components
+        if (layout.gamepad) {
+            this.getComponentTypes().forEach(this.initComponent);
+        }
+        // Model is loaded in WebXRInput
+    }
+
+    private initComponent = (id: string) => {
+        if (!this.layout.gamepad) { return; }
+        const type = this.layout.components[id].type;
+        const buttonIndex = this.layout.gamepad.buttons.indexOf(id);
+        // search for axes
+        let axes: number[] = [];
+        this.layout.gamepad.axes.forEach((axis, index) => {
+            if (axis && axis.componentId === id) {
+                if (axis.axis === "x-axis") {
+                    axes[0] = index;
+                } else {
+                    axes[1] = index;
+                }
+            }
+        });
+        this.components[id] = new WebXRControllerComponent(id, type, buttonIndex, axes);
+    }
+
+    public update(xrFrame: XRFrame) {
+        this.getComponentTypes().forEach((id) => this.getComponent(id).update(this.gamepadObject));
+        this.updateModel(xrFrame);
+    }
+
+    public getComponentTypes() {
+        return Object.keys(this.components);
+    }
+
+    public getMainComponent() {
+        return this.getComponent(this.layout.selectComponentId);
+    }
+
+    public getComponent(id: string) {
+        return this.components[id];
+    }
+
+    public async loadModel(): Promise<boolean> {
+        let useGeneric = !this._getModelLoadingConstraints();
+        let loadingParams = this._getGenericFilenameAndPath();
+        // Checking if GLB loader is present
+        if (useGeneric) {
+            Logger.Warn("You need to reference GLTF loader to load Windows Motion Controllers model. Falling back to generic models");
+        } else {
+            loadingParams = this._getFilenameAndPath();
+        }
+        return new Promise((resolve, reject) => {
+            SceneLoader.ImportMesh("", loadingParams.path, loadingParams.filename, this.scene, (meshes: AbstractMesh[]) => {
+                if (useGeneric) {
+                    this._getGenericParentMesh(meshes);
+                } else {
+                    this._setRootMesh(meshes);
+                }
+                this._processLoadedModel(meshes);
+                this._modelReady = true;
+                this.onModelLoadedObservable.notifyObservers(this);
+                resolve(true);
+            }, null, (_scene: Scene, message: string) => {
+                Logger.Log(message);
+                Logger.Warn(`Failed to retrieve controller model of type ${this.profileId} from the remote server: ${loadingParams.path}${loadingParams.filename}`);
+                reject(message);
+            });
+        });
+    }
+
+    public updateModel(xrFrame: XRFrame) {
+        if (!this._modelReady) {
+            return;
+        }
+        this._updateModel(xrFrame);
+    }
+
+    /**
+     * Moves the axis on the controller mesh based on its current state
+     * @param axis the index of the axis
+     * @param axisValue the value of the axis which determines the meshes new position
+     * @hidden
+     */
+    protected _lerpAxisTransform(axisMap: IMotionControllerAxisMeshMap, axisValue: number) {
+
+        if (!axisMap.minMesh.rotationQuaternion || !axisMap.maxMesh.rotationQuaternion || !axisMap.valueMesh.rotationQuaternion) {
+            return;
+        }
+
+        // Convert from gamepad value range (-1 to +1) to lerp range (0 to 1)
+        let lerpValue = axisValue * 0.5 + 0.5;
+        Quaternion.SlerpToRef(
+            axisMap.minMesh.rotationQuaternion,
+            axisMap.maxMesh.rotationQuaternion,
+            lerpValue,
+            axisMap.valueMesh.rotationQuaternion);
+        Vector3.LerpToRef(
+            axisMap.minMesh.position,
+            axisMap.maxMesh.position,
+            lerpValue,
+            axisMap.valueMesh.position);
+    }
+
+    /**
+     * Moves the buttons on the controller mesh based on their current state
+     * @param buttonName the name of the button to move
+     * @param buttonValue the value of the button which determines the buttons new position
+     */
+    protected _lerpButtonTransform(buttonMap: IMotionControllerButtonMeshMap, buttonValue: number) {
+
+        if (!buttonMap
+            || !buttonMap.unpressedMesh.rotationQuaternion
+            || !buttonMap.pressedMesh.rotationQuaternion
+            || !buttonMap.valueMesh.rotationQuaternion) {
+            return;
+        }
+
+        Quaternion.SlerpToRef(
+            buttonMap.unpressedMesh.rotationQuaternion,
+            buttonMap.pressedMesh.rotationQuaternion,
+            buttonValue,
+            buttonMap.valueMesh.rotationQuaternion);
+        Vector3.LerpToRef(
+            buttonMap.unpressedMesh.position,
+            buttonMap.pressedMesh.position,
+            buttonValue,
+            buttonMap.valueMesh.position);
+    }
+
+    private _getGenericFilenameAndPath(): { filename: string, path: string } {
+        return {
+            filename: "generic.babylon",
+            path: "https://controllers.babylonjs.com/generic/"
+        };
+    }
+
+    private _getGenericParentMesh(meshes: AbstractMesh[]): void {
+        // TODO set the parent mesh of the generic controller
+    }
+
+    protected abstract _getFilenameAndPath(): { filename: string, path: string };
+    protected abstract _processLoadedModel(meshes: AbstractMesh[]): void;
+    protected abstract _setRootMesh(meshes: AbstractMesh[]): void;
+    protected abstract _updateModel(xrFrame: XRFrame): void;
+    protected abstract _getModelLoadingConstraints(): boolean;
+
+    public dispose(): void {
+        this.getComponentTypes().forEach((id) => this.getComponent(id).dispose());
+    }
+}

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

@@ -0,0 +1,92 @@
+import { IMinimalMotionControllerObject, MotionControllerComponentType } from "./webXRAbstractController";
+import { Observable } from '../../../Misc/observable';
+import { IDisposable } from '../../../scene';
+
+export class WebXRControllerComponent implements IDisposable {
+
+    public onButtonStateChanged: Observable<WebXRControllerComponent> = new Observable();
+    public onAxisValueChanged: Observable<{ x: number, y: number }> = new Observable();
+
+    private _currentValue: number = 0;
+    private _touched: boolean = false;
+    private _pressed: boolean = false;
+    private _axes: {
+        x: number;
+        y: number;
+    } = {
+            x: 0,
+            y: 0
+        };
+
+    constructor(public id: string,
+        public type: MotionControllerComponentType,
+        private _buttonIndex: number = -1,
+        private _axesIndices: number[] = []) {
+
+    }
+
+    public get value() {
+        return this._currentValue;
+    }
+
+    public get pressed() {
+        return this._pressed;
+    }
+
+    public get touched() {
+        return this._touched;
+    }
+
+    public isButton() {
+        return this._buttonIndex !== -1;
+    }
+
+    public isAxes() {
+        return this._axesIndices.length !== 0;
+    }
+
+    public update(nativeController: IMinimalMotionControllerObject) {
+        let buttonUpdated = false;
+        let axesUpdate = false;
+
+        if (this.isButton()) {
+            const button = nativeController.buttons[this._buttonIndex];
+            if (this._currentValue !== button.value) {
+                buttonUpdated = true;
+                this._currentValue = button.value;
+            }
+            if (this._touched !== button.touched) {
+                buttonUpdated = true;
+                this._touched = button.touched;
+            }
+            if (this._pressed !== button.pressed) {
+                buttonUpdated = true;
+                this._pressed = button.pressed;
+            }
+        }
+
+        if (this.isAxes()) {
+            if (this._axes.x !== nativeController.axes[this._axesIndices[0]]) {
+                this._axes.x = nativeController.axes[this._axesIndices[0]];
+                axesUpdate = true;
+            }
+
+            if (this._axes.y !== nativeController.axes[this._axesIndices[1]]) {
+                this._axes.y = nativeController.axes[this._axesIndices[1]];
+                axesUpdate = true;
+            }
+        }
+
+        if (buttonUpdated) {
+            this.onButtonStateChanged.notifyObservers(this);
+        }
+        if (axesUpdate) {
+            this.onAxisValueChanged.notifyObservers(this._axes);
+        }
+    }
+
+    public dispose(): void {
+        this.onAxisValueChanged.clear();
+        this.onButtonStateChanged.clear();
+    }
+}

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

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

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

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

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

@@ -0,0 +1,84 @@
+import {
+    WebXRAbstractMotionController,
+} from './webXRAbstractController';
+import { WebXRGenericTriggerMotionController } from './webXRGenericMotionController';
+import { Scene } from '../../../scene';
+
+export type MotionControllerConstructor = (xrInput: XRInputSource, scene: Scene) => WebXRAbstractMotionController;
+
+/**
+ * The MotionController Manager manages all registered motion controllers and loads the right one when needed.
+ *
+ * When this repository is complete: https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/assets
+ * it should be replaced with auto-loaded controllers.
+ *
+ * When using a model try to stay as generic as possible. Eventually there will be no need in any of the controller classes
+ */
+export class WebXRMotionControllerManager {
+    private static _AvailableControllers: { [type: string]: MotionControllerConstructor } = {};
+    private static _Fallbacks: { [profileId: string]: string[] } = {};
+    public static RegisterController(type: string, constructFunction: MotionControllerConstructor) {
+        this._AvailableControllers[type] = constructFunction;
+    }
+
+    public static GetMotionControllerWithXRInput(xrInput: XRInputSource, scene: Scene) {
+        for (let i = 0; i < xrInput.profiles.length; ++i) {
+            if (this._AvailableControllers[xrInput.profiles[i]]) {
+                return this._AvailableControllers[xrInput.profiles[i]](xrInput, scene);
+            }
+        }
+        // try using the gamepad id
+        if (xrInput.gamepad) {
+            switch (xrInput.gamepad.id) {
+
+            }
+        }
+        // check fallbacks
+        for (let i = 0; i < xrInput.profiles.length; ++i) {
+            const fallbacks = this.FindFallbackWithProfileId(xrInput.profiles[i]);
+            if (fallbacks) {
+                for (let j = 0; j < fallbacks.length; ++j) {
+                    if (this._AvailableControllers[fallbacks[j]]) {
+                        return this._AvailableControllers[fallbacks[j]](xrInput, scene);
+                    }
+                }
+            }
+        }
+        // return the most generic thing we have
+        return this._AvailableControllers[WebXRGenericTriggerMotionController.ProfileId](xrInput, scene);
+    }
+
+    public static FindFallbackWithProfileId(profileId: string): string[] {
+        return this._Fallbacks[profileId];
+    }
+
+    public static RegisterFallbacksForProfileId(profileId: string, fallbacks: string[]) {
+        if (this._Fallbacks[profileId]) {
+            this._Fallbacks[profileId].push(...fallbacks);
+        } else {
+            this._Fallbacks[profileId] = fallbacks;
+        }
+    }
+
+    public static DefaultFallbacks() {
+        this.RegisterFallbacksForProfileId("google-daydream", ["generic-touchpad"]);
+        this.RegisterFallbacksForProfileId("htc-vive-focus", ["generic-trigger-touchpad"]);
+        this.RegisterFallbacksForProfileId("htc-vive", ["generic-trigger-squeeze-touchpad"]);
+        this.RegisterFallbacksForProfileId("magicleap-one", ["generic-trigger-squeeze-touchpad"]);
+        this.RegisterFallbacksForProfileId("microsoft-mixed-reality", ["generic-trigger-squeeze-touchpad-thumbstick"]);
+        this.RegisterFallbacksForProfileId("oculus-go", ["generic-trigger-touchpad"]);
+        this.RegisterFallbacksForProfileId("oculus-touch-v2", ["oculus-touch", "generic-trigger-squeeze-thumbstick"]);
+        this.RegisterFallbacksForProfileId("oculus-touch", ["generic-trigger-squeeze-thumbstick"]);
+        this.RegisterFallbacksForProfileId("samsung-gearvr", ["microsoft-mixed-reality", "generic-trigger-squeeze-touchpad-thumbstick"]);
+        this.RegisterFallbacksForProfileId("samsung-odyssey", ["generic-touchpad"]);
+        this.RegisterFallbacksForProfileId("valve-index", ["generic-trigger-squeeze-touchpad-thumbstick"]);
+    }
+}
+
+// register the generic profile(s) here so we will at least have them
+WebXRMotionControllerManager.RegisterController(WebXRGenericTriggerMotionController.ProfileId, (xrInput: XRInputSource, scene: Scene) => {
+    return new WebXRGenericTriggerMotionController(scene, <any>(xrInput.gamepad), xrInput.handedness);
+});
+
+// register fallbacks
+WebXRMotionControllerManager.DefaultFallbacks();