WebXRControllerPointerSelection.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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. /**
  33. * A module that will enable pointer selection for motion controllers of XR Input Sources
  34. */
  35. export class WebXRControllerPointerSelection implements IWebXRFeature {
  36. /**
  37. * The module's name
  38. */
  39. public static readonly Name = Name;
  40. /**
  41. * The (Babylon) version of this module.
  42. * This is an integer representing the implementation version.
  43. * This number does not correspond to the webxr specs version
  44. */
  45. public static readonly Version = 1;
  46. /**
  47. * This color will be set to the laser pointer when selection is triggered
  48. */
  49. public onPickedLaserPointerColor: Color3 = new Color3(0.7, 0.7, 0.7);
  50. /**
  51. * This color will be applied to the selection ring when selection is triggered
  52. */
  53. public onPickedSelectionMeshColor: Color3 = new Color3(0.7, 0.7, 0.7);
  54. /**
  55. * default color of the selection ring
  56. */
  57. public selectionMeshDefaultColor: Color3 = new Color3(0.5, 0.5, 0.5);
  58. /**
  59. * Default color of the laser pointer
  60. */
  61. public lasterPointerDefaultColor: Color3 = new Color3(0.5, 0.5, 0.5);
  62. private static _idCounter = 0;
  63. private _observerTracked: Nullable<Observer<XRFrame>>;
  64. private _attached: boolean = false;
  65. private _tmpRay = new Ray(new Vector3(), new Vector3());
  66. private _controllers: {
  67. [controllerUniqueId: string]: {
  68. xrController: WebXRController;
  69. selectionComponent?: WebXRControllerComponent;
  70. onButtonChangedObserver?: Nullable<Observer<WebXRControllerComponent>>;
  71. laserPointer: AbstractMesh;
  72. selectionMesh: AbstractMesh;
  73. pick: Nullable<PickingInfo>;
  74. id: number;
  75. };
  76. } = {};
  77. /**
  78. * Is this feature attached
  79. */
  80. public get attached() {
  81. return this._attached;
  82. }
  83. private _scene: Scene;
  84. /**
  85. * constructs a new background remover module
  86. * @param _xrSessionManager the session manager for this module
  87. * @param _options read-only options to be used in this module
  88. */
  89. constructor(private _xrSessionManager: WebXRSessionManager, private readonly _options: IWebXRControllerPointerSelectionOptions) {
  90. this._scene = this._xrSessionManager.scene;
  91. }
  92. /**
  93. * attach this feature
  94. * Will usually be called by the features manager
  95. *
  96. * @returns true if successful.
  97. */
  98. attach(): boolean {
  99. this._options.xrInput.controllers.forEach(this._attachController);
  100. this._options.xrInput.onControllerAddedObservable.add(this._attachController);
  101. this._options.xrInput.onControllerRemovedObservable.add((controller) => {
  102. // REMOVE the controller
  103. this._detachController(controller.uniqueId);
  104. });
  105. this._observerTracked = this._xrSessionManager.onXRFrameObservable.add(() => {
  106. Object.keys(this._controllers).forEach((id) => {
  107. const controllerData = this._controllers[id];
  108. // Every frame check collisions/input
  109. controllerData.xrController.getWorldPointerRayToRef(this._tmpRay);
  110. controllerData.pick = this._scene.pickWithRay(this._tmpRay);
  111. if (controllerData.selectionComponent && controllerData.selectionComponent.pressed) {
  112. (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.onPickedSelectionMeshColor;
  113. (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.onPickedLaserPointerColor;
  114. } else {
  115. (<StandardMaterial>controllerData.selectionMesh.material).emissiveColor = this.selectionMeshDefaultColor;
  116. (<StandardMaterial>controllerData.laserPointer.material).emissiveColor = this.lasterPointerDefaultColor;
  117. }
  118. const pick = controllerData.pick;
  119. if (pick && pick.pickedPoint && pick.hit) {
  120. // Update laser state
  121. this._updatePointerDistance(controllerData.laserPointer, pick.distance);
  122. // Update cursor state
  123. controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
  124. controllerData.selectionMesh.scaling.x = Math.sqrt(pick.distance);
  125. controllerData.selectionMesh.scaling.y = Math.sqrt(pick.distance);
  126. controllerData.selectionMesh.scaling.z = Math.sqrt(pick.distance);
  127. // To avoid z-fighting
  128. let pickNormal = this._convertNormalToDirectionOfRay(pick.getNormal(true), this._tmpRay);
  129. let deltaFighting = 0.001;
  130. controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
  131. if (pickNormal) {
  132. let axis1 = Vector3.Cross(Axis.Y, pickNormal);
  133. let axis2 = Vector3.Cross(pickNormal, axis1);
  134. Vector3.RotationFromAxisToRef(axis2, pickNormal, axis1, controllerData.selectionMesh.rotation);
  135. controllerData.selectionMesh.position.addInPlace(pickNormal.scale(deltaFighting));
  136. }
  137. controllerData.selectionMesh.isVisible = true;
  138. } else {
  139. controllerData.selectionMesh.isVisible = false;
  140. }
  141. });
  142. });
  143. this._attached = true;
  144. return true;
  145. }
  146. /**
  147. * detach this feature.
  148. * Will usually be called by the features manager
  149. *
  150. * @returns true if successful.
  151. */
  152. detach(): boolean {
  153. if (this._observerTracked) {
  154. this._xrSessionManager.onXRFrameObservable.remove(this._observerTracked);
  155. }
  156. Object.keys(this._controllers).forEach((controllerId) => {
  157. this._detachController(controllerId);
  158. });
  159. this._attached = false;
  160. return true;
  161. }
  162. private _attachController = (xrController: WebXRController) => {
  163. // only support tracker pointer
  164. if (xrController.inputSource.targetRayMode !== "tracked-pointer") {
  165. return;
  166. }
  167. if (this._controllers[xrController.uniqueId] || !xrController.gamepadController) {
  168. // already attached
  169. return;
  170. }
  171. const { laserPointer, selectionMesh } = this._generateNewMeshPair(xrController);
  172. // get two new meshes
  173. this._controllers[xrController.uniqueId] = {
  174. xrController,
  175. laserPointer,
  176. selectionMesh,
  177. pick: null,
  178. id: WebXRControllerPointerSelection._idCounter++
  179. };
  180. const controllerData = this._controllers[xrController.uniqueId];
  181. if (this._options.overrideButtonId) {
  182. controllerData.selectionComponent = xrController.gamepadController.getComponent(this._options.overrideButtonId);
  183. }
  184. if (!controllerData.selectionComponent) {
  185. controllerData.selectionComponent = xrController.gamepadController.getMainComponent();
  186. }
  187. let observer: Nullable<Observer<XRFrame>> = null;
  188. controllerData.onButtonChangedObserver = controllerData.selectionComponent.onButtonStateChanged.add((component) => {
  189. if (component.changes.pressed) {
  190. const pressed = component.changes.pressed.current;
  191. if (controllerData.pick) {
  192. if (pressed) {
  193. this._scene.simulatePointerDown(controllerData.pick, { pointerId: controllerData.id });
  194. observer = this._xrSessionManager.onXRFrameObservable.add(() => {
  195. if (controllerData.pick) {
  196. this._scene.simulatePointerMove(controllerData.pick, { pointerId: controllerData.id });
  197. }
  198. });
  199. } else {
  200. this._xrSessionManager.onXRFrameObservable.remove(observer);
  201. this._scene.simulatePointerUp(controllerData.pick, { pointerId: controllerData.id });
  202. }
  203. }
  204. }
  205. });
  206. }
  207. private _detachController(xrControllerUniqueId: string) {
  208. const controllerData = this._controllers[xrControllerUniqueId];
  209. if (!controllerData) { return; }
  210. if (controllerData.selectionComponent) {
  211. if (controllerData.onButtonChangedObserver) {
  212. controllerData.selectionComponent.onButtonStateChanged.remove(controllerData.onButtonChangedObserver);
  213. }
  214. }
  215. controllerData.selectionMesh.dispose();
  216. controllerData.laserPointer.dispose();
  217. // remove from the map
  218. delete this._controllers[xrControllerUniqueId];
  219. }
  220. private _generateNewMeshPair(xrController: WebXRController) {
  221. const laserPointer = CylinderBuilder.CreateCylinder("laserPointer", {
  222. height: 1,
  223. diameterTop: 0.0002,
  224. diameterBottom: 0.004,
  225. tessellation: 20,
  226. subdivisions: 1
  227. }, this._scene);
  228. laserPointer.parent = xrController.pointer;
  229. let laserPointerMaterial = new StandardMaterial("laserPointerMat", this._scene);
  230. laserPointerMaterial.emissiveColor = this.lasterPointerDefaultColor;
  231. laserPointerMaterial.alpha = 0.6;
  232. laserPointer.material = laserPointerMaterial;
  233. laserPointer.rotation.x = Math.PI / 2;
  234. this._updatePointerDistance(laserPointer, 1);
  235. laserPointer.isPickable = false;
  236. // Create a gaze tracker for the XR controller
  237. const selectionMesh = TorusBuilder.CreateTorus("gazeTracker", {
  238. diameter: 0.0035 * 3,
  239. thickness: 0.0025 * 3,
  240. tessellation: 20
  241. }, this._scene);
  242. selectionMesh.bakeCurrentTransformIntoVertices();
  243. selectionMesh.isPickable = false;
  244. selectionMesh.isVisible = false;
  245. let targetMat = new StandardMaterial("targetMat", this._scene);
  246. targetMat.specularColor = Color3.Black();
  247. targetMat.emissiveColor = this.selectionMeshDefaultColor;
  248. targetMat.backFaceCulling = false;
  249. selectionMesh.material = targetMat;
  250. return {
  251. laserPointer,
  252. selectionMesh
  253. };
  254. }
  255. private _convertNormalToDirectionOfRay(normal: Nullable<Vector3>, ray: Ray) {
  256. if (normal) {
  257. let angle = Math.acos(Vector3.Dot(normal, ray.direction));
  258. if (angle < Math.PI / 2) {
  259. normal.scaleInPlace(-1);
  260. }
  261. }
  262. return normal;
  263. }
  264. private _updatePointerDistance(_laserPointer: AbstractMesh, distance: number = 100) {
  265. _laserPointer.scaling.y = distance;
  266. _laserPointer.position.z = distance / 2;
  267. }
  268. /**
  269. * Dispose this feature and all of the resources attached
  270. */
  271. dispose(): void {
  272. this.detach();
  273. }
  274. }
  275. //register the plugin
  276. WebXRFeaturesManager.AddWebXRFeature(WebXRControllerPointerSelection.Name, (xrSessionManager, options) => {
  277. return () => new WebXRControllerPointerSelection(xrSessionManager, options);
  278. }, WebXRControllerPointerSelection.Version, true);