WebXRControllerMovement.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import { WebXRFeaturesManager, WebXRFeatureName } from "../webXRFeaturesManager.js";
  2. import { WebXRControllerComponent } from "../motionController/webXRControllerComponent.js";
  3. import { Matrix, Quaternion, Vector3 } from "../../Maths/math.vector.js";
  4. import { WebXRAbstractFeature } from "./WebXRAbstractFeature.js";
  5. import { Tools } from "../../Misc/tools.js";
  6. /**
  7. * This is a movement feature to be used with WebXR-enabled motion controllers.
  8. * When enabled and attached, the feature will allow a user to move around and rotate in the scene using
  9. * the input of the attached controllers.
  10. */
  11. export class WebXRControllerMovement extends WebXRAbstractFeature {
  12. /**
  13. * Current movement direction. Will be null before XR Frames have been processed.
  14. */
  15. get movementDirection() {
  16. return this._movementDirection;
  17. }
  18. /**
  19. * Is movement enabled
  20. */
  21. get movementEnabled() {
  22. return this._featureContext.movementEnabled;
  23. }
  24. /**
  25. * Sets whether movement is enabled or not
  26. * @param enabled is movement enabled
  27. */
  28. set movementEnabled(enabled) {
  29. this._featureContext.movementEnabled = enabled;
  30. }
  31. /**
  32. * If movement follows viewer pose
  33. */
  34. get movementOrientationFollowsViewerPose() {
  35. return this._featureContext.movementOrientationFollowsViewerPose;
  36. }
  37. /**
  38. * Sets whether movement follows viewer pose
  39. * @param followsPose is movement should follow viewer pose
  40. */
  41. set movementOrientationFollowsViewerPose(followsPose) {
  42. this._featureContext.movementOrientationFollowsViewerPose = followsPose;
  43. }
  44. /**
  45. * Gets movement speed
  46. */
  47. get movementSpeed() {
  48. return this._featureContext.movementSpeed;
  49. }
  50. /**
  51. * Sets movement speed
  52. * @param movementSpeed movement speed
  53. */
  54. set movementSpeed(movementSpeed) {
  55. this._featureContext.movementSpeed = movementSpeed;
  56. }
  57. /**
  58. * Gets minimum threshold the controller's thumbstick/touchpad must pass before being recognized for movement (avoids jitter/unintentional movement)
  59. */
  60. get movementThreshold() {
  61. return this._featureContext.movementThreshold;
  62. }
  63. /**
  64. * Sets minimum threshold the controller's thumbstick/touchpad must pass before being recognized for movement (avoids jitter/unintentional movement)
  65. * @param movementThreshold new threshold
  66. */
  67. set movementThreshold(movementThreshold) {
  68. this._featureContext.movementThreshold = movementThreshold;
  69. }
  70. /**
  71. * Is rotation enabled
  72. */
  73. get rotationEnabled() {
  74. return this._featureContext.rotationEnabled;
  75. }
  76. /**
  77. * Sets whether rotation is enabled or not
  78. * @param enabled is rotation enabled
  79. */
  80. set rotationEnabled(enabled) {
  81. this._featureContext.rotationEnabled = enabled;
  82. }
  83. /**
  84. * Gets rotation speed factor
  85. */
  86. get rotationSpeed() {
  87. return this._featureContext.rotationSpeed;
  88. }
  89. /**
  90. * Sets rotation speed factor (1.0 is default)
  91. * @param rotationSpeed new rotation speed factor
  92. */
  93. set rotationSpeed(rotationSpeed) {
  94. this._featureContext.rotationSpeed = rotationSpeed;
  95. }
  96. /**
  97. * Gets minimum threshold the controller's thumbstick/touchpad must pass before being recognized for rotation (avoids jitter/unintentional rotation)
  98. */
  99. get rotationThreshold() {
  100. return this._featureContext.rotationThreshold;
  101. }
  102. /**
  103. * Sets minimum threshold the controller's thumbstick/touchpad must pass before being recognized for rotation (avoids jitter/unintentional rotation)
  104. * @param threshold new threshold
  105. */
  106. set rotationThreshold(threshold) {
  107. this._featureContext.rotationThreshold = threshold;
  108. }
  109. /**
  110. * constructs a new movement controller system
  111. * @param _xrSessionManager an instance of WebXRSessionManager
  112. * @param options configuration object for this feature
  113. */
  114. constructor(_xrSessionManager, options) {
  115. super(_xrSessionManager);
  116. this._controllers = {};
  117. this._currentRegistrationConfigurations = [];
  118. // forward direction for movement, which may differ from viewer pose.
  119. this._movementDirection = new Quaternion();
  120. // unused
  121. this._tmpRotationMatrix = Matrix.Identity();
  122. this._tmpTranslationDirection = new Vector3();
  123. this._tmpMovementTranslation = new Vector3();
  124. this._tempCacheQuaternion = new Quaternion();
  125. this._attachController = (xrController) => {
  126. if (this._controllers[xrController.uniqueId]) {
  127. // already attached
  128. return;
  129. }
  130. this._controllers[xrController.uniqueId] = {
  131. xrController,
  132. registeredComponents: [],
  133. };
  134. const controllerData = this._controllers[xrController.uniqueId];
  135. // movement controller only available to gamepad-enabled input sources.
  136. if (controllerData.xrController.inputSource.targetRayMode === "tracked-pointer" && controllerData.xrController.inputSource.gamepad) {
  137. // motion controller support
  138. const initController = () => {
  139. if (xrController.motionController) {
  140. for (const registration of this._currentRegistrationConfigurations) {
  141. let component = null;
  142. if (registration.allowedComponentTypes) {
  143. for (const componentType of registration.allowedComponentTypes) {
  144. const componentOfType = xrController.motionController.getComponentOfType(componentType);
  145. if (componentOfType !== null) {
  146. component = componentOfType;
  147. break;
  148. }
  149. }
  150. }
  151. if (registration.mainComponentOnly) {
  152. const mainComponent = xrController.motionController.getMainComponent();
  153. if (mainComponent === null) {
  154. continue;
  155. }
  156. component = mainComponent;
  157. }
  158. if (typeof registration.componentSelectionPredicate === "function") {
  159. // if does not match we do want to ignore a previously found component
  160. component = registration.componentSelectionPredicate(xrController);
  161. }
  162. if (component && registration.forceHandedness) {
  163. if (xrController.inputSource.handedness !== registration.forceHandedness) {
  164. continue; // do not register
  165. }
  166. }
  167. if (component === null) {
  168. continue; // do not register
  169. }
  170. const registeredComponent = {
  171. registrationConfiguration: registration,
  172. component,
  173. };
  174. controllerData.registeredComponents.push(registeredComponent);
  175. if ("axisChangedHandler" in registration) {
  176. registeredComponent.onAxisChangedObserver = component.onAxisValueChangedObservable.add((axesData) => {
  177. registration.axisChangedHandler(axesData, this._movementState, this._featureContext, this._xrInput);
  178. });
  179. }
  180. if ("buttonChangedhandler" in registration) {
  181. registeredComponent.onButtonChangedObserver = component.onButtonStateChangedObservable.add(() => {
  182. if (component.changes.pressed) {
  183. registration.buttonChangedhandler(component.changes.pressed, this._movementState, this._featureContext, this._xrInput);
  184. }
  185. });
  186. }
  187. }
  188. }
  189. };
  190. if (xrController.motionController) {
  191. initController();
  192. }
  193. else {
  194. xrController.onMotionControllerInitObservable.addOnce(() => {
  195. initController();
  196. });
  197. }
  198. }
  199. };
  200. if (!options || options.xrInput === undefined) {
  201. Tools.Error('WebXRControllerMovement feature requires "xrInput" option.');
  202. return;
  203. }
  204. if (Array.isArray(options.customRegistrationConfigurations)) {
  205. this._currentRegistrationConfigurations = options.customRegistrationConfigurations;
  206. }
  207. else {
  208. this._currentRegistrationConfigurations = WebXRControllerMovement.REGISTRATIONS.default;
  209. }
  210. // synchronized from feature setter properties
  211. this._featureContext = {
  212. movementEnabled: options.movementEnabled || true,
  213. movementOrientationFollowsViewerPose: options.movementOrientationFollowsViewerPose ?? true,
  214. movementSpeed: options.movementSpeed ?? 1,
  215. movementThreshold: options.movementThreshold ?? 0.25,
  216. rotationEnabled: options.rotationEnabled ?? true,
  217. rotationSpeed: options.rotationSpeed ?? 1.0,
  218. rotationThreshold: options.rotationThreshold ?? 0.25,
  219. };
  220. this._movementState = {
  221. moveX: 0,
  222. moveY: 0,
  223. rotateX: 0,
  224. rotateY: 0,
  225. };
  226. this._xrInput = options.xrInput;
  227. }
  228. attach() {
  229. if (!super.attach()) {
  230. return false;
  231. }
  232. this._xrInput.controllers.forEach(this._attachController);
  233. this._addNewAttachObserver(this._xrInput.onControllerAddedObservable, this._attachController);
  234. this._addNewAttachObserver(this._xrInput.onControllerRemovedObservable, (controller) => {
  235. // REMOVE the controller
  236. this._detachController(controller.uniqueId);
  237. });
  238. return true;
  239. }
  240. detach() {
  241. if (!super.detach()) {
  242. return false;
  243. }
  244. Object.keys(this._controllers).forEach((controllerId) => {
  245. this._detachController(controllerId);
  246. });
  247. this._controllers = {};
  248. return true;
  249. }
  250. /**
  251. * Occurs on every XR frame.
  252. * @param _xrFrame
  253. */
  254. _onXRFrame(_xrFrame) {
  255. if (!this.attached) {
  256. return;
  257. }
  258. if (this._movementState.rotateX !== 0 && this._featureContext.rotationEnabled) {
  259. // smooth rotation
  260. const deltaMillis = this._xrSessionManager.scene.getEngine().getDeltaTime();
  261. const rotationY = deltaMillis * 0.001 * this._featureContext.rotationSpeed * this._movementState.rotateX * (this._xrSessionManager.scene.useRightHandedSystem ? -1 : 1);
  262. if (this._featureContext.movementOrientationFollowsViewerPose) {
  263. this._xrInput.xrCamera.cameraRotation.y += rotationY;
  264. Quaternion.RotationYawPitchRollToRef(rotationY, 0, 0, this._tempCacheQuaternion);
  265. this._xrInput.xrCamera.rotationQuaternion.multiplyToRef(this._tempCacheQuaternion, this._movementDirection);
  266. }
  267. else {
  268. // movement orientation direction does not affect camera. We use rotation speed multiplier
  269. // otherwise need to implement inertia and constraints for same feel as TargetCamera.
  270. Quaternion.RotationYawPitchRollToRef(rotationY * 3.0, 0, 0, this._tempCacheQuaternion);
  271. this._movementDirection.multiplyInPlace(this._tempCacheQuaternion);
  272. }
  273. }
  274. else if (this._featureContext.movementOrientationFollowsViewerPose) {
  275. this._movementDirection.copyFrom(this._xrInput.xrCamera.rotationQuaternion);
  276. }
  277. if ((this._movementState.moveX || this._movementState.moveY) && this._featureContext.movementEnabled) {
  278. Matrix.FromQuaternionToRef(this._movementDirection, this._tmpRotationMatrix);
  279. this._tmpTranslationDirection.set(this._movementState.moveX, 0, this._movementState.moveY * (this._xrSessionManager.scene.useRightHandedSystem ? 1.0 : -1.0));
  280. // move according to forward direction based on camera speed
  281. Vector3.TransformCoordinatesToRef(this._tmpTranslationDirection, this._tmpRotationMatrix, this._tmpMovementTranslation);
  282. this._tmpMovementTranslation.scaleInPlace(this._xrInput.xrCamera._computeLocalCameraSpeed() * this._featureContext.movementSpeed);
  283. this._xrInput.xrCamera.cameraDirection.addInPlace(this._tmpMovementTranslation);
  284. }
  285. }
  286. _detachController(xrControllerUniqueId) {
  287. const controllerData = this._controllers[xrControllerUniqueId];
  288. if (!controllerData) {
  289. return;
  290. }
  291. for (const registeredComponent of controllerData.registeredComponents) {
  292. if (registeredComponent.onAxisChangedObserver) {
  293. registeredComponent.component.onAxisValueChangedObservable.remove(registeredComponent.onAxisChangedObserver);
  294. }
  295. if (registeredComponent.onButtonChangedObserver) {
  296. registeredComponent.component.onButtonStateChangedObservable.remove(registeredComponent.onButtonChangedObserver);
  297. }
  298. }
  299. // remove from the map
  300. delete this._controllers[xrControllerUniqueId];
  301. }
  302. }
  303. /**
  304. * The module's name
  305. */
  306. WebXRControllerMovement.Name = WebXRFeatureName.MOVEMENT;
  307. /**
  308. * Standard controller configurations.
  309. */
  310. WebXRControllerMovement.REGISTRATIONS = {
  311. default: [
  312. {
  313. allowedComponentTypes: [WebXRControllerComponent.THUMBSTICK_TYPE, WebXRControllerComponent.TOUCHPAD_TYPE],
  314. forceHandedness: "left",
  315. axisChangedHandler: (axes, movementState, featureContext) => {
  316. movementState.rotateX = Math.abs(axes.x) > featureContext.rotationThreshold ? axes.x : 0;
  317. movementState.rotateY = Math.abs(axes.y) > featureContext.rotationThreshold ? axes.y : 0;
  318. },
  319. },
  320. {
  321. allowedComponentTypes: [WebXRControllerComponent.THUMBSTICK_TYPE, WebXRControllerComponent.TOUCHPAD_TYPE],
  322. forceHandedness: "right",
  323. axisChangedHandler: (axes, movementState, featureContext) => {
  324. movementState.moveX = Math.abs(axes.x) > featureContext.movementThreshold ? axes.x : 0;
  325. movementState.moveY = Math.abs(axes.y) > featureContext.movementThreshold ? axes.y : 0;
  326. },
  327. },
  328. ],
  329. };
  330. /**
  331. * The (Babylon) version of this module.
  332. * This is an integer representing the implementation version.
  333. * This number does not correspond to the webxr specs version
  334. */
  335. WebXRControllerMovement.Version = 1;
  336. WebXRFeaturesManager.AddWebXRFeature(WebXRControllerMovement.Name, (xrSessionManager, options) => {
  337. return () => new WebXRControllerMovement(xrSessionManager, options);
  338. }, WebXRControllerMovement.Version, true);
  339. //# sourceMappingURL=WebXRControllerMovement.js.map