WebXRNearInteraction.js 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831
  1. import { WebXRFeaturesManager, WebXRFeatureName } from "../webXRFeaturesManager.js";
  2. import { CreateSphere } from "../../Meshes/Builders/sphereBuilder.js";
  3. import { Vector3, Quaternion, TmpVectors } from "../../Maths/math.vector.js";
  4. import { Ray } from "../../Culling/ray.js";
  5. import { PickingInfo } from "../../Collisions/pickingInfo.js";
  6. import { WebXRAbstractFeature } from "./WebXRAbstractFeature.js";
  7. import { UtilityLayerRenderer } from "../../Rendering/utilityLayerRenderer.js";
  8. import { BoundingSphere } from "../../Culling/boundingSphere.js";
  9. import { StandardMaterial } from "../../Materials/standardMaterial.js";
  10. import { Color3 } from "../../Maths/math.color.js";
  11. import { NodeMaterial } from "../../Materials/Node/nodeMaterial.js";
  12. import { Animation } from "../../Animations/animation.js";
  13. import { QuadraticEase, EasingFunction } from "../../Animations/easing.js";
  14. // side effects
  15. import "../../Meshes/subMesh.project.js";
  16. // Tracks the interaction animation state when using a motion controller with a near interaction orb
  17. var ControllerOrbAnimationState;
  18. (function (ControllerOrbAnimationState) {
  19. /**
  20. * Orb is invisible
  21. */
  22. ControllerOrbAnimationState[ControllerOrbAnimationState["DEHYDRATED"] = 0] = "DEHYDRATED";
  23. /**
  24. * Orb is visible and inside the hover range
  25. */
  26. ControllerOrbAnimationState[ControllerOrbAnimationState["HOVER"] = 1] = "HOVER";
  27. /**
  28. * Orb is visible and touching a near interaction target
  29. */
  30. ControllerOrbAnimationState[ControllerOrbAnimationState["TOUCH"] = 2] = "TOUCH";
  31. })(ControllerOrbAnimationState || (ControllerOrbAnimationState = {}));
  32. /**
  33. * Where should the near interaction mesh be attached to when using a motion controller for near interaction
  34. */
  35. export var WebXRNearControllerMode;
  36. (function (WebXRNearControllerMode) {
  37. /**
  38. * Motion controllers will not support near interaction
  39. */
  40. WebXRNearControllerMode[WebXRNearControllerMode["DISABLED"] = 0] = "DISABLED";
  41. /**
  42. * The interaction point for motion controllers will be inside of them
  43. */
  44. WebXRNearControllerMode[WebXRNearControllerMode["CENTERED_ON_CONTROLLER"] = 1] = "CENTERED_ON_CONTROLLER";
  45. /**
  46. * The interaction point for motion controllers will be in front of the controller
  47. */
  48. WebXRNearControllerMode[WebXRNearControllerMode["CENTERED_IN_FRONT"] = 2] = "CENTERED_IN_FRONT";
  49. })(WebXRNearControllerMode || (WebXRNearControllerMode = {}));
  50. /**
  51. * A module that will enable near interaction near interaction for hands and motion controllers of XR Input Sources
  52. */
  53. export class WebXRNearInteraction extends WebXRAbstractFeature {
  54. /**
  55. * constructs a new background remover module
  56. * @param _xrSessionManager the session manager for this module
  57. * @param _options read-only options to be used in this module
  58. */
  59. constructor(_xrSessionManager, _options) {
  60. super(_xrSessionManager);
  61. this._options = _options;
  62. this._tmpRay = new Ray(new Vector3(), new Vector3());
  63. this._attachController = (xrController) => {
  64. if (this._controllers[xrController.uniqueId]) {
  65. // already attached
  66. return;
  67. }
  68. // get two new meshes
  69. const { touchCollisionMesh, touchCollisionMeshFunction, hydrateCollisionMeshFunction } = this._generateNewTouchPointMesh();
  70. const selectionMesh = this._generateVisualCue();
  71. this._controllers[xrController.uniqueId] = {
  72. xrController,
  73. meshUnderPointer: null,
  74. nearInteractionTargetMesh: null,
  75. pick: null,
  76. stalePick: null,
  77. touchCollisionMesh,
  78. touchCollisionMeshFunction: touchCollisionMeshFunction,
  79. hydrateCollisionMeshFunction: hydrateCollisionMeshFunction,
  80. currentAnimationState: ControllerOrbAnimationState.DEHYDRATED,
  81. grabRay: new Ray(new Vector3(), new Vector3()),
  82. hoverInteraction: false,
  83. nearInteraction: false,
  84. grabInteraction: false,
  85. downTriggered: false,
  86. id: WebXRNearInteraction._IdCounter++,
  87. pickedPointVisualCue: selectionMesh,
  88. };
  89. this._controllers[xrController.uniqueId]._worldScaleObserver =
  90. this._controllers[xrController.uniqueId]._worldScaleObserver ||
  91. this._xrSessionManager.onWorldScaleFactorChangedObservable.add((values) => {
  92. if (values.newScaleFactor !== values.previousScaleFactor) {
  93. this._controllers[xrController.uniqueId].touchCollisionMesh.dispose();
  94. this._controllers[xrController.uniqueId].pickedPointVisualCue.dispose();
  95. const { touchCollisionMesh, touchCollisionMeshFunction, hydrateCollisionMeshFunction } = this._generateNewTouchPointMesh();
  96. this._controllers[xrController.uniqueId].touchCollisionMesh = touchCollisionMesh;
  97. this._controllers[xrController.uniqueId].touchCollisionMeshFunction = touchCollisionMeshFunction;
  98. this._controllers[xrController.uniqueId].hydrateCollisionMeshFunction = hydrateCollisionMeshFunction;
  99. this._controllers[xrController.uniqueId].pickedPointVisualCue = this._generateVisualCue();
  100. }
  101. });
  102. if (this._attachedController) {
  103. if (!this._options.enableNearInteractionOnAllControllers &&
  104. this._options.preferredHandedness &&
  105. xrController.inputSource.handedness === this._options.preferredHandedness) {
  106. this._attachedController = xrController.uniqueId;
  107. }
  108. }
  109. else {
  110. if (!this._options.enableNearInteractionOnAllControllers) {
  111. this._attachedController = xrController.uniqueId;
  112. }
  113. }
  114. switch (xrController.inputSource.targetRayMode) {
  115. case "tracked-pointer":
  116. return this._attachNearInteractionMode(xrController);
  117. case "gaze":
  118. return null;
  119. case "screen":
  120. return null;
  121. }
  122. };
  123. this._controllers = {};
  124. this._farInteractionFeature = null;
  125. /**
  126. * default color of the selection ring
  127. */
  128. this.selectionMeshDefaultColor = new Color3(0.8, 0.8, 0.8);
  129. /**
  130. * This color will be applied to the selection ring when selection is triggered
  131. */
  132. this.selectionMeshPickedColor = new Color3(0.3, 0.3, 1.0);
  133. this._hoverRadius = 0.1;
  134. this._pickRadius = 0.02;
  135. this._controllerPickRadius = 0.03; // The radius is slightly larger here to make it easier to manipulate since it's not tied to the hand position
  136. this._nearGrabLengthScale = 5;
  137. this._scene = this._xrSessionManager.scene;
  138. if (this._options.nearInteractionControllerMode === undefined) {
  139. this._options.nearInteractionControllerMode = WebXRNearControllerMode.CENTERED_IN_FRONT;
  140. }
  141. if (this._options.farInteractionFeature) {
  142. this._farInteractionFeature = this._options.farInteractionFeature;
  143. }
  144. }
  145. /**
  146. * Attach this feature
  147. * Will usually be called by the features manager
  148. *
  149. * @returns true if successful.
  150. */
  151. attach() {
  152. if (!super.attach()) {
  153. return false;
  154. }
  155. this._options.xrInput.controllers.forEach(this._attachController);
  156. this._addNewAttachObserver(this._options.xrInput.onControllerAddedObservable, this._attachController);
  157. this._addNewAttachObserver(this._options.xrInput.onControllerRemovedObservable, (controller) => {
  158. // REMOVE the controller
  159. this._detachController(controller.uniqueId);
  160. });
  161. this._scene.constantlyUpdateMeshUnderPointer = true;
  162. return true;
  163. }
  164. /**
  165. * Detach this feature.
  166. * Will usually be called by the features manager
  167. *
  168. * @returns true if successful.
  169. */
  170. detach() {
  171. if (!super.detach()) {
  172. return false;
  173. }
  174. Object.keys(this._controllers).forEach((controllerId) => {
  175. this._detachController(controllerId);
  176. });
  177. return true;
  178. }
  179. /**
  180. * Will get the mesh under a specific pointer.
  181. * `scene.meshUnderPointer` will only return one mesh - either left or right.
  182. * @param controllerId the controllerId to check
  183. * @returns The mesh under pointer or null if no mesh is under the pointer
  184. */
  185. getMeshUnderPointer(controllerId) {
  186. if (this._controllers[controllerId]) {
  187. return this._controllers[controllerId].meshUnderPointer;
  188. }
  189. else {
  190. return null;
  191. }
  192. }
  193. /**
  194. * Get the xr controller that correlates to the pointer id in the pointer event
  195. *
  196. * @param id the pointer id to search for
  197. * @returns the controller that correlates to this id or null if not found
  198. */
  199. getXRControllerByPointerId(id) {
  200. const keys = Object.keys(this._controllers);
  201. for (let i = 0; i < keys.length; ++i) {
  202. if (this._controllers[keys[i]].id === id) {
  203. return this._controllers[keys[i]].xrController || null;
  204. }
  205. }
  206. return null;
  207. }
  208. /**
  209. * This function sets webXRControllerPointerSelection feature that will be disabled when
  210. * the hover range is reached for a mesh and will be reattached when not in hover range.
  211. * This is used to remove the selection rays when moving.
  212. * @param farInteractionFeature the feature to disable when finger is in hover range for a mesh
  213. */
  214. setFarInteractionFeature(farInteractionFeature) {
  215. this._farInteractionFeature = farInteractionFeature;
  216. }
  217. /**
  218. * Filter used for near interaction pick and hover
  219. * @param mesh the mesh candidate to be pick-filtered
  220. * @returns if the mesh should be included in the list of candidate meshes for near interaction
  221. */
  222. _nearPickPredicate(mesh) {
  223. return mesh.isEnabled() && mesh.isVisible && mesh.isPickable && mesh.isNearPickable;
  224. }
  225. /**
  226. * Filter used for near interaction grab
  227. * @param mesh the mesh candidate to be pick-filtered
  228. * @returns if the mesh should be included in the list of candidate meshes for near interaction
  229. */
  230. _nearGrabPredicate(mesh) {
  231. return mesh.isEnabled() && mesh.isVisible && mesh.isPickable && mesh.isNearGrabbable;
  232. }
  233. /**
  234. * Filter used for any near interaction
  235. * @param mesh the mesh candidate to be pick-filtered
  236. * @returns if the mesh should be included in the list of candidate meshes for near interaction
  237. */
  238. _nearInteractionPredicate(mesh) {
  239. return mesh.isEnabled() && mesh.isVisible && mesh.isPickable && (mesh.isNearPickable || mesh.isNearGrabbable);
  240. }
  241. _controllerAvailablePredicate(mesh, controllerId) {
  242. let parent = mesh;
  243. while (parent) {
  244. if (parent.reservedDataStore && parent.reservedDataStore.nearInteraction && parent.reservedDataStore.nearInteraction.excludedControllerId === controllerId) {
  245. return false;
  246. }
  247. parent = parent.parent;
  248. }
  249. return true;
  250. }
  251. _handleTransitionAnimation(controllerData, newState) {
  252. if (controllerData.currentAnimationState === newState ||
  253. this._options.nearInteractionControllerMode !== WebXRNearControllerMode.CENTERED_IN_FRONT ||
  254. !!controllerData.xrController?.inputSource.hand) {
  255. return;
  256. }
  257. // Don't always break to allow for animation fallthrough on rare cases of multi-transitions
  258. if (newState > controllerData.currentAnimationState) {
  259. switch (controllerData.currentAnimationState) {
  260. case ControllerOrbAnimationState.DEHYDRATED: {
  261. controllerData.hydrateCollisionMeshFunction(true);
  262. if (newState === ControllerOrbAnimationState.HOVER) {
  263. break;
  264. }
  265. }
  266. // eslint-disable-next-line no-fallthrough
  267. case ControllerOrbAnimationState.HOVER: {
  268. controllerData.touchCollisionMeshFunction(true);
  269. if (newState === ControllerOrbAnimationState.TOUCH) {
  270. break;
  271. }
  272. }
  273. }
  274. }
  275. else {
  276. switch (controllerData.currentAnimationState) {
  277. case ControllerOrbAnimationState.TOUCH: {
  278. controllerData.touchCollisionMeshFunction(false);
  279. if (newState === ControllerOrbAnimationState.HOVER) {
  280. break;
  281. }
  282. }
  283. // eslint-disable-next-line no-fallthrough
  284. case ControllerOrbAnimationState.HOVER: {
  285. controllerData.hydrateCollisionMeshFunction(false);
  286. if (newState === ControllerOrbAnimationState.DEHYDRATED) {
  287. break;
  288. }
  289. }
  290. }
  291. }
  292. controllerData.currentAnimationState = newState;
  293. }
  294. _processTouchPoint(id, position, orientation) {
  295. const controllerData = this._controllers[id];
  296. // Position and orientation could be temporary values, se we take care of them before calling any functions that use temporary vectors/quaternions
  297. controllerData.grabRay.origin.copyFrom(position);
  298. orientation.toEulerAnglesToRef(TmpVectors.Vector3[0]);
  299. controllerData.grabRay.direction.copyFrom(TmpVectors.Vector3[0]);
  300. if (this._options.nearInteractionControllerMode === WebXRNearControllerMode.CENTERED_IN_FRONT && !controllerData.xrController?.inputSource.hand) {
  301. // offset the touch point in the direction the transform is facing
  302. controllerData.xrController.getWorldPointerRayToRef(this._tmpRay);
  303. controllerData.grabRay.origin.addInPlace(this._tmpRay.direction.scale(0.05));
  304. }
  305. controllerData.grabRay.length = this._nearGrabLengthScale * this._hoverRadius * this._xrSessionManager.worldScalingFactor;
  306. controllerData.touchCollisionMesh.position.copyFrom(controllerData.grabRay.origin).scaleInPlace(this._xrSessionManager.worldScalingFactor);
  307. }
  308. _onXRFrame(_xrFrame) {
  309. Object.keys(this._controllers).forEach((id) => {
  310. // only do this for the selected pointer
  311. const controllerData = this._controllers[id];
  312. const handData = controllerData.xrController?.inputSource.hand;
  313. // If near interaction is not enabled/available for this controller, return early
  314. if ((!this._options.enableNearInteractionOnAllControllers && id !== this._attachedController) ||
  315. !controllerData.xrController ||
  316. (!handData && (!this._options.nearInteractionControllerMode || !controllerData.xrController.inputSource.gamepad))) {
  317. controllerData.pick = null;
  318. return;
  319. }
  320. controllerData.hoverInteraction = false;
  321. controllerData.nearInteraction = false;
  322. // Every frame check collisions/input
  323. if (controllerData.xrController) {
  324. if (handData) {
  325. const xrIndexTip = handData.get("index-finger-tip");
  326. if (xrIndexTip) {
  327. const indexTipPose = _xrFrame.getJointPose(xrIndexTip, this._xrSessionManager.referenceSpace);
  328. if (indexTipPose && indexTipPose.transform) {
  329. const axisRHSMultiplier = this._scene.useRightHandedSystem ? 1 : -1;
  330. TmpVectors.Vector3[0].set(indexTipPose.transform.position.x, indexTipPose.transform.position.y, indexTipPose.transform.position.z * axisRHSMultiplier);
  331. TmpVectors.Quaternion[0].set(indexTipPose.transform.orientation.x, indexTipPose.transform.orientation.y, indexTipPose.transform.orientation.z * axisRHSMultiplier, indexTipPose.transform.orientation.w * axisRHSMultiplier);
  332. this._processTouchPoint(id, TmpVectors.Vector3[0], TmpVectors.Quaternion[0]);
  333. }
  334. }
  335. }
  336. else if (controllerData.xrController.inputSource.gamepad && this._options.nearInteractionControllerMode !== WebXRNearControllerMode.DISABLED) {
  337. let controllerPose = controllerData.xrController.pointer;
  338. if (controllerData.xrController.grip && this._options.nearInteractionControllerMode === WebXRNearControllerMode.CENTERED_ON_CONTROLLER) {
  339. controllerPose = controllerData.xrController.grip;
  340. }
  341. this._processTouchPoint(id, controllerPose.position, controllerPose.rotationQuaternion);
  342. }
  343. }
  344. else {
  345. return;
  346. }
  347. const accuratePickInfo = (originalScenePick, utilityScenePick) => {
  348. let pick = null;
  349. if (!utilityScenePick || !utilityScenePick.hit) {
  350. // No hit in utility scene
  351. pick = originalScenePick;
  352. }
  353. else if (!originalScenePick || !originalScenePick.hit) {
  354. // No hit in original scene
  355. pick = utilityScenePick;
  356. }
  357. else if (utilityScenePick.distance < originalScenePick.distance) {
  358. // Hit is closer in utility scene
  359. pick = utilityScenePick;
  360. }
  361. else {
  362. // Hit is closer in original scene
  363. pick = originalScenePick;
  364. }
  365. return pick;
  366. };
  367. const populateNearInteractionInfo = (nearInteractionInfo) => {
  368. let result = new PickingInfo();
  369. let nearInteractionAtOrigin = false;
  370. const nearInteraction = nearInteractionInfo && nearInteractionInfo.pickedPoint && nearInteractionInfo.hit;
  371. if (nearInteractionInfo?.pickedPoint) {
  372. nearInteractionAtOrigin = nearInteractionInfo.pickedPoint.x === 0 && nearInteractionInfo.pickedPoint.y === 0 && nearInteractionInfo.pickedPoint.z === 0;
  373. }
  374. if (nearInteraction && !nearInteractionAtOrigin) {
  375. result = nearInteractionInfo;
  376. }
  377. return result;
  378. };
  379. // Don't perform touch logic while grabbing, to prevent triggering touch interactions while in the middle of a grab interaction
  380. // Dont update cursor logic either - the cursor should already be visible for the grab to be in range,
  381. // and in order to maintain its position on the target mesh it is parented for the duration of the grab.
  382. if (!controllerData.grabInteraction) {
  383. let pick = null;
  384. // near interaction hover
  385. let utilitySceneHoverPick = null;
  386. if (this._options.useUtilityLayer && this._utilityLayerScene) {
  387. utilitySceneHoverPick = this._pickWithSphere(controllerData, this._hoverRadius * this._xrSessionManager.worldScalingFactor, this._utilityLayerScene, (mesh) => this._nearInteractionPredicate(mesh));
  388. }
  389. const originalSceneHoverPick = this._pickWithSphere(controllerData, this._hoverRadius * this._xrSessionManager.worldScalingFactor, this._scene, (mesh) => this._nearInteractionPredicate(mesh));
  390. const hoverPickInfo = accuratePickInfo(originalSceneHoverPick, utilitySceneHoverPick);
  391. if (hoverPickInfo && hoverPickInfo.hit) {
  392. pick = populateNearInteractionInfo(hoverPickInfo);
  393. if (pick.hit) {
  394. controllerData.hoverInteraction = true;
  395. }
  396. }
  397. // near interaction pick
  398. if (controllerData.hoverInteraction) {
  399. let utilitySceneNearPick = null;
  400. const radius = (handData ? this._pickRadius : this._controllerPickRadius) * this._xrSessionManager.worldScalingFactor;
  401. if (this._options.useUtilityLayer && this._utilityLayerScene) {
  402. utilitySceneNearPick = this._pickWithSphere(controllerData, radius, this._utilityLayerScene, (mesh) => this._nearPickPredicate(mesh));
  403. }
  404. const originalSceneNearPick = this._pickWithSphere(controllerData, radius, this._scene, (mesh) => this._nearPickPredicate(mesh));
  405. const pickInfo = accuratePickInfo(originalSceneNearPick, utilitySceneNearPick);
  406. const nearPick = populateNearInteractionInfo(pickInfo);
  407. if (nearPick.hit) {
  408. // Near pick takes precedence over hover interaction
  409. pick = nearPick;
  410. controllerData.nearInteraction = true;
  411. }
  412. }
  413. controllerData.stalePick = controllerData.pick;
  414. controllerData.pick = pick;
  415. // Update mesh under pointer
  416. if (controllerData.pick && controllerData.pick.pickedPoint && controllerData.pick.hit) {
  417. controllerData.meshUnderPointer = controllerData.pick.pickedMesh;
  418. controllerData.pickedPointVisualCue.position.copyFrom(controllerData.pick.pickedPoint);
  419. controllerData.pickedPointVisualCue.isVisible = true;
  420. if (this._farInteractionFeature && this._farInteractionFeature.attached) {
  421. this._farInteractionFeature._setPointerSelectionDisabledByPointerId(controllerData.id, true);
  422. }
  423. }
  424. else {
  425. controllerData.meshUnderPointer = null;
  426. controllerData.pickedPointVisualCue.isVisible = false;
  427. if (this._farInteractionFeature && this._farInteractionFeature.attached) {
  428. this._farInteractionFeature._setPointerSelectionDisabledByPointerId(controllerData.id, false);
  429. }
  430. }
  431. }
  432. // Update the interaction animation. Only updates if the visible touch mesh is active
  433. let state = ControllerOrbAnimationState.DEHYDRATED;
  434. if (controllerData.grabInteraction || controllerData.nearInteraction) {
  435. state = ControllerOrbAnimationState.TOUCH;
  436. }
  437. else if (controllerData.hoverInteraction) {
  438. state = ControllerOrbAnimationState.HOVER;
  439. }
  440. this._handleTransitionAnimation(controllerData, state);
  441. });
  442. }
  443. get _utilityLayerScene() {
  444. return this._options.customUtilityLayerScene || UtilityLayerRenderer.DefaultUtilityLayer.utilityLayerScene;
  445. }
  446. _generateVisualCue() {
  447. const sceneToRenderTo = this._options.useUtilityLayer ? this._options.customUtilityLayerScene || UtilityLayerRenderer.DefaultUtilityLayer.utilityLayerScene : this._scene;
  448. const selectionMesh = CreateSphere("nearInteraction", {
  449. diameter: 0.0035 * 3 * this._xrSessionManager.worldScalingFactor,
  450. }, sceneToRenderTo);
  451. selectionMesh.bakeCurrentTransformIntoVertices();
  452. selectionMesh.isPickable = false;
  453. selectionMesh.isVisible = false;
  454. selectionMesh.rotationQuaternion = Quaternion.Identity();
  455. const targetMat = new StandardMaterial("targetMat", sceneToRenderTo);
  456. targetMat.specularColor = Color3.Black();
  457. targetMat.emissiveColor = this.selectionMeshDefaultColor;
  458. targetMat.backFaceCulling = false;
  459. selectionMesh.material = targetMat;
  460. return selectionMesh;
  461. }
  462. _isControllerReadyForNearInteraction(id) {
  463. if (this._farInteractionFeature) {
  464. return this._farInteractionFeature._getPointerSelectionDisabledByPointerId(id);
  465. }
  466. return true;
  467. }
  468. _attachNearInteractionMode(xrController) {
  469. const controllerData = this._controllers[xrController.uniqueId];
  470. const pointerEventInit = {
  471. pointerId: controllerData.id,
  472. pointerType: "xr-near",
  473. };
  474. controllerData.onFrameObserver = this._xrSessionManager.onXRFrameObservable.add(() => {
  475. if ((!this._options.enableNearInteractionOnAllControllers && xrController.uniqueId !== this._attachedController) ||
  476. !controllerData.xrController ||
  477. (!controllerData.xrController.inputSource.hand && (!this._options.nearInteractionControllerMode || !controllerData.xrController.inputSource.gamepad))) {
  478. return;
  479. }
  480. if (controllerData.pick) {
  481. controllerData.pick.ray = controllerData.grabRay;
  482. }
  483. if (controllerData.pick && this._isControllerReadyForNearInteraction(controllerData.id)) {
  484. this._scene.simulatePointerMove(controllerData.pick, pointerEventInit);
  485. }
  486. // Near pick pointer event
  487. if (controllerData.nearInteraction && controllerData.pick && controllerData.pick.hit) {
  488. if (!controllerData.nearInteractionTargetMesh) {
  489. this._scene.simulatePointerDown(controllerData.pick, pointerEventInit);
  490. controllerData.nearInteractionTargetMesh = controllerData.meshUnderPointer;
  491. controllerData.downTriggered = true;
  492. }
  493. }
  494. else if (controllerData.nearInteractionTargetMesh && controllerData.stalePick) {
  495. this._scene.simulatePointerUp(controllerData.stalePick, pointerEventInit);
  496. controllerData.downTriggered = false;
  497. controllerData.nearInteractionTargetMesh = null;
  498. }
  499. });
  500. const grabCheck = (pressed) => {
  501. if (this._options.enableNearInteractionOnAllControllers ||
  502. (xrController.uniqueId === this._attachedController && this._isControllerReadyForNearInteraction(controllerData.id))) {
  503. if (controllerData.pick) {
  504. controllerData.pick.ray = controllerData.grabRay;
  505. }
  506. if (pressed && controllerData.pick && controllerData.meshUnderPointer && this._nearGrabPredicate(controllerData.meshUnderPointer)) {
  507. controllerData.grabInteraction = true;
  508. controllerData.pickedPointVisualCue.isVisible = false;
  509. this._scene.simulatePointerDown(controllerData.pick, pointerEventInit);
  510. controllerData.downTriggered = true;
  511. }
  512. else if (!pressed && controllerData.pick && controllerData.grabInteraction) {
  513. this._scene.simulatePointerUp(controllerData.pick, pointerEventInit);
  514. controllerData.downTriggered = false;
  515. controllerData.grabInteraction = false;
  516. controllerData.pickedPointVisualCue.isVisible = true;
  517. }
  518. }
  519. else {
  520. if (pressed && !this._options.enableNearInteractionOnAllControllers && !this._options.disableSwitchOnClick) {
  521. this._attachedController = xrController.uniqueId;
  522. }
  523. }
  524. };
  525. if (xrController.inputSource.gamepad) {
  526. const init = (motionController) => {
  527. controllerData.squeezeComponent = motionController.getComponent("grasp");
  528. if (controllerData.squeezeComponent) {
  529. controllerData.onSqueezeButtonChangedObserver = controllerData.squeezeComponent.onButtonStateChangedObservable.add((component) => {
  530. if (component.changes.pressed) {
  531. const pressed = component.changes.pressed.current;
  532. grabCheck(pressed);
  533. }
  534. });
  535. }
  536. else {
  537. controllerData.selectionComponent = motionController.getMainComponent();
  538. controllerData.onButtonChangedObserver = controllerData.selectionComponent.onButtonStateChangedObservable.add((component) => {
  539. if (component.changes.pressed) {
  540. const pressed = component.changes.pressed.current;
  541. grabCheck(pressed);
  542. }
  543. });
  544. }
  545. };
  546. if (xrController.motionController) {
  547. init(xrController.motionController);
  548. }
  549. else {
  550. xrController.onMotionControllerInitObservable.add(init);
  551. }
  552. }
  553. else {
  554. // use the select and squeeze events
  555. const selectStartListener = (event) => {
  556. if (controllerData.xrController &&
  557. event.inputSource === controllerData.xrController.inputSource &&
  558. controllerData.pick &&
  559. this._isControllerReadyForNearInteraction(controllerData.id) &&
  560. controllerData.meshUnderPointer &&
  561. this._nearGrabPredicate(controllerData.meshUnderPointer)) {
  562. controllerData.grabInteraction = true;
  563. controllerData.pickedPointVisualCue.isVisible = false;
  564. this._scene.simulatePointerDown(controllerData.pick, pointerEventInit);
  565. controllerData.downTriggered = true;
  566. }
  567. };
  568. const selectEndListener = (event) => {
  569. if (controllerData.xrController &&
  570. event.inputSource === controllerData.xrController.inputSource &&
  571. controllerData.pick &&
  572. this._isControllerReadyForNearInteraction(controllerData.id)) {
  573. this._scene.simulatePointerUp(controllerData.pick, pointerEventInit);
  574. controllerData.grabInteraction = false;
  575. controllerData.pickedPointVisualCue.isVisible = true;
  576. controllerData.downTriggered = false;
  577. }
  578. };
  579. controllerData.eventListeners = {
  580. selectend: selectEndListener,
  581. selectstart: selectStartListener,
  582. };
  583. this._xrSessionManager.session.addEventListener("selectstart", selectStartListener);
  584. this._xrSessionManager.session.addEventListener("selectend", selectEndListener);
  585. }
  586. }
  587. _detachController(xrControllerUniqueId) {
  588. const controllerData = this._controllers[xrControllerUniqueId];
  589. if (!controllerData) {
  590. return;
  591. }
  592. if (controllerData.squeezeComponent) {
  593. if (controllerData.onSqueezeButtonChangedObserver) {
  594. controllerData.squeezeComponent.onButtonStateChangedObservable.remove(controllerData.onSqueezeButtonChangedObserver);
  595. }
  596. }
  597. if (controllerData.selectionComponent) {
  598. if (controllerData.onButtonChangedObserver) {
  599. controllerData.selectionComponent.onButtonStateChangedObservable.remove(controllerData.onButtonChangedObserver);
  600. }
  601. }
  602. if (controllerData.onFrameObserver) {
  603. this._xrSessionManager.onXRFrameObservable.remove(controllerData.onFrameObserver);
  604. }
  605. if (controllerData.eventListeners) {
  606. Object.keys(controllerData.eventListeners).forEach((eventName) => {
  607. const func = controllerData.eventListeners && controllerData.eventListeners[eventName];
  608. if (func) {
  609. this._xrSessionManager.session.removeEventListener(eventName, func);
  610. }
  611. });
  612. }
  613. controllerData.touchCollisionMesh.dispose();
  614. controllerData.pickedPointVisualCue.dispose();
  615. this._xrSessionManager.runInXRFrame(() => {
  616. if (!controllerData.downTriggered) {
  617. return;
  618. }
  619. // Fire a pointerup in case controller was detached before a pointerup event was fired
  620. const pointerEventInit = {
  621. pointerId: controllerData.id,
  622. pointerType: "xr-near",
  623. };
  624. this._scene.simulatePointerUp(new PickingInfo(), pointerEventInit);
  625. });
  626. // remove world scale observer
  627. if (controllerData._worldScaleObserver) {
  628. this._xrSessionManager.onWorldScaleFactorChangedObservable.remove(controllerData._worldScaleObserver);
  629. }
  630. // remove from the map
  631. delete this._controllers[xrControllerUniqueId];
  632. if (this._attachedController === xrControllerUniqueId) {
  633. // check for other controllers
  634. const keys = Object.keys(this._controllers);
  635. if (keys.length) {
  636. this._attachedController = keys[0];
  637. }
  638. else {
  639. this._attachedController = "";
  640. }
  641. }
  642. }
  643. _generateNewTouchPointMesh() {
  644. const worldScale = this._xrSessionManager.worldScalingFactor;
  645. // populate information for near hover, pick and pinch
  646. const meshCreationScene = this._options.useUtilityLayer ? this._options.customUtilityLayerScene || UtilityLayerRenderer.DefaultUtilityLayer.utilityLayerScene : this._scene;
  647. const touchCollisionMesh = CreateSphere("PickSphere", { diameter: 1 * worldScale }, meshCreationScene);
  648. touchCollisionMesh.isVisible = false;
  649. // Generate the material for the touch mesh visuals
  650. if (this._options.motionControllerOrbMaterial) {
  651. touchCollisionMesh.material = this._options.motionControllerOrbMaterial;
  652. }
  653. else {
  654. NodeMaterial.ParseFromSnippetAsync("8RUNKL#3", meshCreationScene).then((nodeMaterial) => {
  655. touchCollisionMesh.material = nodeMaterial;
  656. });
  657. }
  658. const easingFunction = new QuadraticEase();
  659. easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);
  660. // Adjust the visual size based off of the size of the touch collision orb.
  661. // Having the size perfectly match for hover gives a more accurate tell for when the user will start interacting with the target
  662. // Sizes for other states are somewhat arbitrary, as they are based on what feels nice during an interaction
  663. const hoverSizeVec = new Vector3(this._controllerPickRadius, this._controllerPickRadius, this._controllerPickRadius).scaleInPlace(worldScale);
  664. const touchSize = this._controllerPickRadius * (4 / 3);
  665. const touchSizeVec = new Vector3(touchSize, touchSize, touchSize).scaleInPlace(worldScale);
  666. const hydrateTransitionSize = this._controllerPickRadius * (7 / 6);
  667. const hydrateTransitionSizeVec = new Vector3(hydrateTransitionSize, hydrateTransitionSize, hydrateTransitionSize).scaleInPlace(worldScale);
  668. const touchHoverTransitionSize = this._controllerPickRadius * (4 / 5);
  669. const touchHoverTransitionSizeVec = new Vector3(touchHoverTransitionSize, touchHoverTransitionSize, touchHoverTransitionSize).scaleInPlace(worldScale);
  670. const hoverTouchTransitionSize = this._controllerPickRadius * (3 / 2);
  671. const hoverTouchTransitionSizeVec = new Vector3(hoverTouchTransitionSize, hoverTouchTransitionSize, hoverTouchTransitionSize).scaleInPlace(worldScale);
  672. const touchKeys = [
  673. { frame: 0, value: hoverSizeVec },
  674. { frame: 10, value: hoverTouchTransitionSizeVec },
  675. { frame: 18, value: touchSizeVec },
  676. ];
  677. const releaseKeys = [
  678. { frame: 0, value: touchSizeVec },
  679. { frame: 10, value: touchHoverTransitionSizeVec },
  680. { frame: 18, value: hoverSizeVec },
  681. ];
  682. const hydrateKeys = [
  683. { frame: 0, value: Vector3.ZeroReadOnly },
  684. { frame: 12, value: hydrateTransitionSizeVec },
  685. { frame: 15, value: hoverSizeVec },
  686. ];
  687. const dehydrateKeys = [
  688. { frame: 0, value: hoverSizeVec },
  689. { frame: 10, value: Vector3.ZeroReadOnly },
  690. { frame: 15, value: Vector3.ZeroReadOnly },
  691. ];
  692. const touchAction = new Animation("touch", "scaling", 60, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CONSTANT);
  693. const releaseAction = new Animation("release", "scaling", 60, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CONSTANT);
  694. const hydrateAction = new Animation("hydrate", "scaling", 60, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CONSTANT);
  695. const dehydrateAction = new Animation("dehydrate", "scaling", 60, Animation.ANIMATIONTYPE_VECTOR3, Animation.ANIMATIONLOOPMODE_CONSTANT);
  696. touchAction.setEasingFunction(easingFunction);
  697. releaseAction.setEasingFunction(easingFunction);
  698. hydrateAction.setEasingFunction(easingFunction);
  699. dehydrateAction.setEasingFunction(easingFunction);
  700. touchAction.setKeys(touchKeys);
  701. releaseAction.setKeys(releaseKeys);
  702. hydrateAction.setKeys(hydrateKeys);
  703. dehydrateAction.setKeys(dehydrateKeys);
  704. const touchCollisionMeshFunction = (isTouch) => {
  705. const action = isTouch ? touchAction : releaseAction;
  706. meshCreationScene.beginDirectAnimation(touchCollisionMesh, [action], 0, 18, false, 1);
  707. };
  708. const hydrateCollisionMeshFunction = (isHydration) => {
  709. const action = isHydration ? hydrateAction : dehydrateAction;
  710. if (isHydration) {
  711. touchCollisionMesh.isVisible = true;
  712. }
  713. meshCreationScene.beginDirectAnimation(touchCollisionMesh, [action], 0, 15, false, 1, () => {
  714. if (!isHydration) {
  715. touchCollisionMesh.isVisible = false;
  716. }
  717. });
  718. };
  719. return { touchCollisionMesh, touchCollisionMeshFunction, hydrateCollisionMeshFunction };
  720. }
  721. _pickWithSphere(controllerData, radius, sceneToUse, predicate) {
  722. const pickingInfo = new PickingInfo();
  723. pickingInfo.distance = +Infinity;
  724. if (controllerData.touchCollisionMesh && controllerData.xrController) {
  725. const position = controllerData.touchCollisionMesh.position;
  726. const sphere = BoundingSphere.CreateFromCenterAndRadius(position, radius);
  727. for (let meshIndex = 0; meshIndex < sceneToUse.meshes.length; meshIndex++) {
  728. const mesh = sceneToUse.meshes[meshIndex];
  729. if (!predicate(mesh) || !this._controllerAvailablePredicate(mesh, controllerData.xrController.uniqueId)) {
  730. continue;
  731. }
  732. const result = WebXRNearInteraction.PickMeshWithSphere(mesh, sphere);
  733. if (result && result.hit && result.distance < pickingInfo.distance) {
  734. pickingInfo.hit = result.hit;
  735. pickingInfo.pickedMesh = mesh;
  736. pickingInfo.pickedPoint = result.pickedPoint;
  737. pickingInfo.aimTransform = controllerData.xrController.pointer;
  738. pickingInfo.gripTransform = controllerData.xrController.grip || null;
  739. pickingInfo.originMesh = controllerData.touchCollisionMesh;
  740. pickingInfo.distance = result.distance;
  741. pickingInfo.bu = result.bu;
  742. pickingInfo.bv = result.bv;
  743. pickingInfo.faceId = result.faceId;
  744. pickingInfo.subMeshId = result.subMeshId;
  745. }
  746. }
  747. }
  748. return pickingInfo;
  749. }
  750. /**
  751. * Picks a mesh with a sphere
  752. * @param mesh the mesh to pick
  753. * @param sphere picking sphere in world coordinates
  754. * @param skipBoundingInfo a boolean indicating if we should skip the bounding info check
  755. * @returns the picking info
  756. */
  757. static PickMeshWithSphere(mesh, sphere, skipBoundingInfo = false) {
  758. const subMeshes = mesh.subMeshes;
  759. const pi = new PickingInfo();
  760. const boundingInfo = mesh.getBoundingInfo();
  761. if (!mesh._generatePointsArray()) {
  762. return pi;
  763. }
  764. if (!mesh.subMeshes || !boundingInfo) {
  765. return pi;
  766. }
  767. if (!skipBoundingInfo && !BoundingSphere.Intersects(boundingInfo.boundingSphere, sphere)) {
  768. return pi;
  769. }
  770. const result = TmpVectors.Vector3[0];
  771. const tmpVec = TmpVectors.Vector3[1];
  772. const tmpRay = new Ray(Vector3.Zero(), Vector3.Zero(), 1);
  773. let distance = +Infinity;
  774. let tmp, tmpDistanceSphereToCenter, tmpDistanceSurfaceToCenter, intersectionInfo;
  775. const center = TmpVectors.Vector3[2];
  776. const worldToMesh = TmpVectors.Matrix[0];
  777. worldToMesh.copyFrom(mesh.getWorldMatrix());
  778. worldToMesh.invert();
  779. Vector3.TransformCoordinatesToRef(sphere.center, worldToMesh, center);
  780. for (let index = 0; index < subMeshes.length; index++) {
  781. const subMesh = subMeshes[index];
  782. subMesh.projectToRef(center, mesh._positions, mesh.getIndices(), tmpVec);
  783. Vector3.TransformCoordinatesToRef(tmpVec, mesh.getWorldMatrix(), tmpVec);
  784. tmp = Vector3.Distance(tmpVec, sphere.center);
  785. // Check for finger inside of mesh
  786. tmpDistanceSurfaceToCenter = Vector3.Distance(tmpVec, mesh.getAbsolutePosition());
  787. tmpDistanceSphereToCenter = Vector3.Distance(sphere.center, mesh.getAbsolutePosition());
  788. if (tmpDistanceSphereToCenter !== -1 && tmpDistanceSurfaceToCenter !== -1 && tmpDistanceSurfaceToCenter > tmpDistanceSphereToCenter) {
  789. tmp = 0;
  790. tmpVec.copyFrom(sphere.center);
  791. }
  792. if (tmp !== -1 && tmp < distance) {
  793. distance = tmp;
  794. // ray between the sphere center and the point on the mesh
  795. Ray.CreateFromToToRef(sphere.center, tmpVec, tmpRay);
  796. tmpRay.length = distance * 2;
  797. intersectionInfo = tmpRay.intersectsMesh(mesh);
  798. result.copyFrom(tmpVec);
  799. }
  800. }
  801. if (distance < sphere.radius) {
  802. pi.hit = true;
  803. pi.distance = distance;
  804. pi.pickedMesh = mesh;
  805. pi.pickedPoint = result.clone();
  806. if (intersectionInfo && intersectionInfo.bu !== null && intersectionInfo.bv !== null) {
  807. pi.faceId = intersectionInfo.faceId;
  808. pi.subMeshId = intersectionInfo.subMeshId;
  809. pi.bu = intersectionInfo.bu;
  810. pi.bv = intersectionInfo.bv;
  811. }
  812. }
  813. return pi;
  814. }
  815. }
  816. WebXRNearInteraction._IdCounter = 200;
  817. /**
  818. * The module's name
  819. */
  820. WebXRNearInteraction.Name = WebXRFeatureName.NEAR_INTERACTION;
  821. /**
  822. * The (Babylon) version of this module.
  823. * This is an integer representing the implementation version.
  824. * This number does not correspond to the WebXR specs version
  825. */
  826. WebXRNearInteraction.Version = 1;
  827. //Register the plugin
  828. WebXRFeaturesManager.AddWebXRFeature(WebXRNearInteraction.Name, (xrSessionManager, options) => {
  829. return () => new WebXRNearInteraction(xrSessionManager, options);
  830. }, WebXRNearInteraction.Version, true);
  831. //# sourceMappingURL=WebXRNearInteraction.js.map