WebXRControllerPointerSelection.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439
  1. import { WebXRFeaturesManager, IWebXRFeature } from "../webXRFeaturesManager";
  2. import { WebXRSessionManager } from '../webXRSessionManager';
  3. import { AbstractMesh } from '../../../Meshes/abstractMesh';
  4. import { Observer } from '../../../Misc/observable';
  5. import { WebXRInput } from '../webXRInput';
  6. import { WebXRController } from '../webXRController';
  7. import { Scene } from '../../../scene';
  8. import { WebXRControllerComponent } from '../motionController/webXRControllerComponent';
  9. import { Nullable } from '../../../types';
  10. import { Vector3 } from '../../../Maths/math.vector';
  11. import { Color3 } from '../../../Maths/math.color';
  12. import { Axis } from '../../../Maths/math.axis';
  13. import { StandardMaterial } from '../../../Materials/standardMaterial';
  14. import { CylinderBuilder } from '../../../Meshes/Builders/cylinderBuilder';
  15. import { TorusBuilder } from '../../../Meshes/Builders/torusBuilder';
  16. import { Ray } from '../../../Culling/ray';
  17. import { PickingInfo } from '../../../Collisions/pickingInfo';
  18. const Name = "xr-controller-pointer-selection";
  19. /**
  20. * Options interface for the pointer selection module
  21. */
  22. export interface IWebXRControllerPointerSelectionOptions {
  23. /**
  24. * the xr input to use with this pointer selection
  25. */
  26. xrInput: WebXRInput;
  27. /**
  28. * Different button type to use instead of the main component
  29. */
  30. overrideButtonId?: string;
  31. /**
  32. * The amount of time in miliseconds it takes between pick found something to a pointer down event.
  33. * Used in gaze modes. Tracked pointer uses the trigger, screen uses touch events
  34. * 3000 means 3 seconds between pointing at something and selecting it
  35. */
  36. timeToSelect?: number;
  37. /**
  38. * Disable the pointer up event when the xr controller in screen and gaze mode is disposed (meaning - when the user removed the finger from the screen)
  39. * If not disabled, the last picked point will be used to execute a pointer up event
  40. * If disabled, pointer up event will be triggered right after the pointer down event.
  41. * Used in screen and gaze target ray mode only
  42. */
  43. disablePointerUpOnTouchOut: boolean;
  44. }
  45. /**
  46. * A module that will enable pointer selection for motion controllers of XR Input Sources
  47. */
  48. export class WebXRControllerPointerSelection implements IWebXRFeature {
  49. /**
  50. * The module's name
  51. */
  52. public static readonly Name = Name;
  53. /**
  54. * The (Babylon) version of this module.
  55. * This is an integer representing the implementation version.
  56. * This number does not correspond to the webxr specs version
  57. */
  58. public static readonly Version = 1;
  59. /**
  60. * This color will be set to the laser pointer when selection is triggered
  61. */
  62. public laserPointerPickedColor: Color3 = new Color3(0.7, 0.7, 0.7);
  63. /**
  64. * This color will be applied to the selection ring when selection is triggered
  65. */
  66. public selectionMeshPickedColor: Color3 = new Color3(0.7, 0.7, 0.7);
  67. /**
  68. * default color of the selection ring
  69. */
  70. public selectionMeshDefaultColor: Color3 = new Color3(0.5, 0.5, 0.5);
  71. /**
  72. * Default color of the laser pointer
  73. */
  74. public lasterPointerDefaultColor: Color3 = new Color3(0.5, 0.5, 0.5);
  75. private static _idCounter = 0;
  76. private _observerTracked: Nullable<Observer<XRFrame>>;
  77. private _attached: boolean = false;
  78. private _tmpRay = new Ray(new Vector3(), new Vector3());
  79. private _controllers: {
  80. [controllerUniqueId: string]: {
  81. xrController: WebXRController;
  82. selectionComponent?: WebXRControllerComponent;
  83. onButtonChangedObserver?: Nullable<Observer<WebXRControllerComponent>>;
  84. onFrameObserver?: Nullable<Observer<XRFrame>>;
  85. laserPointer: AbstractMesh;
  86. selectionMesh: AbstractMesh;
  87. pick: Nullable<PickingInfo>;
  88. id: number;
  89. };
  90. } = {};
  91. /**
  92. * Is this feature attached
  93. */
  94. public get attached() {
  95. return this._attached;
  96. }
  97. private _scene: Scene;
  98. /**
  99. * constructs a new background remover module
  100. * @param _xrSessionManager the session manager for this module
  101. * @param _options read-only options to be used in this module
  102. */
  103. constructor(private _xrSessionManager: WebXRSessionManager, private readonly _options: IWebXRControllerPointerSelectionOptions) {
  104. this._scene = this._xrSessionManager.scene;
  105. }
  106. /**
  107. * attach this feature
  108. * Will usually be called by the features manager
  109. *
  110. * @returns true if successful.
  111. */
  112. attach(): boolean {
  113. this._options.xrInput.controllers.forEach(this._attachController);
  114. this._options.xrInput.onControllerAddedObservable.add(this._attachController);
  115. this._options.xrInput.onControllerRemovedObservable.add((controller) => {
  116. // REMOVE the controller
  117. this._detachController(controller.uniqueId);
  118. });
  119. this._observerTracked = this._xrSessionManager.onXRFrameObservable.add(() => {
  120. Object.keys(this._controllers).forEach((id) => {
  121. const controllerData = this._controllers[id];
  122. // Every frame check collisions/input
  123. controllerData.xrController.getWorldPointerRayToRef(this._tmpRay);
  124. controllerData.pick = this._scene.pickWithRay(this._tmpRay);
  125. const pick = controllerData.pick;
  126. if (pick && pick.pickedPoint && pick.hit) {
  127. // Update laser state
  128. this._updatePointerDistance(controllerData.laserPointer, pick.distance);
  129. // Update cursor state
  130. controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
  131. controllerData.selectionMesh.scaling.x = Math.sqrt(pick.distance);
  132. controllerData.selectionMesh.scaling.y = Math.sqrt(pick.distance);
  133. controllerData.selectionMesh.scaling.z = Math.sqrt(pick.distance);
  134. // To avoid z-fighting
  135. let pickNormal = this._convertNormalToDirectionOfRay(pick.getNormal(true), this._tmpRay);
  136. let deltaFighting = 0.001;
  137. controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
  138. if (pickNormal) {
  139. let axis1 = Vector3.Cross(Axis.Y, pickNormal);
  140. let axis2 = Vector3.Cross(pickNormal, axis1);
  141. Vector3.RotationFromAxisToRef(axis2, pickNormal, axis1, controllerData.selectionMesh.rotation);
  142. controllerData.selectionMesh.position.addInPlace(pickNormal.scale(deltaFighting));
  143. }
  144. controllerData.selectionMesh.isVisible = true;
  145. } else {
  146. controllerData.selectionMesh.isVisible = false;
  147. }
  148. });
  149. });
  150. this._attached = true;
  151. return true;
  152. }
  153. /**
  154. * detach this feature.
  155. * Will usually be called by the features manager
  156. *
  157. * @returns true if successful.
  158. */
  159. detach(): boolean {
  160. if (this._observerTracked) {
  161. this._xrSessionManager.onXRFrameObservable.remove(this._observerTracked);
  162. }
  163. Object.keys(this._controllers).forEach((controllerId) => {
  164. this._detachController(controllerId);
  165. });
  166. this._attached = false;
  167. return true;
  168. }
  169. /**
  170. * Get the xr controller that correlates to the pointer id in the pointer event
  171. *
  172. * @param id the pointer id to search for
  173. */
  174. public getXRControllerByPointerId(id: number): Nullable<WebXRController> {
  175. const keys = Object.keys(this._controllers);
  176. for (let i = 0; i < keys.length; ++i) {
  177. if (this._controllers[keys[i]].id === id) {
  178. return this._controllers[keys[i]].xrController;
  179. }
  180. }
  181. return null;
  182. }
  183. private _attachController = (xrController: WebXRController) => {
  184. if (this._controllers[xrController.uniqueId]) {
  185. // already attached
  186. return;
  187. }
  188. // only support tracker pointer
  189. const { laserPointer, selectionMesh } = this._generateNewMeshPair(xrController);
  190. // get two new meshes
  191. this._controllers[xrController.uniqueId] = {
  192. xrController,
  193. laserPointer,
  194. selectionMesh,
  195. pick: null,
  196. id: WebXRControllerPointerSelection._idCounter++
  197. };
  198. switch (xrController.inputSource.targetRayMode) {
  199. case "tracked-pointer":
  200. return this._attachTrackedPointerRayMode(xrController);
  201. case "gaze":
  202. return this._attachGazeMode(xrController);
  203. case "screen":
  204. return this._attachScreenRayMode(xrController);
  205. }
  206. }
  207. private _attachScreenRayMode(xrController: WebXRController) {
  208. const controllerData = this._controllers[xrController.uniqueId];
  209. let downTriggered = false;
  210. controllerData.onFrameObserver = this._xrSessionManager.onXRFrameObservable.add(() => {
  211. if (!controllerData.pick || (this._options.disablePointerUpOnTouchOut && downTriggered)) { return; }
  212. if (!downTriggered) {
  213. this._scene.simulatePointerDown(controllerData.pick, { pointerId: controllerData.id });
  214. downTriggered = true;
  215. if (this._options.disablePointerUpOnTouchOut) {
  216. this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
  217. }
  218. } else {
  219. this._scene.simulatePointerMove(controllerData.pick, { pointerId: controllerData.id });
  220. }
  221. });
  222. xrController.onDisposeObservable.addOnce(() => {
  223. if (controllerData.pick && downTriggered && !this._options.disablePointerUpOnTouchOut) {
  224. this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
  225. }
  226. });
  227. }
  228. private _attachGazeMode(xrController: WebXRController) {
  229. const controllerData = this._controllers[xrController.uniqueId];
  230. // attached when touched, detaches when raised
  231. const timeToSelect = this._options.timeToSelect || 3000;
  232. let oldPick = new PickingInfo();
  233. let discMesh = TorusBuilder.CreateTorus("selection", {
  234. diameter: 0.0035 * 15,
  235. thickness: 0.0025 * 6,
  236. tessellation: 20
  237. }, this._scene);
  238. discMesh.isVisible = false;
  239. discMesh.isPickable = false;
  240. discMesh.parent = controllerData.selectionMesh;
  241. let timer = 0;
  242. let downTriggered = false;
  243. controllerData.onFrameObserver = this._xrSessionManager.onXRFrameObservable.add(() => {
  244. if (!controllerData.pick) { return; }
  245. if (controllerData.pick.hit) {
  246. discMesh.isVisible = true;
  247. } else {
  248. discMesh.isVisible = false;
  249. }
  250. if (controllerData.pick.hit && controllerData.pick.pickedMesh === oldPick.pickedMesh) {
  251. timer += this._scene.getEngine().getDeltaTime();
  252. if (timer >= timeToSelect) {
  253. this._scene.simulatePointerDown(controllerData.pick, { pointerId: controllerData.id });
  254. downTriggered = true;
  255. // pointer up right after down, if disable on touch out
  256. if (this._options.disablePointerUpOnTouchOut) {
  257. this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
  258. }
  259. timer = 0;
  260. }
  261. const scaleFactor = 1 - (timer / timeToSelect);
  262. discMesh.scaling.set(scaleFactor, scaleFactor, scaleFactor);
  263. } else {
  264. downTriggered = false;
  265. timer = 0;
  266. }
  267. oldPick = controllerData.pick;
  268. });
  269. xrController.onDisposeObservable.addOnce(() => {
  270. if (controllerData.pick && !this._options.disablePointerUpOnTouchOut && downTriggered) {
  271. this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
  272. }
  273. discMesh.dispose();
  274. });
  275. }
  276. private _attachTrackedPointerRayMode(xrController: WebXRController) {
  277. if (!xrController.gamepadController) {
  278. return;
  279. }
  280. const controllerData = this._controllers[xrController.uniqueId];
  281. if (this._options.overrideButtonId) {
  282. controllerData.selectionComponent = xrController.gamepadController.getComponent(this._options.overrideButtonId);
  283. }
  284. if (!controllerData.selectionComponent) {
  285. controllerData.selectionComponent = xrController.gamepadController.getMainComponent();
  286. }
  287. let observer: Nullable<Observer<XRFrame>> = null;
  288. controllerData.onFrameObserver = this._xrSessionManager.onXRFrameObservable.add(() => {
  289. if (controllerData.selectionComponent && controllerData.selectionComponent.pressed) {
  290. (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshPickedColor;
  291. (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.laserPointerPickedColor;
  292. } else {
  293. (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshDefaultColor;
  294. (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.lasterPointerDefaultColor;
  295. }
  296. });
  297. controllerData.onButtonChangedObserver = controllerData.selectionComponent.onButtonStateChanged.add((component) => {
  298. if (component.changes.pressed) {
  299. const pressed = component.changes.pressed.current;
  300. if (controllerData.pick) {
  301. if (pressed) {
  302. this._scene.simulatePointerDown(controllerData.pick, { pointerId: controllerData.id });
  303. observer = this._xrSessionManager.onXRFrameObservable.add(() => {
  304. if (controllerData.pick) {
  305. this._scene.simulatePointerMove(controllerData.pick, { pointerId: controllerData.id });
  306. }
  307. });
  308. } else {
  309. this._xrSessionManager.onXRFrameObservable.remove(observer);
  310. this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
  311. }
  312. }
  313. }
  314. });
  315. }
  316. private _detachController(xrControllerUniqueId: string) {
  317. const controllerData = this._controllers[xrControllerUniqueId];
  318. if (!controllerData) { return; }
  319. if (controllerData.selectionComponent) {
  320. if (controllerData.onButtonChangedObserver) {
  321. controllerData.selectionComponent.onButtonStateChanged.remove(controllerData.onButtonChangedObserver);
  322. }
  323. }
  324. if (controllerData.onFrameObserver) {
  325. this._xrSessionManager.onXRFrameObservable.remove(controllerData.onFrameObserver);
  326. }
  327. controllerData.selectionMesh.dispose();
  328. controllerData.laserPointer.dispose();
  329. // remove from the map
  330. delete this._controllers[xrControllerUniqueId];
  331. }
  332. private _generateNewMeshPair(xrController: WebXRController) {
  333. const laserPointer = CylinderBuilder.CreateCylinder("laserPointer", {
  334. height: 1,
  335. diameterTop: 0.0002,
  336. diameterBottom: 0.004,
  337. tessellation: 20,
  338. subdivisions: 1
  339. }, this._scene);
  340. laserPointer.parent = xrController.pointer;
  341. let laserPointerMaterial = new StandardMaterial("laserPointerMat", this._scene);
  342. laserPointerMaterial.emissiveColor = this.lasterPointerDefaultColor;
  343. laserPointerMaterial.alpha = 0.6;
  344. laserPointer.material = laserPointerMaterial;
  345. laserPointer.rotation.x = Math.PI / 2;
  346. this._updatePointerDistance(laserPointer, 1);
  347. laserPointer.isPickable = false;
  348. // Create a gaze tracker for the XR controller
  349. const selectionMesh = TorusBuilder.CreateTorus("gazeTracker", {
  350. diameter: 0.0035 * 3,
  351. thickness: 0.0025 * 3,
  352. tessellation: 20
  353. }, this._scene);
  354. selectionMesh.bakeCurrentTransformIntoVertices();
  355. selectionMesh.isPickable = false;
  356. selectionMesh.isVisible = false;
  357. let targetMat = new StandardMaterial("targetMat", this._scene);
  358. targetMat.specularColor = Color3.Black();
  359. targetMat.emissiveColor = this.selectionMeshDefaultColor;
  360. targetMat.backFaceCulling = false;
  361. selectionMesh.material = targetMat;
  362. return {
  363. laserPointer,
  364. selectionMesh
  365. };
  366. }
  367. private _convertNormalToDirectionOfRay(normal: Nullable<Vector3>, ray: Ray) {
  368. if (normal) {
  369. let angle = Math.acos(Vector3.Dot(normal, ray.direction));
  370. if (angle < Math.PI / 2) {
  371. normal.scaleInPlace(-1);
  372. }
  373. }
  374. return normal;
  375. }
  376. private _updatePointerDistance(_laserPointer: AbstractMesh, distance: number = 100) {
  377. _laserPointer.scaling.y = distance;
  378. _laserPointer.position.z = distance / 2;
  379. console.log(distance, _laserPointer.position);
  380. }
  381. /**
  382. * Dispose this feature and all of the resources attached
  383. */
  384. dispose(): void {
  385. this.detach();
  386. }
  387. }
  388. //register the plugin
  389. WebXRFeaturesManager.AddWebXRFeature(WebXRControllerPointerSelection.Name, (xrSessionManager, options) => {
  390. return () => new WebXRControllerPointerSelection(xrSessionManager, options);
  391. }, WebXRControllerPointerSelection.Version, true);