WebXRControllerPointerSelection.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  1. import { WebXRFeaturesManager, WebXRFeatureName } from "../webXRFeaturesManager.js";
  2. import { Matrix, Vector3 } from "../../Maths/math.vector.js";
  3. import { Color3 } from "../../Maths/math.color.js";
  4. import { Axis } from "../../Maths/math.axis.js";
  5. import { StandardMaterial } from "../../Materials/standardMaterial.js";
  6. import { CreateCylinder } from "../../Meshes/Builders/cylinderBuilder.js";
  7. import { CreateTorus } from "../../Meshes/Builders/torusBuilder.js";
  8. import { Ray } from "../../Culling/ray.js";
  9. import { PickingInfo } from "../../Collisions/pickingInfo.js";
  10. import { WebXRAbstractFeature } from "./WebXRAbstractFeature.js";
  11. import { UtilityLayerRenderer } from "../../Rendering/utilityLayerRenderer.js";
  12. import { Viewport } from "../../Maths/math.viewport.js";
  13. import { Tools } from "../../Misc/tools.js";
  14. /**
  15. * A module that will enable pointer selection for motion controllers of XR Input Sources
  16. */
  17. export class WebXRControllerPointerSelection extends WebXRAbstractFeature {
  18. /**
  19. * constructs a new background remover module
  20. * @param _xrSessionManager the session manager for this module
  21. * @param _options read-only options to be used in this module
  22. */
  23. constructor(_xrSessionManager, _options) {
  24. super(_xrSessionManager);
  25. this._options = _options;
  26. this._attachController = (xrController) => {
  27. if (this._controllers[xrController.uniqueId]) {
  28. // already attached
  29. return;
  30. }
  31. const { laserPointer, selectionMesh } = this._generateNewMeshPair(this._options.forceGripIfAvailable && xrController.grip ? xrController.grip : xrController.pointer);
  32. // get two new meshes
  33. this._controllers[xrController.uniqueId] = {
  34. xrController,
  35. laserPointer,
  36. selectionMesh,
  37. meshUnderPointer: null,
  38. pick: null,
  39. tmpRay: new Ray(new Vector3(), new Vector3()),
  40. disabledByNearInteraction: false,
  41. id: WebXRControllerPointerSelection._IdCounter++,
  42. };
  43. if (this._attachedController) {
  44. if (!this._options.enablePointerSelectionOnAllControllers &&
  45. this._options.preferredHandedness &&
  46. xrController.inputSource.handedness === this._options.preferredHandedness) {
  47. this._attachedController = xrController.uniqueId;
  48. }
  49. }
  50. else {
  51. if (!this._options.enablePointerSelectionOnAllControllers) {
  52. this._attachedController = xrController.uniqueId;
  53. }
  54. }
  55. switch (xrController.inputSource.targetRayMode) {
  56. case "tracked-pointer":
  57. return this._attachTrackedPointerRayMode(xrController);
  58. case "gaze":
  59. return this._attachGazeMode(xrController);
  60. case "screen":
  61. case "transient-pointer":
  62. return this._attachScreenRayMode(xrController);
  63. }
  64. };
  65. this._controllers = {};
  66. this._tmpVectorForPickCompare = new Vector3();
  67. /**
  68. * Disable lighting on the laser pointer (so it will always be visible)
  69. */
  70. this.disablePointerLighting = true;
  71. /**
  72. * Disable lighting on the selection mesh (so it will always be visible)
  73. */
  74. this.disableSelectionMeshLighting = true;
  75. /**
  76. * Should the laser pointer be displayed
  77. */
  78. this.displayLaserPointer = true;
  79. /**
  80. * Should the selection mesh be displayed (The ring at the end of the laser pointer)
  81. */
  82. this.displaySelectionMesh = true;
  83. /**
  84. * This color will be set to the laser pointer when selection is triggered
  85. */
  86. this.laserPointerPickedColor = new Color3(0.9, 0.9, 0.9);
  87. /**
  88. * Default color of the laser pointer
  89. */
  90. this.laserPointerDefaultColor = new Color3(0.7, 0.7, 0.7);
  91. /**
  92. * default color of the selection ring
  93. */
  94. this.selectionMeshDefaultColor = new Color3(0.8, 0.8, 0.8);
  95. /**
  96. * This color will be applied to the selection ring when selection is triggered
  97. */
  98. this.selectionMeshPickedColor = new Color3(0.3, 0.3, 1.0);
  99. this._identityMatrix = Matrix.Identity();
  100. this._screenCoordinatesRef = Vector3.Zero();
  101. this._viewportRef = new Viewport(0, 0, 0, 0);
  102. this._scene = this._xrSessionManager.scene;
  103. // force look and pick mode if using WebXR on safari, assuming it is vision OS
  104. // Only if not explicitly set. If set to false, it will not be forced
  105. if (this._options.lookAndPickMode === undefined && (this._scene.getEngine()._badDesktopOS || this._scene.getEngine()._badOS)) {
  106. this._options.lookAndPickMode = true;
  107. }
  108. // look and pick mode extra state changes
  109. if (this._options.lookAndPickMode) {
  110. this._options.enablePointerSelectionOnAllControllers = true;
  111. this.displayLaserPointer = false;
  112. }
  113. }
  114. /**
  115. * attach this feature
  116. * Will usually be called by the features manager
  117. *
  118. * @returns true if successful.
  119. */
  120. attach() {
  121. if (!super.attach()) {
  122. return false;
  123. }
  124. this._options.xrInput.controllers.forEach(this._attachController);
  125. this._addNewAttachObserver(this._options.xrInput.onControllerAddedObservable, this._attachController, true);
  126. this._addNewAttachObserver(this._options.xrInput.onControllerRemovedObservable, (controller) => {
  127. // REMOVE the controller
  128. this._detachController(controller.uniqueId);
  129. }, true);
  130. this._scene.constantlyUpdateMeshUnderPointer = true;
  131. if (this._options.gazeCamera) {
  132. const webXRCamera = this._options.gazeCamera;
  133. const { laserPointer, selectionMesh } = this._generateNewMeshPair(webXRCamera);
  134. this._controllers["camera"] = {
  135. webXRCamera,
  136. laserPointer,
  137. selectionMesh,
  138. meshUnderPointer: null,
  139. pick: null,
  140. tmpRay: new Ray(new Vector3(), new Vector3()),
  141. disabledByNearInteraction: false,
  142. id: WebXRControllerPointerSelection._IdCounter++,
  143. };
  144. this._attachGazeMode();
  145. }
  146. return true;
  147. }
  148. /**
  149. * detach this feature.
  150. * Will usually be called by the features manager
  151. *
  152. * @returns true if successful.
  153. */
  154. detach() {
  155. if (!super.detach()) {
  156. return false;
  157. }
  158. Object.keys(this._controllers).forEach((controllerId) => {
  159. this._detachController(controllerId);
  160. });
  161. return true;
  162. }
  163. /**
  164. * Will get the mesh under a specific pointer.
  165. * `scene.meshUnderPointer` will only return one mesh - either left or right.
  166. * @param controllerId the controllerId to check
  167. * @returns The mesh under pointer or null if no mesh is under the pointer
  168. */
  169. getMeshUnderPointer(controllerId) {
  170. if (this._controllers[controllerId]) {
  171. return this._controllers[controllerId].meshUnderPointer;
  172. }
  173. else {
  174. return null;
  175. }
  176. }
  177. /**
  178. * Get the xr controller that correlates to the pointer id in the pointer event
  179. *
  180. * @param id the pointer id to search for
  181. * @returns the controller that correlates to this id or null if not found
  182. */
  183. getXRControllerByPointerId(id) {
  184. const keys = Object.keys(this._controllers);
  185. for (let i = 0; i < keys.length; ++i) {
  186. if (this._controllers[keys[i]].id === id) {
  187. return this._controllers[keys[i]].xrController || null;
  188. }
  189. }
  190. return null;
  191. }
  192. /**
  193. * @internal
  194. */
  195. _getPointerSelectionDisabledByPointerId(id) {
  196. const keys = Object.keys(this._controllers);
  197. for (let i = 0; i < keys.length; ++i) {
  198. if (this._controllers[keys[i]].id === id) {
  199. return this._controllers[keys[i]].disabledByNearInteraction;
  200. }
  201. }
  202. return true;
  203. }
  204. /**
  205. * @internal
  206. */
  207. _setPointerSelectionDisabledByPointerId(id, state) {
  208. const keys = Object.keys(this._controllers);
  209. for (let i = 0; i < keys.length; ++i) {
  210. if (this._controllers[keys[i]].id === id) {
  211. this._controllers[keys[i]].disabledByNearInteraction = state;
  212. return;
  213. }
  214. }
  215. }
  216. _onXRFrame(_xrFrame) {
  217. Object.keys(this._controllers).forEach((id) => {
  218. // look and pick mode
  219. // only do this for the selected pointer
  220. const controllerData = this._controllers[id];
  221. if (this._options.lookAndPickMode && controllerData.xrController?.inputSource.targetRayMode !== "transient-pointer") {
  222. return;
  223. }
  224. if ((!this._options.enablePointerSelectionOnAllControllers && id !== this._attachedController) || controllerData.disabledByNearInteraction) {
  225. controllerData.selectionMesh.isVisible = false;
  226. controllerData.laserPointer.isVisible = false;
  227. controllerData.pick = null;
  228. return;
  229. }
  230. controllerData.laserPointer.isVisible = this.displayLaserPointer;
  231. let controllerGlobalPosition;
  232. // Every frame check collisions/input
  233. if (controllerData.xrController) {
  234. controllerGlobalPosition =
  235. this._options.forceGripIfAvailable && controllerData.xrController.grip
  236. ? controllerData.xrController.grip.position
  237. : controllerData.xrController.pointer.position;
  238. controllerData.xrController.getWorldPointerRayToRef(controllerData.tmpRay, this._options.forceGripIfAvailable);
  239. }
  240. else if (controllerData.webXRCamera) {
  241. controllerGlobalPosition = controllerData.webXRCamera.position;
  242. controllerData.webXRCamera.getForwardRayToRef(controllerData.tmpRay);
  243. }
  244. else {
  245. return;
  246. }
  247. if (this._options.maxPointerDistance) {
  248. controllerData.tmpRay.length = this._options.maxPointerDistance;
  249. }
  250. // update pointerX and pointerY of the scene. Only if the flag is set to true!
  251. if (!this._options.disableScenePointerVectorUpdate && controllerGlobalPosition) {
  252. const scene = this._xrSessionManager.scene;
  253. const camera = this._options.xrInput.xrCamera;
  254. if (camera) {
  255. camera.viewport.toGlobalToRef(scene.getEngine().getRenderWidth() / camera.rigCameras.length, scene.getEngine().getRenderHeight(), this._viewportRef);
  256. Vector3.ProjectToRef(controllerGlobalPosition, this._identityMatrix, camera.getTransformationMatrix(), this._viewportRef, this._screenCoordinatesRef);
  257. // stay safe
  258. if (typeof this._screenCoordinatesRef.x === "number" &&
  259. typeof this._screenCoordinatesRef.y === "number" &&
  260. !isNaN(this._screenCoordinatesRef.x) &&
  261. !isNaN(this._screenCoordinatesRef.y) &&
  262. this._screenCoordinatesRef.x !== Infinity &&
  263. this._screenCoordinatesRef.y !== Infinity) {
  264. scene.pointerX = this._screenCoordinatesRef.x;
  265. scene.pointerY = this._screenCoordinatesRef.y;
  266. controllerData.screenCoordinates = {
  267. x: this._screenCoordinatesRef.x,
  268. y: this._screenCoordinatesRef.y,
  269. };
  270. }
  271. }
  272. }
  273. let utilityScenePick = null;
  274. if (this._utilityLayerScene) {
  275. utilityScenePick = this._utilityLayerScene.pickWithRay(controllerData.tmpRay, this._utilityLayerScene.pointerMovePredicate || this.raySelectionPredicate);
  276. }
  277. const originalScenePick = this._scene.pickWithRay(controllerData.tmpRay, this._scene.pointerMovePredicate || this.raySelectionPredicate);
  278. if (!utilityScenePick || !utilityScenePick.hit) {
  279. // No hit in utility scene
  280. controllerData.pick = originalScenePick;
  281. }
  282. else if (!originalScenePick || !originalScenePick.hit) {
  283. // No hit in original scene
  284. controllerData.pick = utilityScenePick;
  285. }
  286. else if (utilityScenePick.distance < originalScenePick.distance) {
  287. // Hit is closer in utility scene
  288. controllerData.pick = utilityScenePick;
  289. }
  290. else {
  291. // Hit is closer in original scene
  292. controllerData.pick = originalScenePick;
  293. }
  294. if (controllerData.pick && controllerData.xrController) {
  295. controllerData.pick.aimTransform = controllerData.xrController.pointer;
  296. controllerData.pick.gripTransform = controllerData.xrController.grip || null;
  297. controllerData.pick.originMesh = controllerData.xrController.pointer;
  298. }
  299. const pick = controllerData.pick;
  300. if (pick && pick.pickedPoint && pick.hit) {
  301. // Update laser state
  302. this._updatePointerDistance(controllerData.laserPointer, pick.distance);
  303. // Update cursor state
  304. controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
  305. controllerData.selectionMesh.scaling.x = Math.sqrt(pick.distance);
  306. controllerData.selectionMesh.scaling.y = Math.sqrt(pick.distance);
  307. controllerData.selectionMesh.scaling.z = Math.sqrt(pick.distance);
  308. // To avoid z-fighting
  309. const pickNormal = this._convertNormalToDirectionOfRay(pick.getNormal(true), controllerData.tmpRay);
  310. const deltaFighting = 0.001;
  311. controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
  312. if (pickNormal) {
  313. const axis1 = Vector3.Cross(Axis.Y, pickNormal);
  314. const axis2 = Vector3.Cross(pickNormal, axis1);
  315. Vector3.RotationFromAxisToRef(axis2, pickNormal, axis1, controllerData.selectionMesh.rotation);
  316. controllerData.selectionMesh.position.addInPlace(pickNormal.scale(deltaFighting));
  317. }
  318. controllerData.selectionMesh.isVisible = true && this.displaySelectionMesh;
  319. controllerData.meshUnderPointer = pick.pickedMesh;
  320. }
  321. else {
  322. controllerData.selectionMesh.isVisible = false;
  323. this._updatePointerDistance(controllerData.laserPointer, 1);
  324. controllerData.meshUnderPointer = null;
  325. }
  326. });
  327. }
  328. get _utilityLayerScene() {
  329. return this._options.customUtilityLayerScene || UtilityLayerRenderer.DefaultUtilityLayer.utilityLayerScene;
  330. }
  331. _attachGazeMode(xrController) {
  332. const controllerData = this._controllers[(xrController && xrController.uniqueId) || "camera"];
  333. // attached when touched, detaches when raised
  334. const timeToSelect = this._options.timeToSelect || 3000;
  335. const sceneToRenderTo = this._options.useUtilityLayer ? this._utilityLayerScene : this._scene;
  336. let oldPick = new PickingInfo();
  337. const discMesh = CreateTorus("selection", {
  338. diameter: 0.0035 * 15,
  339. thickness: 0.0025 * 6,
  340. tessellation: 20,
  341. }, sceneToRenderTo);
  342. discMesh.isVisible = false;
  343. discMesh.isPickable = false;
  344. discMesh.parent = controllerData.selectionMesh;
  345. let timer = 0;
  346. let downTriggered = false;
  347. const pointerEventInit = {
  348. pointerId: controllerData.id,
  349. pointerType: "xr",
  350. };
  351. controllerData.onFrameObserver = this._xrSessionManager.onXRFrameObservable.add(() => {
  352. if (!controllerData.pick) {
  353. return;
  354. }
  355. this._augmentPointerInit(pointerEventInit, controllerData.id, controllerData.screenCoordinates);
  356. controllerData.laserPointer.material.alpha = 0;
  357. discMesh.isVisible = false;
  358. if (controllerData.pick.hit) {
  359. if (!this._pickingMoved(oldPick, controllerData.pick)) {
  360. if (timer > timeToSelect / 10) {
  361. discMesh.isVisible = true;
  362. }
  363. timer += this._scene.getEngine().getDeltaTime();
  364. if (timer >= timeToSelect) {
  365. this._scene.simulatePointerDown(controllerData.pick, pointerEventInit);
  366. // this pointerdown event is not setting the controllerData.pointerDownTriggered to avoid a pointerUp event when this feature is detached
  367. downTriggered = true;
  368. // pointer up right after down, if disable on touch out
  369. if (this._options.disablePointerUpOnTouchOut) {
  370. this._scene.simulatePointerUp(controllerData.pick, pointerEventInit);
  371. }
  372. discMesh.isVisible = false;
  373. }
  374. else {
  375. const scaleFactor = 1 - timer / timeToSelect;
  376. discMesh.scaling.set(scaleFactor, scaleFactor, scaleFactor);
  377. }
  378. }
  379. else {
  380. if (downTriggered) {
  381. if (!this._options.disablePointerUpOnTouchOut) {
  382. this._scene.simulatePointerUp(controllerData.pick, pointerEventInit);
  383. }
  384. }
  385. downTriggered = false;
  386. timer = 0;
  387. }
  388. }
  389. else {
  390. downTriggered = false;
  391. timer = 0;
  392. }
  393. this._scene.simulatePointerMove(controllerData.pick, pointerEventInit);
  394. oldPick = controllerData.pick;
  395. });
  396. if (this._options.renderingGroupId !== undefined) {
  397. discMesh.renderingGroupId = this._options.renderingGroupId;
  398. }
  399. if (xrController) {
  400. xrController.onDisposeObservable.addOnce(() => {
  401. if (controllerData.pick && !this._options.disablePointerUpOnTouchOut && downTriggered) {
  402. this._scene.simulatePointerUp(controllerData.pick, pointerEventInit);
  403. controllerData.finalPointerUpTriggered = true;
  404. }
  405. discMesh.dispose();
  406. });
  407. }
  408. }
  409. _attachScreenRayMode(xrController) {
  410. const controllerData = this._controllers[xrController.uniqueId];
  411. let downTriggered = false;
  412. const pointerEventInit = {
  413. pointerId: controllerData.id,
  414. pointerType: "xr",
  415. };
  416. controllerData.onFrameObserver = this._xrSessionManager.onXRFrameObservable.add(() => {
  417. this._augmentPointerInit(pointerEventInit, controllerData.id, controllerData.screenCoordinates);
  418. if (!controllerData.pick || (this._options.disablePointerUpOnTouchOut && downTriggered)) {
  419. return;
  420. }
  421. if (!downTriggered) {
  422. this._scene.simulatePointerDown(controllerData.pick, pointerEventInit);
  423. controllerData.pointerDownTriggered = true;
  424. downTriggered = true;
  425. if (this._options.disablePointerUpOnTouchOut) {
  426. this._scene.simulatePointerUp(controllerData.pick, pointerEventInit);
  427. }
  428. }
  429. else {
  430. this._scene.simulatePointerMove(controllerData.pick, pointerEventInit);
  431. }
  432. });
  433. xrController.onDisposeObservable.addOnce(() => {
  434. this._augmentPointerInit(pointerEventInit, controllerData.id, controllerData.screenCoordinates);
  435. this._xrSessionManager.runInXRFrame(() => {
  436. if (controllerData.pick && !controllerData.finalPointerUpTriggered && downTriggered && !this._options.disablePointerUpOnTouchOut) {
  437. this._scene.simulatePointerUp(controllerData.pick, pointerEventInit);
  438. controllerData.finalPointerUpTriggered = true;
  439. }
  440. });
  441. });
  442. }
  443. _attachTrackedPointerRayMode(xrController) {
  444. const controllerData = this._controllers[xrController.uniqueId];
  445. if (this._options.forceGazeMode) {
  446. return this._attachGazeMode(xrController);
  447. }
  448. const pointerEventInit = {
  449. pointerId: controllerData.id,
  450. pointerType: "xr",
  451. };
  452. controllerData.onFrameObserver = this._xrSessionManager.onXRFrameObservable.add(() => {
  453. controllerData.laserPointer.material.disableLighting = this.disablePointerLighting;
  454. controllerData.selectionMesh.material.disableLighting = this.disableSelectionMeshLighting;
  455. if (controllerData.pick) {
  456. this._augmentPointerInit(pointerEventInit, controllerData.id, controllerData.screenCoordinates);
  457. this._scene.simulatePointerMove(controllerData.pick, pointerEventInit);
  458. }
  459. });
  460. if (xrController.inputSource.gamepad) {
  461. const init = (motionController) => {
  462. if (this._options.overrideButtonId) {
  463. controllerData.selectionComponent = motionController.getComponent(this._options.overrideButtonId);
  464. }
  465. if (!controllerData.selectionComponent) {
  466. controllerData.selectionComponent = motionController.getMainComponent();
  467. }
  468. controllerData.onButtonChangedObserver = controllerData.selectionComponent.onButtonStateChangedObservable.add((component) => {
  469. if (component.changes.pressed) {
  470. const pressed = component.changes.pressed.current;
  471. if (controllerData.pick) {
  472. if (this._options.enablePointerSelectionOnAllControllers || xrController.uniqueId === this._attachedController) {
  473. this._augmentPointerInit(pointerEventInit, controllerData.id, controllerData.screenCoordinates);
  474. if (pressed) {
  475. this._scene.simulatePointerDown(controllerData.pick, pointerEventInit);
  476. controllerData.pointerDownTriggered = true;
  477. controllerData.selectionMesh.material.emissiveColor = this.selectionMeshPickedColor;
  478. controllerData.laserPointer.material.emissiveColor = this.laserPointerPickedColor;
  479. }
  480. else {
  481. this._scene.simulatePointerUp(controllerData.pick, pointerEventInit);
  482. controllerData.selectionMesh.material.emissiveColor = this.selectionMeshDefaultColor;
  483. controllerData.laserPointer.material.emissiveColor = this.laserPointerDefaultColor;
  484. }
  485. }
  486. }
  487. else {
  488. if (pressed && !this._options.enablePointerSelectionOnAllControllers && !this._options.disableSwitchOnClick) {
  489. // force a pointer up if switching controllers
  490. // get the controller that was attached before
  491. const prevController = this._controllers[this._attachedController];
  492. if (prevController && prevController.pointerDownTriggered && !prevController.finalPointerUpTriggered) {
  493. this._augmentPointerInit(pointerEventInit, prevController.id, prevController.screenCoordinates);
  494. this._scene.simulatePointerUp(new PickingInfo(), {
  495. pointerId: prevController.id,
  496. pointerType: "xr",
  497. });
  498. prevController.finalPointerUpTriggered = true;
  499. }
  500. this._attachedController = xrController.uniqueId;
  501. }
  502. }
  503. }
  504. });
  505. };
  506. if (xrController.motionController) {
  507. init(xrController.motionController);
  508. }
  509. else {
  510. xrController.onMotionControllerInitObservable.add(init);
  511. }
  512. }
  513. else {
  514. // use the select and squeeze events
  515. const selectStartListener = (event) => {
  516. this._xrSessionManager.onXRFrameObservable.addOnce(() => {
  517. this._augmentPointerInit(pointerEventInit, controllerData.id, controllerData.screenCoordinates);
  518. if (controllerData.xrController && event.inputSource === controllerData.xrController.inputSource && controllerData.pick) {
  519. this._scene.simulatePointerDown(controllerData.pick, pointerEventInit);
  520. controllerData.pointerDownTriggered = true;
  521. controllerData.selectionMesh.material.emissiveColor = this.selectionMeshPickedColor;
  522. controllerData.laserPointer.material.emissiveColor = this.laserPointerPickedColor;
  523. }
  524. });
  525. };
  526. const selectEndListener = (event) => {
  527. this._xrSessionManager.onXRFrameObservable.addOnce(() => {
  528. this._augmentPointerInit(pointerEventInit, controllerData.id, controllerData.screenCoordinates);
  529. if (controllerData.xrController && event.inputSource === controllerData.xrController.inputSource && controllerData.pick) {
  530. this._scene.simulatePointerUp(controllerData.pick, pointerEventInit);
  531. controllerData.selectionMesh.material.emissiveColor = this.selectionMeshDefaultColor;
  532. controllerData.laserPointer.material.emissiveColor = this.laserPointerDefaultColor;
  533. }
  534. });
  535. };
  536. controllerData.eventListeners = {
  537. selectend: selectEndListener,
  538. selectstart: selectStartListener,
  539. };
  540. this._xrSessionManager.session.addEventListener("selectstart", selectStartListener);
  541. this._xrSessionManager.session.addEventListener("selectend", selectEndListener);
  542. }
  543. }
  544. _convertNormalToDirectionOfRay(normal, ray) {
  545. if (normal) {
  546. const angle = Math.acos(Vector3.Dot(normal, ray.direction));
  547. if (angle < Math.PI / 2) {
  548. normal.scaleInPlace(-1);
  549. }
  550. }
  551. return normal;
  552. }
  553. _detachController(xrControllerUniqueId) {
  554. const controllerData = this._controllers[xrControllerUniqueId];
  555. if (!controllerData) {
  556. return;
  557. }
  558. if (controllerData.selectionComponent) {
  559. if (controllerData.onButtonChangedObserver) {
  560. controllerData.selectionComponent.onButtonStateChangedObservable.remove(controllerData.onButtonChangedObserver);
  561. }
  562. }
  563. if (controllerData.onFrameObserver) {
  564. this._xrSessionManager.onXRFrameObservable.remove(controllerData.onFrameObserver);
  565. }
  566. if (controllerData.eventListeners) {
  567. Object.keys(controllerData.eventListeners).forEach((eventName) => {
  568. const func = controllerData.eventListeners && controllerData.eventListeners[eventName];
  569. if (func) {
  570. // For future reference - this is an issue in the WebXR typings.
  571. this._xrSessionManager.session.removeEventListener(eventName, func);
  572. }
  573. });
  574. }
  575. if (!controllerData.finalPointerUpTriggered && controllerData.pointerDownTriggered) {
  576. // Stay safe and fire a pointerup, in case it wasn't already triggered
  577. const pointerEventInit = {
  578. pointerId: controllerData.id,
  579. pointerType: "xr",
  580. };
  581. this._xrSessionManager.runInXRFrame(() => {
  582. this._augmentPointerInit(pointerEventInit, controllerData.id, controllerData.screenCoordinates);
  583. this._scene.simulatePointerUp(controllerData.pick || new PickingInfo(), pointerEventInit);
  584. controllerData.finalPointerUpTriggered = true;
  585. });
  586. }
  587. this._xrSessionManager.scene.onBeforeRenderObservable.addOnce(() => {
  588. try {
  589. controllerData.selectionMesh.dispose();
  590. controllerData.laserPointer.dispose();
  591. // remove from the map
  592. delete this._controllers[xrControllerUniqueId];
  593. if (this._attachedController === xrControllerUniqueId) {
  594. // check for other controllers
  595. const keys = Object.keys(this._controllers);
  596. if (keys.length) {
  597. this._attachedController = keys[0];
  598. }
  599. else {
  600. this._attachedController = "";
  601. }
  602. }
  603. }
  604. catch (e) {
  605. Tools.Warn("controller already detached.");
  606. }
  607. });
  608. }
  609. _generateNewMeshPair(meshParent) {
  610. const sceneToRenderTo = this._options.useUtilityLayer ? this._options.customUtilityLayerScene || UtilityLayerRenderer.DefaultUtilityLayer.utilityLayerScene : this._scene;
  611. const laserPointer = this._options.customLasterPointerMeshGenerator
  612. ? this._options.customLasterPointerMeshGenerator()
  613. : CreateCylinder("laserPointer", {
  614. height: 1,
  615. diameterTop: 0.0002,
  616. diameterBottom: 0.004,
  617. tessellation: 20,
  618. subdivisions: 1,
  619. }, sceneToRenderTo);
  620. laserPointer.parent = meshParent;
  621. const laserPointerMaterial = new StandardMaterial("laserPointerMat", sceneToRenderTo);
  622. laserPointerMaterial.emissiveColor = this.laserPointerDefaultColor;
  623. laserPointerMaterial.alpha = 0.7;
  624. laserPointer.material = laserPointerMaterial;
  625. laserPointer.rotation.x = Math.PI / 2;
  626. this._updatePointerDistance(laserPointer, 1);
  627. laserPointer.isPickable = false;
  628. laserPointer.isVisible = false;
  629. // Create a gaze tracker for the XR controller
  630. const selectionMesh = this._options.customSelectionMeshGenerator
  631. ? this._options.customSelectionMeshGenerator()
  632. : CreateTorus("gazeTracker", {
  633. diameter: 0.0035 * 3,
  634. thickness: 0.0025 * 3,
  635. tessellation: 20,
  636. }, sceneToRenderTo);
  637. selectionMesh.bakeCurrentTransformIntoVertices();
  638. selectionMesh.isPickable = false;
  639. selectionMesh.isVisible = false;
  640. const targetMat = new StandardMaterial("targetMat", sceneToRenderTo);
  641. targetMat.specularColor = Color3.Black();
  642. targetMat.emissiveColor = this.selectionMeshDefaultColor;
  643. targetMat.backFaceCulling = false;
  644. selectionMesh.material = targetMat;
  645. if (this._options.renderingGroupId !== undefined) {
  646. laserPointer.renderingGroupId = this._options.renderingGroupId;
  647. selectionMesh.renderingGroupId = this._options.renderingGroupId;
  648. }
  649. return {
  650. laserPointer,
  651. selectionMesh,
  652. };
  653. }
  654. _pickingMoved(oldPick, newPick) {
  655. if (!oldPick.hit || !newPick.hit) {
  656. return true;
  657. }
  658. if (!oldPick.pickedMesh || !oldPick.pickedPoint || !newPick.pickedMesh || !newPick.pickedPoint) {
  659. return true;
  660. }
  661. if (oldPick.pickedMesh !== newPick.pickedMesh) {
  662. return true;
  663. }
  664. oldPick.pickedPoint?.subtractToRef(newPick.pickedPoint, this._tmpVectorForPickCompare);
  665. this._tmpVectorForPickCompare.set(Math.abs(this._tmpVectorForPickCompare.x), Math.abs(this._tmpVectorForPickCompare.y), Math.abs(this._tmpVectorForPickCompare.z));
  666. const delta = (this._options.gazeModePointerMovedFactor || 1) * 0.01 * newPick.distance;
  667. const length = this._tmpVectorForPickCompare.length();
  668. if (length > delta) {
  669. return true;
  670. }
  671. return false;
  672. }
  673. _updatePointerDistance(_laserPointer, distance = 100) {
  674. _laserPointer.scaling.y = distance;
  675. // a bit of distance from the controller
  676. if (this._scene.useRightHandedSystem) {
  677. distance *= -1;
  678. }
  679. _laserPointer.position.z = distance / 2 + 0.05;
  680. }
  681. _augmentPointerInit(pointerEventInit, id, screenCoordinates) {
  682. pointerEventInit.pointerId = id;
  683. pointerEventInit.pointerType = "xr";
  684. if (screenCoordinates) {
  685. pointerEventInit.screenX = screenCoordinates.x;
  686. pointerEventInit.screenY = screenCoordinates.y;
  687. }
  688. }
  689. /** @internal */
  690. get lasterPointerDefaultColor() {
  691. // here due to a typo
  692. return this.laserPointerDefaultColor;
  693. }
  694. }
  695. WebXRControllerPointerSelection._IdCounter = 200;
  696. /**
  697. * The module's name
  698. */
  699. WebXRControllerPointerSelection.Name = WebXRFeatureName.POINTER_SELECTION;
  700. /**
  701. * The (Babylon) version of this module.
  702. * This is an integer representing the implementation version.
  703. * This number does not correspond to the WebXR specs version
  704. */
  705. WebXRControllerPointerSelection.Version = 1;
  706. //register the plugin
  707. WebXRFeaturesManager.AddWebXRFeature(WebXRControllerPointerSelection.Name, (xrSessionManager, options) => {
  708. return () => new WebXRControllerPointerSelection(xrSessionManager, options);
  709. }, WebXRControllerPointerSelection.Version, true);
  710. //# sourceMappingURL=WebXRControllerPointerSelection.js.map