touchButton3D.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. // Assumptions: absolute position of button mesh is inside the mesh
  2. import { DeepImmutableObject, Nullable } from "babylonjs/types";
  3. import { Vector3, Quaternion } from "babylonjs/Maths/math.vector";
  4. import { Mesh } from "babylonjs/Meshes/mesh";
  5. import { AbstractMesh } from "babylonjs/Meshes/abstractMesh";
  6. import { TransformNode } from "babylonjs/Meshes/transformNode";
  7. import { Scene } from "babylonjs/scene";
  8. import { Ray } from "babylonjs/Culling/ray";
  9. import { Button3D } from "./button3D";
  10. /**
  11. * Enum for Button States
  12. */
  13. enum ButtonState {
  14. /** None */
  15. None = 0,
  16. /** Pointer Entered */
  17. Hover = 1,
  18. /** Pointer Down */
  19. Press = 2
  20. }
  21. class TouchButton3DManager {
  22. private _touchButtonList = new Map<number, TouchButton3D>();
  23. private _buttonIndex = 1;
  24. private _sceneRegisteredOn: Nullable<Scene>;
  25. private _handleCollisions = () => {
  26. if (this._sceneRegisteredOn != null) {
  27. const touchMeshes = this._sceneRegisteredOn.getMeshesByTags("touchEnabled");
  28. this._touchButtonList.forEach(function (button: TouchButton3D) {
  29. touchMeshes.forEach(function (mesh: Mesh) {
  30. button._collisionCheckForStateChange(mesh);
  31. });
  32. });
  33. }
  34. }
  35. /**
  36. * Creates a new touchButton3DManager
  37. */
  38. constructor() {
  39. }
  40. public addButton(button: TouchButton3D): number {
  41. const index = this._buttonIndex++;
  42. this._touchButtonList.set(index, button);
  43. return index;
  44. }
  45. public removeButton(index: number): boolean {
  46. return this._touchButtonList.delete(index);
  47. }
  48. public enableCollisionHandling(scene: Scene) {
  49. if (this._sceneRegisteredOn != scene) {
  50. if (this._sceneRegisteredOn != null) {
  51. this.disableCollisionHandling();
  52. }
  53. scene.registerBeforeRender(this._handleCollisions);
  54. this._sceneRegisteredOn = scene;
  55. }
  56. }
  57. public disableCollisionHandling() {
  58. this._sceneRegisteredOn?.unregisterBeforeRender(this._handleCollisions);
  59. this._sceneRegisteredOn = null;
  60. }
  61. }
  62. /**
  63. * Class used to create a touchable button in 3D
  64. */
  65. export class TouchButton3D extends Button3D {
  66. private _collisionMesh: Mesh;
  67. private _collidableFrontDirection: Vector3;
  68. private _lastTouchPoint: Vector3;
  69. private _tempButtonForwardRay: Ray;
  70. private _lastKnownCollidableScale: Vector3;
  71. private _collidableInitialized = false;
  72. private _frontOffset = 0;
  73. private _backOffset = 0;
  74. private _hoverOffset = 0;
  75. private _pushThroughBackOffset = 0;
  76. private _activeInteractions = new Map<number, ButtonState>();
  77. private _previousHeight = new Map<number, number>();
  78. private static _buttonManager = new TouchButton3DManager();
  79. private _buttonManagerIndex = 0;
  80. /**
  81. * Creates a new touchable button
  82. * @param name defines the control name
  83. * @param collisionMesh mesh to track collisions with
  84. */
  85. constructor(name?: string, collisionMesh?: Mesh) {
  86. super(name);
  87. this._tempButtonForwardRay = new Ray(Vector3.Zero(), Vector3.Zero());
  88. if (collisionMesh) {
  89. this.collisionMesh = collisionMesh;
  90. }
  91. this._buttonManagerIndex = TouchButton3D._buttonManager.addButton(this);
  92. }
  93. /**
  94. * Sets the front-facing direction of the button
  95. * @param frontDir the forward direction of the button
  96. */
  97. public set collidableFrontDirection(frontWorldDir: Vector3) {
  98. this._collidableFrontDirection = frontWorldDir.normalize();
  99. // Zero out the scale to force it to be proplerly updated in _updateDistanceOffsets
  100. this._lastKnownCollidableScale = Vector3.Zero();
  101. this._updateDistanceOffsets();
  102. }
  103. private _getWorldMatrixData(mesh: Mesh) {
  104. let translation = Vector3.Zero();
  105. let rotation = Quaternion.Identity();
  106. let scale = Vector3.Zero();
  107. mesh.getWorldMatrix().decompose(scale, rotation, translation);
  108. return {translation: translation, rotation: rotation, scale: scale};
  109. }
  110. /**
  111. * Sets the mesh used for testing input collision
  112. * @param collisionMesh the new collision mesh for the button
  113. */
  114. public set collisionMesh(collisionMesh: Mesh) {
  115. if (this._collisionMesh) {
  116. this._collisionMesh.dispose();
  117. }
  118. // parent the mesh to sync transforms
  119. if (!collisionMesh.parent && this.mesh) {
  120. collisionMesh.setParent(this.mesh);
  121. }
  122. this._collisionMesh = collisionMesh;
  123. this._collisionMesh.metadata = this;
  124. this.collidableFrontDirection = collisionMesh.forward;
  125. this._collidableInitialized = true;
  126. }
  127. /**
  128. * Sets the scene used on the manager to register the collision callback on
  129. * @param scene the scene to use
  130. */
  131. public set sceneForCollisions(scene: Scene) {
  132. TouchButton3D._buttonManager.enableCollisionHandling(scene);
  133. }
  134. /*
  135. * Given a point, and two points on a line, this returns the distance between
  136. * the point and the closest point on the line. The closest point on the line
  137. * does not have to be between the two given points.
  138. *
  139. * Based off the 3D point-line distance equation
  140. *
  141. * Assumes lineDirection is normalized
  142. */
  143. private _getShortestDistancePointToLine(point: Vector3, linePoint: Vector3, lineDirection: Vector3) {
  144. const pointToLine = linePoint.subtract(point);
  145. const cross = lineDirection.cross(pointToLine);
  146. return cross.length();
  147. }
  148. /*
  149. * Checks to see if collidable is in a position to interact with the button
  150. * - check if collidable has a plane height off the button that is within range
  151. * - check that collidable + normal ray intersect the bounding sphere
  152. */
  153. private _isPrimedForInteraction(collidable: Vector3): boolean {
  154. // Check if the collidable has an appropriate planar height
  155. const heightFromCenter = this._getHeightFromButtonCenter(collidable);
  156. if (heightFromCenter > this._hoverOffset || heightFromCenter < this._pushThroughBackOffset) {
  157. return false;
  158. }
  159. // Check if the collidable or its hover ray lands within the bounding sphere of the button
  160. const distanceFromCenter = this._getShortestDistancePointToLine(this._collisionMesh.getAbsolutePosition(),
  161. collidable,
  162. this._collidableFrontDirection);
  163. return distanceFromCenter <= this._collisionMesh.getBoundingInfo().boundingSphere.radiusWorld;
  164. }
  165. /*
  166. * Returns a Vector3 of the collidable's projected position on the button
  167. * Returns the collidable's position if it is inside the button
  168. */
  169. private _getPointOnButton(collidable: Vector3): Vector3 {
  170. const heightFromCenter = this._getHeightFromButtonCenter(collidable);
  171. if (heightFromCenter <= this._frontOffset && heightFromCenter >= this._backOffset) {
  172. // The collidable is in the button, return its position
  173. return collidable;
  174. }
  175. else if (heightFromCenter > this._frontOffset) {
  176. // The collidable is in front of the button, project it to the surface
  177. const collidableDistanceToFront = (this._frontOffset - heightFromCenter);
  178. return collidable.add(this._collidableFrontDirection.scale(collidableDistanceToFront));
  179. }
  180. else {
  181. // The collidable is behind the button, project it to its back
  182. const collidableDistanceToBack = (this._backOffset - heightFromCenter);
  183. return collidable.add(this._collidableFrontDirection.scale(collidableDistanceToBack));
  184. }
  185. }
  186. /*
  187. * Updates the distance values.
  188. * Should be called when the front direction changes, or the mesh size changes
  189. *
  190. * Sets the following values:
  191. * _frontOffset
  192. * _backOffset
  193. * _hoverOffset
  194. * _pushThroughBackOffset
  195. *
  196. * Requires population of:
  197. * _collisionMesh
  198. * _collidableFrontDirection
  199. */
  200. private _updateDistanceOffsets() {
  201. let worldMatrixData = this._getWorldMatrixData(this._collisionMesh);
  202. if (!worldMatrixData.scale.equalsWithEpsilon(this._lastKnownCollidableScale)) {
  203. const collisionMeshPos = this._collisionMesh.getAbsolutePosition();
  204. this._tempButtonForwardRay.origin = collisionMeshPos;
  205. this._tempButtonForwardRay.direction = this._collidableFrontDirection;
  206. const frontPickingInfo = this._tempButtonForwardRay.intersectsMesh(this._collisionMesh as DeepImmutableObject<AbstractMesh>);
  207. this._tempButtonForwardRay.direction = this._tempButtonForwardRay.direction.negate();
  208. const backPickingInfo = this._tempButtonForwardRay.intersectsMesh(this._collisionMesh as DeepImmutableObject<AbstractMesh>);
  209. this._frontOffset = 0;
  210. this._backOffset = 0;
  211. if (frontPickingInfo.hit && backPickingInfo.hit) {
  212. this._frontOffset = this._getDistanceOffPlane(frontPickingInfo.pickedPoint!,
  213. this._collidableFrontDirection,
  214. collisionMeshPos);
  215. this._backOffset = this._getDistanceOffPlane(backPickingInfo.pickedPoint!,
  216. this._collidableFrontDirection,
  217. collisionMeshPos);
  218. }
  219. // For now, set the hover height equal to the thickness of the button
  220. const buttonThickness = this._frontOffset - this._backOffset;
  221. this._hoverOffset = this._frontOffset + (buttonThickness * 1.25);
  222. this._pushThroughBackOffset = this._backOffset - (buttonThickness * 1.5);
  223. this._lastKnownCollidableScale = this._getWorldMatrixData(this._collisionMesh).scale;
  224. }
  225. }
  226. // Returns the distance in front of the center of the button
  227. // Returned value is negative when collidable is past the center
  228. private _getHeightFromButtonCenter(collidablePos: Vector3) {
  229. return this._getDistanceOffPlane(collidablePos, this._collidableFrontDirection, this._collisionMesh.getAbsolutePosition());
  230. }
  231. // Returns the distance from pointOnPlane to point along planeNormal
  232. private _getDistanceOffPlane(point: Vector3, planeNormal: Vector3, pointOnPlane: Vector3) {
  233. const d = Vector3.Dot(pointOnPlane, planeNormal);
  234. const abc = Vector3.Dot(point, planeNormal);
  235. return abc - d;
  236. }
  237. // Updates the stored state of the button, and fire pointer events
  238. private _updateButtonState(id: number, newState: ButtonState, pointOnButton: Vector3) {
  239. const dummyPointerId = 0;
  240. const buttonIndex = 0; // Left click
  241. const buttonStateForId = this._activeInteractions.get(id) || ButtonState.None;
  242. // Take into account all inputs interacting with the button to avoid state flickering
  243. let previousPushDepth = 0;
  244. this._activeInteractions.forEach(function(value, key) {
  245. previousPushDepth = Math.max(previousPushDepth, value);
  246. });
  247. if (buttonStateForId != newState) {
  248. if (newState == ButtonState.None) {
  249. this._activeInteractions.delete(id);
  250. }
  251. else {
  252. this._activeInteractions.set(id, newState);
  253. }
  254. }
  255. let newPushDepth = 0;
  256. this._activeInteractions.forEach(function(value, key) {
  257. newPushDepth = Math.max(newPushDepth, value);
  258. });
  259. if (newPushDepth == ButtonState.Press) {
  260. if (previousPushDepth == ButtonState.Hover) {
  261. this._onPointerDown(this, pointOnButton, dummyPointerId, buttonIndex);
  262. }
  263. else if (previousPushDepth == ButtonState.Press) {
  264. this._onPointerMove(this, pointOnButton);
  265. }
  266. }
  267. else if (newPushDepth == ButtonState.Hover) {
  268. if (previousPushDepth == ButtonState.None) {
  269. this._onPointerEnter(this);
  270. }
  271. else if (previousPushDepth == ButtonState.Press) {
  272. this._onPointerUp(this, pointOnButton, dummyPointerId, buttonIndex, false);
  273. }
  274. else {
  275. this._onPointerMove(this, pointOnButton);
  276. }
  277. }
  278. else if (newPushDepth == ButtonState.None) {
  279. if (previousPushDepth == ButtonState.Hover) {
  280. this._onPointerOut(this);
  281. }
  282. else if (previousPushDepth == ButtonState.Press) {
  283. this._onPointerUp(this, pointOnButton, dummyPointerId, buttonIndex, false);
  284. this._onPointerOut(this);
  285. }
  286. }
  287. }
  288. // Decides whether to change button state based on the planar depth of the input source
  289. /** @hidden */
  290. public _collisionCheckForStateChange(mesh: Mesh) {
  291. if (this._collidableInitialized) {
  292. this._updateDistanceOffsets();
  293. const collidablePosition = mesh.getAbsolutePosition();
  294. const inRange = this._isPrimedForInteraction(collidablePosition);
  295. const uniqueId = mesh.uniqueId;
  296. let activeInteraction = this._activeInteractions.get(uniqueId);
  297. if (inRange) {
  298. const pointOnButton = this._getPointOnButton(collidablePosition);
  299. const heightFromCenter = this._getHeightFromButtonCenter(collidablePosition);
  300. const flickerDelta = 0.003;
  301. this._lastTouchPoint = pointOnButton;
  302. const isGreater = function (compareHeight: number) {
  303. return heightFromCenter >= (compareHeight + flickerDelta);
  304. };
  305. const isLower = function (compareHeight: number) {
  306. return heightFromCenter <= (compareHeight - flickerDelta);
  307. };
  308. // Update button state and fire events
  309. switch (activeInteraction || ButtonState.None) {
  310. case ButtonState.None:
  311. if (isGreater(this._frontOffset) &&
  312. isLower(this._hoverOffset)) {
  313. this._updateButtonState(uniqueId, ButtonState.Hover, pointOnButton);
  314. }
  315. break;
  316. case ButtonState.Hover:
  317. if (isGreater(this._hoverOffset)) {
  318. this._updateButtonState(uniqueId, ButtonState.None, pointOnButton);
  319. }
  320. else if (isLower(this._frontOffset)) {
  321. this._updateButtonState(uniqueId, ButtonState.Press, pointOnButton);
  322. }
  323. break;
  324. case ButtonState.Press:
  325. if (isGreater(this._frontOffset)) {
  326. this._updateButtonState(uniqueId, ButtonState.Hover, pointOnButton);
  327. }
  328. else if (isLower(this._pushThroughBackOffset)) {
  329. this._updateButtonState(uniqueId, ButtonState.None, pointOnButton);
  330. }
  331. break;
  332. }
  333. this._previousHeight.set(uniqueId, heightFromCenter);
  334. }
  335. else if ((activeInteraction != undefined) && (activeInteraction != ButtonState.None)) {
  336. this._updateButtonState(uniqueId, ButtonState.None, this._lastTouchPoint);
  337. this._previousHeight.delete(uniqueId);
  338. }
  339. }
  340. }
  341. protected _getTypeName(): string {
  342. return "TouchButton3D";
  343. }
  344. // Mesh association
  345. protected _createNode(scene: Scene): TransformNode {
  346. return super._createNode(scene);
  347. this.sceneForCollisions = scene;
  348. }
  349. /**
  350. * Releases all associated resources
  351. */
  352. public dispose() {
  353. super.dispose();
  354. if (this._collisionMesh) {
  355. this._collisionMesh.dispose();
  356. }
  357. TouchButton3D._buttonManager.removeButton(this._buttonManagerIndex);
  358. }
  359. }