module BABYLON { export enum PoseEnabledControllerType { VIVE, OCULUS, WINDOWS, GENERIC } export interface MutableGamepadButton { value: number; touched: boolean; pressed: boolean; } export class PoseEnabledControllerHelper { public static InitiateController(vrGamepad: any) { // Oculus Touch if (vrGamepad.id.indexOf('Oculus Touch') !== -1) { return new OculusTouchController(vrGamepad); } // Windows Mixed Reality controllers // else if (vrGamepad.id.indexOf('Spatial Control') === 0) { // //return new WindowsMixedRealityController(vrGamepad); // } // HTC Vive else if (vrGamepad.id.toLowerCase().indexOf('openvr') !== -1) { return new ViveController(vrGamepad); } // Generic else { return new GenericController(vrGamepad); } } } export class PoseEnabledController extends Gamepad implements PoseControlled { devicePosition: Vector3; deviceRotationQuaternion: Quaternion; deviceScaleFactor: number = 1; public position: Vector3; public rotationQuaternion: Quaternion; public controllerType: PoseEnabledControllerType; private _calculatedPosition: Vector3; private _calculatedRotation: Quaternion; public rawPose: DevicePose; //GamepadPose; public _mesh: AbstractMesh; // a node that will be attached to this Gamepad private _poseControlledCamera: TargetCamera; private _leftHandSystemQuaternion: Quaternion = new Quaternion(); constructor(public vrGamepad) { super(vrGamepad.id, vrGamepad.index, vrGamepad); this.type = Gamepad.POSE_ENABLED; this.controllerType = PoseEnabledControllerType.GENERIC; this.position = Vector3.Zero(); this.rotationQuaternion = new Quaternion(); this.devicePosition = Vector3.Zero(); this.deviceRotationQuaternion = new Quaternion(); this._calculatedPosition = Vector3.Zero(); this._calculatedRotation = new Quaternion(); Quaternion.RotationYawPitchRollToRef(Math.PI, 0, 0, this._leftHandSystemQuaternion); } public update() { super.update(); var pose: GamepadPose = this.vrGamepad.pose; this.updateFromDevice(pose); if (this._mesh) { this._mesh.position.copyFrom(this._calculatedPosition); this._mesh.rotationQuaternion.copyFrom(this._calculatedRotation); } } updateFromDevice(poseData: DevicePose) { if (poseData) { this.rawPose = poseData; if (poseData.position) { this.devicePosition.copyFromFloats(poseData.position[0], poseData.position[1], -poseData.position[2]); if (this._mesh && this._mesh.getScene().useRightHandedSystem) { this.devicePosition.z *= -1; } this.devicePosition.scaleToRef(this.deviceScaleFactor, this._calculatedPosition); this._calculatedPosition.addInPlace(this.position); } if (poseData.orientation) { this.deviceRotationQuaternion.copyFromFloats(this.rawPose.orientation[0], this.rawPose.orientation[1], -this.rawPose.orientation[2], -this.rawPose.orientation[3]); if (this._mesh) { if (this._mesh.getScene().useRightHandedSystem) { this.deviceRotationQuaternion.z *= -1; this.deviceRotationQuaternion.w *= -1; } else { this.deviceRotationQuaternion.multiplyToRef(this._leftHandSystemQuaternion, this.deviceRotationQuaternion); } } // if the camera is set, rotate to the camera's rotation this.deviceRotationQuaternion.multiplyToRef(this.rotationQuaternion, this._calculatedRotation); } } } public attachToMesh(mesh: AbstractMesh) { if (this._mesh) { this._mesh.parent = undefined; } this._mesh = mesh; if (this._poseControlledCamera) { this._mesh.parent = this._poseControlledCamera; } if (!this._mesh.rotationQuaternion) { this._mesh.rotationQuaternion = new Quaternion(); } } public attachToPoseControlledCamera(camera: TargetCamera) { this._poseControlledCamera = camera; if (this._mesh) { this._mesh.parent = this._poseControlledCamera; } } public dispose() { if (this._mesh) { this._mesh.dispose(); } this._mesh = undefined; } public get mesh(): AbstractMesh { return this._mesh; } public getForwardRay(length = 100): Ray { if (!this.mesh) { return new Ray(Vector3.Zero(), new BABYLON.Vector3(0, 0, 1), length); } var m = this.mesh.getWorldMatrix(); var origin = m.getTranslation(); var forward = new BABYLON.Vector3(0, 0, -1); var forwardWorld = BABYLON.Vector3.TransformNormal(forward, m); var direction = BABYLON.Vector3.Normalize(forwardWorld); return new Ray(origin, direction, length); } } export interface GamepadButtonChanges { changed: boolean; pressChanged: boolean; touchChanged: boolean; valueChanged: boolean; } export abstract class WebVRController extends PoseEnabledController { //public onTriggerStateChangedObservable = new Observable<{ state: ExtendedGamepadButton, changes: GamepadButtonChanges }>(); public onTriggerStateChangedObservable = new Observable(); public onMainButtonStateChangedObservable = new Observable(); public onSecondaryButtonStateChangedObservable = new Observable(); public onPadStateChangedObservable = new Observable(); public onPadValuesChangedObservable = new Observable(); protected _buttons: Array; private _onButtonStateChange: (controlledIndex: number, buttonIndex: number, state: ExtendedGamepadButton) => void; public onButtonStateChange(callback: (controlledIndex: number, buttonIndex: number, state: ExtendedGamepadButton) => void) { this._onButtonStateChange = callback; } public pad: StickValues = { x: 0, y: 0 }; public hand: string; // 'left' or 'right', see https://w3c.github.io/gamepad/extensions.html#gamepadhand-enum constructor(vrGamepad) { super(vrGamepad); this._buttons = new Array(vrGamepad.buttons.length); this.hand = vrGamepad.hand; } public update() { super.update(); for (var index = 0; index < this._buttons.length; index++) { this._setButtonValue(this.vrGamepad.buttons[index], this._buttons[index], index); }; if (this.leftStick.x !== this.pad.x || this.leftStick.y !== this.pad.y) { this.pad.x = this.leftStick.x; this.pad.y = this.leftStick.y; this.onPadValuesChangedObservable.notifyObservers(this.pad); } } protected abstract handleButtonChange(buttonIdx: number, value: ExtendedGamepadButton, changes: GamepadButtonChanges); public abstract initControllerMesh(scene: Scene, meshLoaded?: (mesh: AbstractMesh) => void); private _setButtonValue(newState: ExtendedGamepadButton, currentState: ExtendedGamepadButton, buttonIndex: number) { if (!newState) { newState = { pressed: false, touched: false, value: 0 }; } if (!currentState) { this._buttons[buttonIndex] = { pressed: newState.pressed, touched: newState.touched, value: newState.value } return; } this._checkChanges(newState, currentState); if (this._changes.changed) { this._onButtonStateChange && this._onButtonStateChange(this.index, buttonIndex, newState); this.handleButtonChange(buttonIndex, newState, this._changes); } this._buttons[buttonIndex].pressed = newState.pressed; this._buttons[buttonIndex].touched = newState.touched; // oculus triggers are never 0, thou not touched. this._buttons[buttonIndex].value = newState.value < 0.00000001 ? 0 : newState.value; } // avoid GC, store state in a tmp object private _changes: GamepadButtonChanges = { pressChanged: false, touchChanged: false, valueChanged: false, changed: false }; private _checkChanges(newState: ExtendedGamepadButton, currentState: ExtendedGamepadButton) { this._changes.pressChanged = newState.pressed !== currentState.pressed; this._changes.touchChanged = newState.touched !== currentState.touched; this._changes.valueChanged = newState.value !== currentState.value; this._changes.changed = this._changes.pressChanged || this._changes.touchChanged || this._changes.valueChanged; return this._changes; } } export class OculusTouchController extends WebVRController { private _defaultModel: BABYLON.AbstractMesh; public onSecondaryTriggerStateChangedObservable = new Observable(); public onThumbRestChangedObservable = new Observable(); constructor(vrGamepad) { super(vrGamepad); this.controllerType = PoseEnabledControllerType.OCULUS; } public initControllerMesh(scene: Scene, meshLoaded?: (mesh: AbstractMesh) => void) { let meshName = this.hand === 'right' ? 'RightTouch.babylon' : 'LeftTouch.babylon'; SceneLoader.ImportMesh("", "http://yoda.blob.core.windows.net/models/", meshName, scene, (newMeshes) => { /* Parent Mesh name: oculus_touch_left - body - trigger - thumbstick - grip - button_y - button_x - button_enter */ this._defaultModel = newMeshes[1]; if (meshLoaded) { meshLoaded(this._defaultModel); } this.attachToMesh(this._defaultModel); }); } // helper getters for left and right hand. public get onAButtonStateChangedObservable() { if (this.hand === 'right') { return this.onMainButtonStateChangedObservable; } else { throw new Error('No A button on left hand'); } } public get onBButtonStateChangedObservable() { if (this.hand === 'right') { return this.onSecondaryButtonStateChangedObservable; } else { throw new Error('No B button on left hand'); } } public get onXButtonStateChangedObservable() { if (this.hand === 'left') { return this.onMainButtonStateChangedObservable; } else { throw new Error('No X button on right hand'); } } public get onYButtonStateChangedObservable() { if (this.hand === 'left') { return this.onSecondaryButtonStateChangedObservable; } else { throw new Error('No Y button on right hand'); } } /* 0) thumb stick (touch, press, value = pressed (0,1)). value is in this.leftStick 1) index trigger (touch (?), press (only when value > 0.1), value 0 to 1) 2) secondary trigger (same) 3) A (right) X (left), touch, pressed = value 4) B / Y 5) thumb rest */ protected handleButtonChange(buttonIdx: number, state: ExtendedGamepadButton, changes: GamepadButtonChanges) { let notifyObject = state; //{ state: state, changes: changes }; let triggerDirection = this.hand === 'right' ? -1 : 1; switch (buttonIdx) { case 0: this.onPadStateChangedObservable.notifyObservers(notifyObject); return; case 1: // index trigger if (this._defaultModel) { ((this._defaultModel.getChildren()[3])).rotation.x = -notifyObject.value * 0.20; ((this._defaultModel.getChildren()[3])).position.y = -notifyObject.value * 0.005; ((this._defaultModel.getChildren()[3])).position.z = -notifyObject.value * 0.005; } this.onTriggerStateChangedObservable.notifyObservers(notifyObject); return; case 2: // secondary trigger if (this._defaultModel) { ((this._defaultModel.getChildren()[4])).position.x = triggerDirection * notifyObject.value * 0.0035; } this.onSecondaryTriggerStateChangedObservable.notifyObservers(notifyObject); return; case 3: if (this._defaultModel) { if (notifyObject.pressed) { ((this._defaultModel.getChildren()[1])).position.y = -0.001; } else { ((this._defaultModel.getChildren()[1])).position.y = 0; } } this.onMainButtonStateChangedObservable.notifyObservers(notifyObject); return; case 4: if (this._defaultModel) { if (notifyObject.pressed) { ((this._defaultModel.getChildren()[2])).position.y = -0.001; } else { ((this._defaultModel.getChildren()[2])).position.y = 0; } } this.onSecondaryButtonStateChangedObservable.notifyObservers(notifyObject); return; case 5: this.onThumbRestChangedObservable.notifyObservers(notifyObject); return; } } } export class ViveController extends WebVRController { private _defaultModel: BABYLON.AbstractMesh; constructor(vrGamepad) { super(vrGamepad); this.controllerType = PoseEnabledControllerType.VIVE; } public initControllerMesh(scene: Scene, meshLoaded?: (mesh: AbstractMesh) => void) { SceneLoader.ImportMesh("", "http://yoda.blob.core.windows.net/models/", "ViveWand.babylon", scene, (newMeshes) => { /* Parent Mesh name: ViveWand - body - r_gripper - l_gripper - menu_button - system_button - trackpad - trigger - LED */ this._defaultModel = newMeshes[1]; if (meshLoaded) { meshLoaded(this._defaultModel); } this.attachToMesh(this._defaultModel); }); } public get onLeftButtonStateChangedObservable() { return this.onMainButtonStateChangedObservable; } public get onRightButtonStateChangedObservable() { return this.onMainButtonStateChangedObservable; } public get onMenuButtonStateChangedObservable() { return this.onSecondaryButtonStateChangedObservable; } /** * Vive mapping: * 0: touchpad * 1: trigger * 2: left AND right buttons * 3: menu button */ protected handleButtonChange(buttonIdx: number, state: ExtendedGamepadButton, changes: GamepadButtonChanges) { let notifyObject = state; //{ state: state, changes: changes }; switch (buttonIdx) { case 0: this.onPadStateChangedObservable.notifyObservers(notifyObject); return; case 1: // index trigger if (this._defaultModel) { ((this._defaultModel.getChildren()[6])).rotation.x = -notifyObject.value * 0.15; } this.onTriggerStateChangedObservable.notifyObservers(notifyObject); return; case 2: // left AND right button this.onMainButtonStateChangedObservable.notifyObservers(notifyObject); return; case 3: if (this._defaultModel) { if (notifyObject.pressed) { ((this._defaultModel.getChildren()[2])).position.y = -0.001; } else { ((this._defaultModel.getChildren()[2])).position.y = 0; } } this.onSecondaryButtonStateChangedObservable.notifyObservers(notifyObject); return; } } } export class GenericController extends WebVRController { private _defaultModel: BABYLON.AbstractMesh; constructor(vrGamepad) { super(vrGamepad); } public initControllerMesh(scene: Scene, meshLoaded?: (mesh: AbstractMesh) => void) { SceneLoader.ImportMesh("", "http://yoda.blob.core.windows.net/models/", "genericvrcontroller.babylon", scene, (newMeshes) => { this._defaultModel = newMeshes[1]; if (meshLoaded) { meshLoaded(this._defaultModel); } this.attachToMesh(this._defaultModel); }); } protected handleButtonChange(buttonIdx: number, state: ExtendedGamepadButton, changes: GamepadButtonChanges) { console.log("Button id: " + buttonIdx + "state: "); console.dir(state); } } } interface ExtendedGamepadButton extends GamepadButton { readonly pressed: boolean; readonly touched: boolean; readonly value: number; }