webXRMicrosoftMixedRealityController.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257
  1. import {
  2. WebXRAbstractMotionController,
  3. IMinimalMotionControllerObject,
  4. MotionControllerHandness,
  5. IMotionControllerLayoutMap
  6. } from "./webXRAbstractController";
  7. import { WebXRMotionControllerManager } from './webXRMotionControllerManager';
  8. import { AbstractMesh } from '../../../Meshes/abstractMesh';
  9. import { Scene } from '../../../scene';
  10. import { Logger } from '../../../Misc/logger';
  11. import { Mesh } from '../../../Meshes/mesh';
  12. import { Quaternion } from '../../../Maths/math.vector';
  13. import { SceneLoader } from '../../../Loading/sceneLoader';
  14. // https://github.com/immersive-web/webxr-input-profiles/blob/master/packages/registry/profiles/microsoft/microsoft-mixed-reality.json
  15. const MixedRealityProfile: IMotionControllerLayoutMap = {
  16. "left-right": {
  17. "selectComponentId": "xr-standard-trigger",
  18. "components": {
  19. "xr-standard-trigger": { "type": "trigger" },
  20. "xr-standard-squeeze": { "type": "squeeze" },
  21. "xr-standard-touchpad": { "type": "touchpad" },
  22. "xr-standard-thumbstick": { "type": "thumbstick" }
  23. },
  24. "gamepad": {
  25. "mapping": "xr-standard",
  26. "buttons": [
  27. "xr-standard-trigger",
  28. "xr-standard-squeeze",
  29. "xr-standard-touchpad",
  30. "xr-standard-thumbstick"
  31. ],
  32. "axes": [
  33. { "componentId": "xr-standard-touchpad", "axis": "x-axis" },
  34. { "componentId": "xr-standard-touchpad", "axis": "y-axis" },
  35. { "componentId": "xr-standard-thumbstick", "axis": "x-axis" },
  36. { "componentId": "xr-standard-thumbstick", "axis": "y-axis" }
  37. ]
  38. }
  39. }
  40. };
  41. /**
  42. * The motion controller class for all microsoft mixed reality controllers
  43. */
  44. export class WebXRMicrosoftMixedRealityController extends WebXRAbstractMotionController {
  45. /**
  46. * The base url used to load the left and right controller models
  47. */
  48. public static MODEL_BASE_URL: string = 'https://controllers.babylonjs.com/microsoft/';
  49. /**
  50. * The name of the left controller model file
  51. */
  52. public static MODEL_LEFT_FILENAME: string = 'left.glb';
  53. /**
  54. * The name of the right controller model file
  55. */
  56. public static MODEL_RIGHT_FILENAME: string = 'right.glb';
  57. public profileId = "microsoft-mixed-reality";
  58. // use this in the future - https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/assets/profiles/microsoft
  59. protected readonly _mapping = {
  60. defaultButton: {
  61. "valueNodeName": "VALUE",
  62. "unpressedNodeName": "UNPRESSED",
  63. "pressedNodeName": "PRESSED"
  64. },
  65. defaultAxis: {
  66. "valueNodeName": "VALUE",
  67. "minNodeName": "MIN",
  68. "maxNodeName": "MAX"
  69. },
  70. buttons: {
  71. "xr-standard-trigger": {
  72. "rootNodeName": "SELECT",
  73. "componentProperty": "button",
  74. "states": ["default", "touched", "pressed"]
  75. },
  76. "xr-standard-squeeze": {
  77. "rootNodeName": "GRASP",
  78. "componentProperty": "state",
  79. "states": ["pressed"]
  80. },
  81. "xr-standard-touchpad": {
  82. "rootNodeName": "TOUCHPAD_PRESS",
  83. "labelAnchorNodeName": "squeeze-label",
  84. "touchPointNodeName": "TOUCH" // TODO - use this for visual feedback
  85. },
  86. "xr-standard-thumbstick": {
  87. "rootNodeName": "THUMBSTICK_PRESS",
  88. "componentProperty": "state",
  89. "states": ["pressed"],
  90. }
  91. },
  92. axes: {
  93. "xr-standard-touchpad": {
  94. "x-axis": {
  95. "rootNodeName": "TOUCHPAD_TOUCH_X"
  96. },
  97. "y-axis": {
  98. "rootNodeName": "TOUCHPAD_TOUCH_Y"
  99. }
  100. },
  101. "xr-standard-thumbstick": {
  102. "x-axis": {
  103. "rootNodeName": "THUMBSTICK_X"
  104. },
  105. "y-axis": {
  106. "rootNodeName": "THUMBSTICK_Y"
  107. }
  108. }
  109. }
  110. };
  111. constructor(scene: Scene, gamepadObject: IMinimalMotionControllerObject, handness: MotionControllerHandness) {
  112. super(scene, MixedRealityProfile["left-right"], gamepadObject, handness);
  113. }
  114. protected _processLoadedModel(_meshes: AbstractMesh[]): void {
  115. if (!this.rootMesh) { return; }
  116. // Button Meshes
  117. for (let i = 0; i < this.layout.gamepad!.buttons.length; i++) {
  118. const buttonName = this.layout.gamepad!.buttons[i];
  119. if (buttonName) {
  120. const buttonMap = (<any>this._mapping.buttons)[buttonName];
  121. const buttonMeshName = buttonMap.rootNodeName;
  122. if (!buttonMeshName) {
  123. Logger.Log('Skipping unknown button at index: ' + i + ' with mapped name: ' + buttonName);
  124. continue;
  125. }
  126. var buttonMesh = this._getChildByName(this.rootMesh, buttonMeshName);
  127. if (!buttonMesh) {
  128. Logger.Warn('Missing button mesh with name: ' + buttonMeshName);
  129. continue;
  130. }
  131. buttonMap.valueMesh = this._getImmediateChildByName(buttonMesh, this._mapping.defaultButton.valueNodeName);
  132. buttonMap.pressedMesh = this._getImmediateChildByName(buttonMesh, this._mapping.defaultButton.pressedNodeName);
  133. buttonMap.unpressedMesh = this._getImmediateChildByName(buttonMesh, this._mapping.defaultButton.unpressedNodeName);
  134. if (buttonMap.valueMesh && buttonMap.pressedMesh && buttonMap.unpressedMesh) {
  135. const comp = this.getComponent(buttonName);
  136. if (comp) {
  137. comp.onButtonStateChanged.add((component) => {
  138. this._lerpButtonTransform(buttonMap, component.value);
  139. }, undefined, true);
  140. }
  141. } else {
  142. // If we didn't find the mesh, it simply means this button won't have transforms applied as mapped button value changes.
  143. Logger.Warn('Missing button submesh under mesh with name: ' + buttonMeshName);
  144. }
  145. }
  146. }
  147. // Axis Meshes
  148. for (let i = 0; i < this.layout.gamepad!.axes.length; ++i) {
  149. const axisData = this.layout.gamepad!.axes[i];
  150. if (!axisData) {
  151. Logger.Log('Skipping unknown axis at index: ' + i);
  152. continue;
  153. }
  154. const axisMap = (<any>this._mapping.axes)[axisData.componentId][axisData.axis];
  155. var axisMesh = this._getChildByName(this.rootMesh, axisMap.rootNodeName);
  156. if (!axisMesh) {
  157. Logger.Warn('Missing axis mesh with name: ' + axisMap.rootNodeName);
  158. continue;
  159. }
  160. axisMap.valueMesh = this._getImmediateChildByName(axisMesh, this._mapping.defaultAxis.valueNodeName);
  161. axisMap.minMesh = this._getImmediateChildByName(axisMesh, this._mapping.defaultAxis.minNodeName);
  162. axisMap.maxMesh = this._getImmediateChildByName(axisMesh, this._mapping.defaultAxis.maxNodeName);
  163. if (axisMap.valueMesh && axisMap.minMesh && axisMap.maxMesh) {
  164. const comp = this.getComponent(axisData.componentId);
  165. if (comp) {
  166. comp.onAxisValueChanged.add((axisValues) => {
  167. const value = axisData.axis === "x-axis" ? axisValues.x : axisValues.y;
  168. this._lerpAxisTransform(axisMap, value);
  169. }, undefined, true);
  170. }
  171. } else {
  172. // If we didn't find the mesh, it simply means this button won't have transforms applied as mapped button value changes.
  173. Logger.Warn('Missing axis submesh under mesh with name: ' + axisMap.rootNodeName);
  174. }
  175. }
  176. }
  177. // Look through all children recursively. This will return null if no mesh exists with the given name.
  178. private _getChildByName(node: AbstractMesh, name: string): AbstractMesh {
  179. return <AbstractMesh>node.getChildren((n) => n.name === name, false)[0];
  180. }
  181. // Look through only immediate children. This will return null if no mesh exists with the given name.
  182. private _getImmediateChildByName(node: AbstractMesh, name: string): AbstractMesh {
  183. return <AbstractMesh>node.getChildren((n) => n.name == name, true)[0];
  184. }
  185. protected _getFilenameAndPath(): { filename: string; path: string; } {
  186. let filename = "";
  187. if (this.handness === 'left') {
  188. filename = WebXRMicrosoftMixedRealityController.MODEL_LEFT_FILENAME;
  189. }
  190. else { // Right is the default if no hand is specified
  191. filename = WebXRMicrosoftMixedRealityController.MODEL_RIGHT_FILENAME;
  192. }
  193. const device = 'default';
  194. let path = WebXRMicrosoftMixedRealityController.MODEL_BASE_URL + device + '/';
  195. return {
  196. filename,
  197. path
  198. };
  199. }
  200. protected _updateModel(): void {
  201. // no-op. model is updated using observables.
  202. }
  203. protected _getModelLoadingConstraints(): boolean {
  204. return SceneLoader.IsPluginForExtensionAvailable(".glb");
  205. }
  206. protected _setRootMesh(meshes: AbstractMesh[]): void {
  207. this.rootMesh = new Mesh(this.profileId + " " + this.handness, this.scene);
  208. this.rootMesh.isPickable = false;
  209. let rootMesh;
  210. // Find the root node in the loaded glTF scene, and attach it as a child of 'parentMesh'
  211. for (let i = 0; i < meshes.length; i++) {
  212. let mesh = meshes[i];
  213. mesh.isPickable = false;
  214. if (!mesh.parent) {
  215. // Handle root node, attach to the new parentMesh
  216. rootMesh = mesh;
  217. }
  218. }
  219. if (rootMesh) {
  220. rootMesh.setParent(this.rootMesh);
  221. }
  222. this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
  223. }
  224. }
  225. // register the profile
  226. WebXRMotionControllerManager.RegisterController("windows-mixed-reality", (xrInput: XRInputSource, scene: Scene) => {
  227. return new WebXRMicrosoftMixedRealityController(scene, <any>(xrInput.gamepad), xrInput.handedness);
  228. });