webXRAbstractMotionController.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import { WebXRControllerComponent } from "./webXRControllerComponent.js";
  2. import { Observable } from "../../Misc/observable.js";
  3. import { Logger } from "../../Misc/logger.js";
  4. import { SceneLoader } from "../../Loading/sceneLoader.js";
  5. import { Quaternion, Vector3 } from "../../Maths/math.vector.js";
  6. import { Mesh } from "../../Meshes/mesh.js";
  7. /**
  8. * An Abstract Motion controller
  9. * This class receives an xrInput and a profile layout and uses those to initialize the components
  10. * Each component has an observable to check for changes in value and state
  11. */
  12. export class WebXRAbstractMotionController {
  13. /**
  14. * constructs a new abstract motion controller
  15. * @param scene the scene to which the model of the controller will be added
  16. * @param layout The profile layout to load
  17. * @param gamepadObject The gamepad object correlating to this controller
  18. * @param handedness handedness (left/right/none) of this controller
  19. * @param _doNotLoadControllerMesh set this flag to ignore the mesh loading
  20. * @param _controllerCache a cache holding controller models already loaded in this session
  21. */
  22. constructor(
  23. // eslint-disable-next-line @typescript-eslint/naming-convention
  24. scene,
  25. // eslint-disable-next-line @typescript-eslint/naming-convention
  26. layout,
  27. /**
  28. * The gamepad object correlating to this controller
  29. */
  30. gamepadObject,
  31. /**
  32. * handedness (left/right/none) of this controller
  33. */
  34. handedness,
  35. /**
  36. * @internal
  37. */
  38. _doNotLoadControllerMesh = false, _controllerCache) {
  39. this.scene = scene;
  40. this.layout = layout;
  41. this.gamepadObject = gamepadObject;
  42. this.handedness = handedness;
  43. this._doNotLoadControllerMesh = _doNotLoadControllerMesh;
  44. this._controllerCache = _controllerCache;
  45. this._initComponent = (id) => {
  46. if (!id) {
  47. return;
  48. }
  49. const componentDef = this.layout.components[id];
  50. const type = componentDef.type;
  51. const buttonIndex = componentDef.gamepadIndices.button;
  52. // search for axes
  53. const axes = [];
  54. if (componentDef.gamepadIndices.xAxis !== undefined && componentDef.gamepadIndices.yAxis !== undefined) {
  55. axes.push(componentDef.gamepadIndices.xAxis, componentDef.gamepadIndices.yAxis);
  56. }
  57. this.components[id] = new WebXRControllerComponent(id, type, buttonIndex, axes);
  58. };
  59. this._modelReady = false;
  60. /**
  61. * A map of components (WebXRControllerComponent) in this motion controller
  62. * Components have a ComponentType and can also have both button and axis definitions
  63. */
  64. this.components = {};
  65. /**
  66. * Disable the model's animation. Can be set at any time.
  67. */
  68. this.disableAnimation = false;
  69. /**
  70. * Observers registered here will be triggered when the model of this controller is done loading
  71. */
  72. this.onModelLoadedObservable = new Observable();
  73. // initialize the components
  74. if (layout.components) {
  75. Object.keys(layout.components).forEach(this._initComponent);
  76. }
  77. // Model is loaded in WebXRInput
  78. }
  79. /**
  80. * Dispose this controller, the model mesh and all its components
  81. */
  82. dispose() {
  83. this.getComponentIds().forEach((id) => this.getComponent(id).dispose());
  84. if (this.rootMesh) {
  85. this.rootMesh.getChildren(undefined, true).forEach((node) => {
  86. node.setEnabled(false);
  87. });
  88. this.rootMesh.dispose(!!this._controllerCache, !this._controllerCache);
  89. }
  90. }
  91. /**
  92. * Returns all components of specific type
  93. * @param type the type to search for
  94. * @returns an array of components with this type
  95. */
  96. getAllComponentsOfType(type) {
  97. return this.getComponentIds()
  98. .map((id) => this.components[id])
  99. .filter((component) => component.type === type);
  100. }
  101. /**
  102. * get a component based an its component id as defined in layout.components
  103. * @param id the id of the component
  104. * @returns the component correlates to the id or undefined if not found
  105. */
  106. getComponent(id) {
  107. return this.components[id];
  108. }
  109. /**
  110. * Get the list of components available in this motion controller
  111. * @returns an array of strings correlating to available components
  112. */
  113. getComponentIds() {
  114. return Object.keys(this.components);
  115. }
  116. /**
  117. * Get the first component of specific type
  118. * @param type type of component to find
  119. * @returns a controller component or null if not found
  120. */
  121. getComponentOfType(type) {
  122. return this.getAllComponentsOfType(type)[0] || null;
  123. }
  124. /**
  125. * Get the main (Select) component of this controller as defined in the layout
  126. * @returns the main component of this controller
  127. */
  128. getMainComponent() {
  129. return this.getComponent(this.layout.selectComponentId);
  130. }
  131. /**
  132. * Loads the model correlating to this controller
  133. * When the mesh is loaded, the onModelLoadedObservable will be triggered
  134. * @returns A promise fulfilled with the result of the model loading
  135. */
  136. async loadModel() {
  137. const useGeneric = !this._getModelLoadingConstraints();
  138. let loadingParams = this._getGenericFilenameAndPath();
  139. // Checking if GLB loader is present
  140. if (useGeneric) {
  141. Logger.Warn("Falling back to generic models");
  142. }
  143. else {
  144. loadingParams = this._getFilenameAndPath();
  145. }
  146. return new Promise((resolve, reject) => {
  147. const meshesLoaded = (meshes) => {
  148. if (useGeneric) {
  149. this._getGenericParentMesh(meshes);
  150. }
  151. else {
  152. this._setRootMesh(meshes);
  153. }
  154. this._processLoadedModel(meshes);
  155. this._modelReady = true;
  156. this.onModelLoadedObservable.notifyObservers(this);
  157. resolve(true);
  158. };
  159. if (this._controllerCache) {
  160. // look for it in the cache
  161. const found = this._controllerCache.filter((c) => {
  162. return c.filename === loadingParams.filename && c.path === loadingParams.path;
  163. });
  164. if (found[0]) {
  165. found[0].meshes.forEach((mesh) => mesh.setEnabled(true));
  166. meshesLoaded(found[0].meshes);
  167. return;
  168. // found, don't continue to load
  169. }
  170. }
  171. SceneLoader.ImportMesh("", loadingParams.path, loadingParams.filename, this.scene, (meshes) => {
  172. if (this._controllerCache) {
  173. this._controllerCache.push({
  174. ...loadingParams,
  175. meshes,
  176. });
  177. }
  178. meshesLoaded(meshes);
  179. }, null, (_scene, message) => {
  180. Logger.Log(message);
  181. Logger.Warn(`Failed to retrieve controller model of type ${this.profileId} from the remote server: ${loadingParams.path}${loadingParams.filename}`);
  182. reject(message);
  183. });
  184. });
  185. }
  186. /**
  187. * Update this model using the current XRFrame
  188. * @param xrFrame the current xr frame to use and update the model
  189. */
  190. updateFromXRFrame(xrFrame) {
  191. this.getComponentIds().forEach((id) => this.getComponent(id).update(this.gamepadObject));
  192. this.updateModel(xrFrame);
  193. }
  194. /**
  195. * Backwards compatibility due to a deeply-integrated typo
  196. */
  197. get handness() {
  198. return this.handedness;
  199. }
  200. /**
  201. * Pulse (vibrate) this controller
  202. * If the controller does not support pulses, this function will fail silently and return Promise<false> directly after called
  203. * Consecutive calls to this function will cancel the last pulse call
  204. *
  205. * @param value the strength of the pulse in 0.0...1.0 range
  206. * @param duration Duration of the pulse in milliseconds
  207. * @param hapticActuatorIndex optional index of actuator (will usually be 0)
  208. * @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
  209. */
  210. pulse(value, duration, hapticActuatorIndex = 0) {
  211. if (this.gamepadObject.hapticActuators && this.gamepadObject.hapticActuators[hapticActuatorIndex]) {
  212. return this.gamepadObject.hapticActuators[hapticActuatorIndex].pulse(value, duration);
  213. }
  214. else {
  215. return Promise.resolve(false);
  216. }
  217. }
  218. // Look through all children recursively. This will return null if no mesh exists with the given name.
  219. _getChildByName(node, name) {
  220. return node.getChildren((n) => n.name === name, false)[0];
  221. }
  222. // Look through only immediate children. This will return null if no mesh exists with the given name.
  223. _getImmediateChildByName(node, name) {
  224. return node.getChildren((n) => n.name == name, true)[0];
  225. }
  226. /**
  227. * Moves the axis on the controller mesh based on its current state
  228. * @param axisMap
  229. * @param axisValue the value of the axis which determines the meshes new position
  230. * @internal
  231. */
  232. _lerpTransform(axisMap, axisValue, fixValueCoordinates) {
  233. if (!axisMap.minMesh || !axisMap.maxMesh || !axisMap.valueMesh) {
  234. return;
  235. }
  236. if (!axisMap.minMesh.rotationQuaternion || !axisMap.maxMesh.rotationQuaternion || !axisMap.valueMesh.rotationQuaternion) {
  237. return;
  238. }
  239. // Convert from gamepad value range (-1 to +1) to lerp range (0 to 1)
  240. const lerpValue = fixValueCoordinates ? axisValue * 0.5 + 0.5 : axisValue;
  241. Quaternion.SlerpToRef(axisMap.minMesh.rotationQuaternion, axisMap.maxMesh.rotationQuaternion, lerpValue, axisMap.valueMesh.rotationQuaternion);
  242. Vector3.LerpToRef(axisMap.minMesh.position, axisMap.maxMesh.position, lerpValue, axisMap.valueMesh.position);
  243. }
  244. /**
  245. * Update the model itself with the current frame data
  246. * @param xrFrame the frame to use for updating the model mesh
  247. */
  248. // eslint-disable-next-line @typescript-eslint/naming-convention
  249. updateModel(xrFrame) {
  250. if (!this._modelReady) {
  251. return;
  252. }
  253. this._updateModel(xrFrame);
  254. }
  255. _getGenericFilenameAndPath() {
  256. return {
  257. filename: "generic.babylon",
  258. path: "https://controllers.babylonjs.com/generic/",
  259. };
  260. }
  261. _getGenericParentMesh(meshes) {
  262. this.rootMesh = new Mesh(this.profileId + " " + this.handedness, this.scene);
  263. meshes.forEach((mesh) => {
  264. if (!mesh.parent) {
  265. mesh.isPickable = false;
  266. mesh.setParent(this.rootMesh);
  267. }
  268. });
  269. this.rootMesh.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
  270. }
  271. }
  272. //# sourceMappingURL=webXRAbstractMotionController.js.map