webXRControllerPointerSelection.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import { Nullable } from "../../types";
  2. import { Vector3 } from '../../Maths/math.vector';
  3. import { Mesh } from '../../Meshes/mesh';
  4. import { Ray } from '../../Culling/ray';
  5. import { StandardMaterial } from '../../Materials/standardMaterial';
  6. import { WebXRInput } from './webXRInput';
  7. import { Color3 } from '../../Maths/math.color';
  8. import { Axis } from '../../Maths/math.axis';
  9. /**
  10. * Handles pointer input automatically for the pointer of XR controllers
  11. */
  12. export class WebXRControllerPointerSelection {
  13. private static _idCounter = 0;
  14. private _tmpRay = new Ray(new Vector3(), new Vector3());
  15. /**
  16. * Creates a WebXRControllerPointerSelection
  17. * @param input input manager to setup pointer selection
  18. */
  19. constructor(input: WebXRInput) {
  20. input.onControllerAddedObservable.add((controller) => {
  21. let scene = controller.pointer.getScene();
  22. let laserPointer: Mesh;
  23. let cursorMesh: Mesh;
  24. let triggerDown = false;
  25. let id: number;
  26. id = WebXRControllerPointerSelection._idCounter++;
  27. // Create a laser pointer for the XR controller
  28. laserPointer = Mesh.CreateCylinder("laserPointer", 1, 0.0002, 0.004, 20, 1, scene, false);
  29. laserPointer.parent = controller.pointer;
  30. let laserPointerMaterial = new StandardMaterial("laserPointerMat", scene);
  31. laserPointerMaterial.emissiveColor = new Color3(0.7, 0.7, 0.7);
  32. laserPointerMaterial.alpha = 0.6;
  33. laserPointer.material = laserPointerMaterial;
  34. laserPointer.rotation.x = Math.PI / 2;
  35. this._updatePointerDistance(laserPointer, 1);
  36. laserPointer.isPickable = false;
  37. // Create a gaze tracker for the XR controller
  38. cursorMesh = Mesh.CreateTorus("gazeTracker", 0.0035 * 3, 0.0025 * 3, 20, scene, false);
  39. cursorMesh.bakeCurrentTransformIntoVertices();
  40. cursorMesh.isPickable = false;
  41. cursorMesh.isVisible = false;
  42. let targetMat = new StandardMaterial("targetMat", scene);
  43. targetMat.specularColor = Color3.Black();
  44. targetMat.emissiveColor = new Color3(0.7, 0.7, 0.7);
  45. targetMat.backFaceCulling = false;
  46. cursorMesh.material = targetMat;
  47. let renderObserver = scene.onBeforeRenderObservable.add(() => {
  48. // Every frame check collisions/input
  49. controller.getWorldPointerRayToRef(this._tmpRay);
  50. let pick = scene.pickWithRay(this._tmpRay);
  51. if (pick) {
  52. let pressed = false;
  53. if (controller.inputSource.gamepad) {
  54. if (controller.inputSource.gamepad.buttons[0] && controller.inputSource.gamepad.buttons[0].value > 0.7) {
  55. pressed = true;
  56. } else if (controller.inputSource.gamepad.buttons[1] && controller.inputSource.gamepad.buttons[1].pressed) {
  57. pressed = true;
  58. }
  59. }
  60. // in screen mode - means finger is on the screen
  61. if (controller.inputSource.targetRayMode === 'screen') {
  62. pressed = true;
  63. }
  64. if (pressed) {
  65. if (!triggerDown) {
  66. scene.simulatePointerDown(pick, { pointerId: id });
  67. }
  68. triggerDown = true;
  69. } else {
  70. if (triggerDown) {
  71. scene.simulatePointerUp(pick, { pointerId: id });
  72. }
  73. triggerDown = false;
  74. }
  75. scene.simulatePointerMove(pick, { pointerId: id });
  76. }
  77. if (pick && pick.pickedPoint && pick.hit) {
  78. // Update laser state
  79. this._updatePointerDistance(laserPointer, pick.distance);
  80. // Update cursor state
  81. cursorMesh.position.copyFrom(pick.pickedPoint);
  82. cursorMesh.scaling.x = Math.sqrt(pick.distance);
  83. cursorMesh.scaling.y = Math.sqrt(pick.distance);
  84. cursorMesh.scaling.z = Math.sqrt(pick.distance);
  85. // To avoid z-fighting
  86. let pickNormal = this._convertNormalToDirectionOfRay(pick.getNormal(), this._tmpRay);
  87. let deltaFighting = 0.002;
  88. cursorMesh.position.copyFrom(pick.pickedPoint);
  89. if (pickNormal) {
  90. let axis1 = Vector3.Cross(Axis.Y, pickNormal);
  91. let axis2 = Vector3.Cross(pickNormal, axis1);
  92. Vector3.RotationFromAxisToRef(axis2, pickNormal, axis1, cursorMesh.rotation);
  93. cursorMesh.position.addInPlace(pickNormal.scale(deltaFighting));
  94. }
  95. cursorMesh.isVisible = true;
  96. } else {
  97. cursorMesh.isVisible = false;
  98. }
  99. });
  100. controller.onDisposeObservable.addOnce(() => {
  101. laserPointer.dispose();
  102. cursorMesh.dispose();
  103. if (controller.inputSource.targetRayMode === 'screen') {
  104. controller.getWorldPointerRayToRef(this._tmpRay);
  105. let pick = scene.pickWithRay(this._tmpRay);
  106. if (pick) {
  107. scene.simulatePointerUp(pick, { pointerId: id });
  108. }
  109. }
  110. scene.onBeforeRenderObservable.remove(renderObserver);
  111. });
  112. });
  113. }
  114. private _convertNormalToDirectionOfRay(normal: Nullable<Vector3>, ray: Ray) {
  115. if (normal) {
  116. let angle = Math.acos(Vector3.Dot(normal, ray.direction));
  117. if (angle < Math.PI / 2) {
  118. normal.scaleInPlace(-1);
  119. }
  120. }
  121. return normal;
  122. }
  123. private _updatePointerDistance(_laserPointer: Mesh, distance: number = 100) {
  124. _laserPointer.scaling.y = distance;
  125. _laserPointer.position.z = distance / 2;
  126. }
  127. }