webXRAbstractMotionController.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516
  1. import { IDisposable, Scene } from '../../scene';
  2. import { WebXRControllerComponent } from './webXRControllerComponent';
  3. import { Observable } from '../../Misc/observable';
  4. import { Logger } from '../../Misc/logger';
  5. import { SceneLoader } from '../../Loading/sceneLoader';
  6. import { AbstractMesh } from '../../Meshes/abstractMesh';
  7. import { Nullable } from '../../types';
  8. import { Quaternion, Vector3 } from '../../Maths/math.vector';
  9. import { Mesh } from '../../Meshes/mesh';
  10. /**
  11. * Handedness type in xrInput profiles. These can be used to define layouts in the Layout Map.
  12. */
  13. export type MotionControllerHandedness = "none" | "left" | "right";
  14. /**
  15. * The type of components available in motion controllers.
  16. * This is not the name of the component.
  17. */
  18. export type MotionControllerComponentType = "trigger" | "squeeze" | "touchpad" | "thumbstick" | "button";
  19. /**
  20. * The state of a controller component
  21. */
  22. export type MotionControllerComponentStateType = "default" | "touched" | "pressed";
  23. /**
  24. * The schema of motion controller layout.
  25. * No object will be initialized using this interface
  26. * This is used just to define the profile.
  27. */
  28. export interface IMotionControllerLayout {
  29. /**
  30. * Path to load the assets. Usually relative to the base path
  31. */
  32. assetPath: string;
  33. /**
  34. * Available components (unsorted)
  35. */
  36. components: {
  37. /**
  38. * A map of component Ids
  39. */
  40. [componentId: string]: {
  41. /**
  42. * The type of input the component outputs
  43. */
  44. type: MotionControllerComponentType;
  45. /**
  46. * The indices of this component in the gamepad object
  47. */
  48. gamepadIndices: {
  49. /**
  50. * Index of button
  51. */
  52. button?: number;
  53. /**
  54. * If available, index of x-axis
  55. */
  56. xAxis?: number;
  57. /**
  58. * If available, index of y-axis
  59. */
  60. yAxis?: number;
  61. };
  62. /**
  63. * The mesh's root node name
  64. */
  65. rootNodeName: string;
  66. /**
  67. * Animation definitions for this model
  68. */
  69. visualResponses: {
  70. [stateKey: string]: {
  71. /**
  72. * What property will be animated
  73. */
  74. componentProperty: "xAxis" | "yAxis" | "button" | "state";
  75. /**
  76. * What states influence this visual response
  77. */
  78. states: MotionControllerComponentStateType[];
  79. /**
  80. * Type of animation - movement or visibility
  81. */
  82. valueNodeProperty: "transform" | "visibility";
  83. /**
  84. * Base node name to move. Its position will be calculated according to the min and max nodes
  85. */
  86. valueNodeName?: string;
  87. /**
  88. * Minimum movement node
  89. */
  90. minNodeName?: string;
  91. /**
  92. * Max movement node
  93. */
  94. maxNodeName?: string;
  95. }
  96. }
  97. /**
  98. * If touch enabled, what is the name of node to display user feedback
  99. */
  100. touchPointNodeName?: string;
  101. }
  102. };
  103. /**
  104. * Is it xr standard mapping or not
  105. */
  106. gamepadMapping: "" | "xr-standard";
  107. /**
  108. * Base root node of this entire model
  109. */
  110. rootNodeName: string;
  111. /**
  112. * Defines the main button component id
  113. */
  114. selectComponentId: string;
  115. }
  116. /**
  117. * A definition for the layout map in the input profile
  118. */
  119. export interface IMotionControllerLayoutMap {
  120. /**
  121. * Layouts with handedness type as a key
  122. */
  123. [handedness: string /* handedness */]: IMotionControllerLayout;
  124. }
  125. /**
  126. * The XR Input profile schema
  127. * Profiles can be found here:
  128. * https://github.com/immersive-web/webxr-input-profiles/tree/master/packages/registry/profiles
  129. */
  130. export interface IMotionControllerProfile {
  131. /**
  132. * fallback profiles for this profileId
  133. */
  134. fallbackProfileIds: string[];
  135. /**
  136. * The layout map, with handedness as key
  137. */
  138. layouts: IMotionControllerLayoutMap;
  139. /**
  140. * The id of this profile
  141. * correlates to the profile(s) in the xrInput.profiles array
  142. */
  143. profileId: string;
  144. }
  145. /**
  146. * A helper-interface for the 3 meshes needed for controller button animation
  147. * The meshes are provided to the _lerpButtonTransform function to calculate the current position of the value mesh
  148. */
  149. export interface IMotionControllerButtonMeshMap {
  150. /**
  151. * the mesh that defines the pressed value mesh position.
  152. * This is used to find the max-position of this button
  153. */
  154. pressedMesh: AbstractMesh;
  155. /**
  156. * the mesh that defines the unpressed value mesh position.
  157. * This is used to find the min (or initial) position of this button
  158. */
  159. unpressedMesh: AbstractMesh;
  160. /**
  161. * The mesh that will be changed when value changes
  162. */
  163. valueMesh: AbstractMesh;
  164. }
  165. /**
  166. * A helper-interface for the 3 meshes needed for controller axis animation.
  167. * This will be expanded when touchpad animations are fully supported
  168. * The meshes are provided to the _lerpAxisTransform function to calculate the current position of the value mesh
  169. */
  170. export interface IMotionControllerMeshMap {
  171. /**
  172. * the mesh that defines the maximum value mesh position.
  173. */
  174. maxMesh?: AbstractMesh;
  175. /**
  176. * the mesh that defines the minimum value mesh position.
  177. */
  178. minMesh?: AbstractMesh;
  179. /**
  180. * The mesh that will be changed when axis value changes
  181. */
  182. valueMesh: AbstractMesh;
  183. }
  184. /**
  185. * The elements needed for change-detection of the gamepad objects in motion controllers
  186. */
  187. export interface IMinimalMotionControllerObject {
  188. /**
  189. * Available axes of this controller
  190. */
  191. axes: number[];
  192. /**
  193. * An array of available buttons
  194. */
  195. buttons: Array<{
  196. /**
  197. * Value of the button/trigger
  198. */
  199. value: number;
  200. /**
  201. * If the button/trigger is currently touched
  202. */
  203. touched: boolean;
  204. /**
  205. * If the button/trigger is currently pressed
  206. */
  207. pressed: boolean;
  208. }>;
  209. /**
  210. * EXPERIMENTAL haptic support.
  211. */
  212. hapticActuators?: Array<{
  213. pulse: (value: number, duration: number) => Promise<boolean>
  214. }>;
  215. }
  216. /**
  217. * An Abstract Motion controller
  218. * This class receives an xrInput and a profile layout and uses those to initialize the components
  219. * Each component has an observable to check for changes in value and state
  220. */
  221. export abstract class WebXRAbstractMotionController implements IDisposable {
  222. private _initComponent = (id: string) => {
  223. if (!id) { return; }
  224. const componentDef = this.layout.components[id];
  225. const type = componentDef.type;
  226. const buttonIndex = componentDef.gamepadIndices.button;
  227. // search for axes
  228. let axes: number[] = [];
  229. if (componentDef.gamepadIndices.xAxis !== undefined && componentDef.gamepadIndices.yAxis !== undefined) {
  230. axes.push(componentDef.gamepadIndices.xAxis, componentDef.gamepadIndices.yAxis);
  231. }
  232. this.components[id] = new WebXRControllerComponent(id, type, buttonIndex, axes);
  233. }
  234. private _modelReady: boolean = false;
  235. /**
  236. * A map of components (WebXRControllerComponent) in this motion controller
  237. * Components have a ComponentType and can also have both button and axis definitions
  238. */
  239. public readonly components: {
  240. [id: string]: WebXRControllerComponent
  241. } = {};
  242. /**
  243. * Disable the model's animation. Can be set at any time.
  244. */
  245. public disableAnimation: boolean = false;
  246. /**
  247. * Observers registered here will be triggered when the model of this controller is done loading
  248. */
  249. public onModelLoadedObservable: Observable<WebXRAbstractMotionController> = new Observable();
  250. /**
  251. * The profile id of this motion controller
  252. */
  253. public abstract profileId: string;
  254. /**
  255. * The root mesh of the model. It is null if the model was not yet initialized
  256. */
  257. public rootMesh: Nullable<AbstractMesh>;
  258. /**
  259. * constructs a new abstract motion controller
  260. * @param scene the scene to which the model of the controller will be added
  261. * @param layout The profile layout to load
  262. * @param gamepadObject The gamepad object correlating to this controller
  263. * @param handedness handedness (left/right/none) of this controller
  264. * @param _doNotLoadControllerMesh set this flag to ignore the mesh loading
  265. */
  266. constructor(protected scene: Scene, protected layout: IMotionControllerLayout,
  267. /**
  268. * The gamepad object correlating to this controller
  269. */
  270. public gamepadObject: IMinimalMotionControllerObject,
  271. /**
  272. * handedness (left/right/none) of this controller
  273. */
  274. public handedness: MotionControllerHandedness,
  275. _doNotLoadControllerMesh: boolean = false) {
  276. // initialize the components
  277. if (layout.components) {
  278. Object.keys(layout.components).forEach(this._initComponent);
  279. }
  280. // Model is loaded in WebXRInput
  281. }
  282. /**
  283. * Dispose this controller, the model mesh and all its components
  284. */
  285. public dispose(): void {
  286. this.getComponentIds().forEach((id) => this.getComponent(id).dispose());
  287. if (this.rootMesh) {
  288. this.rootMesh.dispose();
  289. }
  290. }
  291. /**
  292. * Returns all components of specific type
  293. * @param type the type to search for
  294. * @return an array of components with this type
  295. */
  296. public getAllComponentsOfType(type: MotionControllerComponentType): WebXRControllerComponent[] {
  297. return this.getComponentIds().map((id) => this.components[id]).filter((component) => component.type === type);
  298. }
  299. /**
  300. * get a component based an its component id as defined in layout.components
  301. * @param id the id of the component
  302. * @returns the component correlates to the id or undefined if not found
  303. */
  304. public getComponent(id: string): WebXRControllerComponent {
  305. return this.components[id];
  306. }
  307. /**
  308. * Get the list of components available in this motion controller
  309. * @returns an array of strings correlating to available components
  310. */
  311. public getComponentIds(): string[] {
  312. return Object.keys(this.components);
  313. }
  314. /**
  315. * Get the first component of specific type
  316. * @param type type of component to find
  317. * @return a controller component or null if not found
  318. */
  319. public getComponentOfType(type: MotionControllerComponentType): Nullable<WebXRControllerComponent> {
  320. return this.getAllComponentsOfType(type)[0] || null;
  321. }
  322. /**
  323. * Get the main (Select) component of this controller as defined in the layout
  324. * @returns the main component of this controller
  325. */
  326. public getMainComponent(): WebXRControllerComponent {
  327. return this.getComponent(this.layout.selectComponentId);
  328. }
  329. /**
  330. * Loads the model correlating to this controller
  331. * When the mesh is loaded, the onModelLoadedObservable will be triggered
  332. * @returns A promise fulfilled with the result of the model loading
  333. */
  334. public async loadModel(): Promise<boolean> {
  335. let useGeneric = !this._getModelLoadingConstraints();
  336. let loadingParams = this._getGenericFilenameAndPath();
  337. // Checking if GLB loader is present
  338. if (useGeneric) {
  339. Logger.Warn("Falling back to generic models");
  340. } else {
  341. loadingParams = this._getFilenameAndPath();
  342. }
  343. return new Promise((resolve, reject) => {
  344. SceneLoader.ImportMesh("", loadingParams.path, loadingParams.filename, this.scene, (meshes: AbstractMesh[]) => {
  345. if (useGeneric) {
  346. this._getGenericParentMesh(meshes);
  347. } else {
  348. this._setRootMesh(meshes);
  349. }
  350. this._processLoadedModel(meshes);
  351. this._modelReady = true;
  352. this.onModelLoadedObservable.notifyObservers(this);
  353. resolve(true);
  354. }, null, (_scene: Scene, message: string) => {
  355. Logger.Log(message);
  356. Logger.Warn(`Failed to retrieve controller model of type ${this.profileId} from the remote server: ${loadingParams.path}${loadingParams.filename}`);
  357. reject(message);
  358. });
  359. });
  360. }
  361. /**
  362. * Update this model using the current XRFrame
  363. * @param xrFrame the current xr frame to use and update the model
  364. */
  365. public updateFromXRFrame(xrFrame: XRFrame): void {
  366. this.getComponentIds().forEach((id) => this.getComponent(id).update(this.gamepadObject));
  367. this.updateModel(xrFrame);
  368. }
  369. /**
  370. * Backwards compatibility due to a deeply-integrated typo
  371. */
  372. public get handness() {
  373. return this.handedness;
  374. }
  375. /**
  376. * Pulse (vibrate) this controller
  377. * If the controller does not support pulses, this function will fail silently and return Promise<false> directly after called
  378. * Consecutive calls to this function will cancel the last pulse call
  379. *
  380. * @param value the strength of the pulse in 0.0...1.0 range
  381. * @param duration Duration of the pulse in milliseconds
  382. * @param hapticActuatorIndex optional index of actuator (will usually be 0)
  383. * @returns a promise that will send true when the pulse has ended and false if the device doesn't support pulse or an error accrued
  384. */
  385. public pulse(value: number, duration: number, hapticActuatorIndex: number = 0): Promise<boolean> {
  386. if (this.gamepadObject.hapticActuators && this.gamepadObject.hapticActuators[hapticActuatorIndex]) {
  387. return this.gamepadObject.hapticActuators[hapticActuatorIndex].pulse(value, duration);
  388. } else {
  389. return Promise.resolve(false);
  390. }
  391. }
  392. // Look through all children recursively. This will return null if no mesh exists with the given name.
  393. protected _getChildByName(node: AbstractMesh, name: string): AbstractMesh {
  394. return <AbstractMesh>node.getChildren((n) => n.name === name, false)[0];
  395. }
  396. // Look through only immediate children. This will return null if no mesh exists with the given name.
  397. protected _getImmediateChildByName(node: AbstractMesh, name: string): AbstractMesh {
  398. return <AbstractMesh>node.getChildren((n) => n.name == name, true)[0];
  399. }
  400. /**
  401. * Moves the axis on the controller mesh based on its current state
  402. * @param axis the index of the axis
  403. * @param axisValue the value of the axis which determines the meshes new position
  404. * @hidden
  405. */
  406. protected _lerpTransform(axisMap: IMotionControllerMeshMap, axisValue: number, fixValueCoordinates?: boolean): void {
  407. if (!axisMap.minMesh || !axisMap.maxMesh) {
  408. return;
  409. }
  410. if (!axisMap.minMesh.rotationQuaternion || !axisMap.maxMesh.rotationQuaternion || !axisMap.valueMesh.rotationQuaternion) {
  411. return;
  412. }
  413. // Convert from gamepad value range (-1 to +1) to lerp range (0 to 1)
  414. let lerpValue = fixValueCoordinates ? axisValue * 0.5 + 0.5 : axisValue;
  415. Quaternion.SlerpToRef(
  416. axisMap.minMesh.rotationQuaternion,
  417. axisMap.maxMesh.rotationQuaternion,
  418. lerpValue,
  419. axisMap.valueMesh.rotationQuaternion);
  420. Vector3.LerpToRef(
  421. axisMap.minMesh.position,
  422. axisMap.maxMesh.position,
  423. lerpValue,
  424. axisMap.valueMesh.position);
  425. }
  426. /**
  427. * Update the model itself with the current frame data
  428. * @param xrFrame the frame to use for updating the model mesh
  429. */
  430. protected updateModel(xrFrame: XRFrame): void {
  431. if (!this._modelReady) {
  432. return;
  433. }
  434. this._updateModel(xrFrame);
  435. }
  436. /**
  437. * Get the filename and path for this controller's model
  438. * @returns a map of filename and path
  439. */
  440. protected abstract _getFilenameAndPath(): { filename: string, path: string };
  441. /**
  442. * This function is called before the mesh is loaded. It checks for loading constraints.
  443. * For example, this function can check if the GLB loader is available
  444. * If this function returns false, the generic controller will be loaded instead
  445. * @returns Is the client ready to load the mesh
  446. */
  447. protected abstract _getModelLoadingConstraints(): boolean;
  448. /**
  449. * This function will be called after the model was successfully loaded and can be used
  450. * for mesh transformations before it is available for the user
  451. * @param meshes the loaded meshes
  452. */
  453. protected abstract _processLoadedModel(meshes: AbstractMesh[]): void;
  454. /**
  455. * Set the root mesh for this controller. Important for the WebXR controller class
  456. * @param meshes the loaded meshes
  457. */
  458. protected abstract _setRootMesh(meshes: AbstractMesh[]): void;
  459. /**
  460. * A function executed each frame that updates the mesh (if needed)
  461. * @param xrFrame the current xrFrame
  462. */
  463. protected abstract _updateModel(xrFrame: XRFrame): void;
  464. private _getGenericFilenameAndPath(): { filename: string, path: string } {
  465. return {
  466. filename: "generic.babylon",
  467. path: "https://controllers.babylonjs.com/generic/"
  468. };
  469. }
  470. private _getGenericParentMesh(meshes: AbstractMesh[]): void {
  471. this.rootMesh = new Mesh(this.profileId + " " + this.handedness, this.scene);
  472. meshes.forEach((mesh) => {
  473. if (!mesh.parent) {
  474. mesh.isPickable = false;
  475. mesh.setParent(this.rootMesh);
  476. }
  477. });
  478. this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
  479. }
  480. }