WebXRControllerTeleportation.ts 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. import { IWebXRFeature, WebXRFeaturesManager, WebXRFeatureName } from "../webXRFeaturesManager";
  2. import { Observer } from "../../Misc/observable";
  3. import { WebXRSessionManager } from "../webXRSessionManager";
  4. import { Nullable } from "../../types";
  5. import { WebXRInput } from "../webXRInput";
  6. import { WebXRInputSource } from "../webXRInputSource";
  7. import { WebXRControllerComponent, IWebXRMotionControllerAxesValue } from "../motionController/webXRControllerComponent";
  8. import { AbstractMesh } from "../../Meshes/abstractMesh";
  9. import { Vector3, Quaternion } from "../../Maths/math.vector";
  10. import { Ray } from "../../Culling/ray";
  11. import { Material } from "../../Materials/material";
  12. import { DynamicTexture } from "../../Materials/Textures/dynamicTexture";
  13. import { CylinderBuilder } from "../../Meshes/Builders/cylinderBuilder";
  14. import { SineEase, EasingFunction } from "../../Animations/easing";
  15. import { Animation } from "../../Animations/animation";
  16. import { Axis } from "../../Maths/math.axis";
  17. import { StandardMaterial } from "../../Materials/standardMaterial";
  18. import { GroundBuilder } from "../../Meshes/Builders/groundBuilder";
  19. import { TorusBuilder } from "../../Meshes/Builders/torusBuilder";
  20. import { PickingInfo } from "../../Collisions/pickingInfo";
  21. import { Curve3 } from "../../Maths/math.path";
  22. import { LinesBuilder } from "../../Meshes/Builders/linesBuilder";
  23. import { WebXRAbstractFeature } from "./WebXRAbstractFeature";
  24. import { Color3 } from "../../Maths/math.color";
  25. import { Scene } from "../../scene";
  26. import { UtilityLayerRenderer } from "../../Rendering/utilityLayerRenderer";
  27. import { PointerEventTypes } from "../../Events/pointerEvents";
  28. import { setAndStartTimer } from "../../Misc/timer";
  29. import { LinesMesh } from "../../Meshes/linesMesh";
  30. /**
  31. * The options container for the teleportation module
  32. */
  33. export interface IWebXRTeleportationOptions {
  34. /**
  35. * if provided, this scene will be used to render meshes.
  36. */
  37. customUtilityLayerScene?: Scene;
  38. /**
  39. * Values to configure the default target mesh
  40. */
  41. defaultTargetMeshOptions?: {
  42. /**
  43. * Fill color of the teleportation area
  44. */
  45. teleportationFillColor?: string;
  46. /**
  47. * Border color for the teleportation area
  48. */
  49. teleportationBorderColor?: string;
  50. /**
  51. * Disable the mesh's animation sequence
  52. */
  53. disableAnimation?: boolean;
  54. /**
  55. * Disable lighting on the material or the ring and arrow
  56. */
  57. disableLighting?: boolean;
  58. /**
  59. * Override the default material of the torus and arrow
  60. */
  61. torusArrowMaterial?: Material;
  62. };
  63. /**
  64. * A list of meshes to use as floor meshes.
  65. * Meshes can be added and removed after initializing the feature using the
  66. * addFloorMesh and removeFloorMesh functions
  67. * If empty, rotation will still work
  68. */
  69. floorMeshes?: AbstractMesh[];
  70. /**
  71. * use this rendering group id for the meshes (optional)
  72. */
  73. renderingGroupId?: number;
  74. /**
  75. * Should teleportation move only to snap points
  76. */
  77. snapPointsOnly?: boolean;
  78. /**
  79. * An array of points to which the teleportation will snap to.
  80. * If the teleportation ray is in the proximity of one of those points, it will be corrected to this point.
  81. */
  82. snapPositions?: Vector3[];
  83. /**
  84. * How close should the teleportation ray be in order to snap to position.
  85. * Default to 0.8 units (meters)
  86. */
  87. snapToPositionRadius?: number;
  88. /**
  89. * Provide your own teleportation mesh instead of babylon's wonderful doughnut.
  90. * If you want to support rotation, make sure your mesh has a direction indicator.
  91. *
  92. * When left untouched, the default mesh will be initialized.
  93. */
  94. teleportationTargetMesh?: AbstractMesh;
  95. /**
  96. * If main component is used (no thumbstick), how long should the "long press" take before teleport
  97. */
  98. timeToTeleport?: number;
  99. /**
  100. * Disable using the thumbstick and use the main component (usually trigger) on long press.
  101. * This will be automatically true if the controller doesn't have a thumbstick or touchpad.
  102. */
  103. useMainComponentOnly?: boolean;
  104. /**
  105. * Should meshes created here be added to a utility layer or the main scene
  106. */
  107. useUtilityLayer?: boolean;
  108. /**
  109. * Babylon XR Input class for controller
  110. */
  111. xrInput: WebXRInput;
  112. /**
  113. * Meshes that the teleportation ray cannot go through
  114. */
  115. pickBlockerMeshes?: AbstractMesh[];
  116. /**
  117. * Should teleport work only on a specific hand?
  118. */
  119. forceHandedness?: XRHandedness;
  120. /**
  121. * If provided, this function will be used to generate the ray mesh instead of the lines mesh being used per default
  122. */
  123. generateRayPathMesh?: (points: Vector3[]) => AbstractMesh;
  124. }
  125. /**
  126. * This is a teleportation feature to be used with WebXR-enabled motion controllers.
  127. * When enabled and attached, the feature will allow a user to move around and rotate in the scene using
  128. * the input of the attached controllers.
  129. */
  130. export class WebXRMotionControllerTeleportation extends WebXRAbstractFeature {
  131. private _controllers: {
  132. [controllerUniqueId: string]: {
  133. xrController: WebXRInputSource;
  134. teleportationComponent?: WebXRControllerComponent;
  135. teleportationState: {
  136. forward: boolean;
  137. backwards: boolean;
  138. currentRotation: number;
  139. baseRotation: number;
  140. rotating: boolean;
  141. };
  142. onAxisChangedObserver?: Nullable<Observer<IWebXRMotionControllerAxesValue>>;
  143. onButtonChangedObserver?: Nullable<Observer<WebXRControllerComponent>>;
  144. };
  145. } = {};
  146. private _currentTeleportationControllerId: string;
  147. private _floorMeshes: AbstractMesh[];
  148. private _quadraticBezierCurve: Nullable<AbstractMesh>;
  149. private _selectionFeature: Nullable<IWebXRFeature>;
  150. private _snapToPositions: Vector3[];
  151. private _snappedToPoint: boolean = false;
  152. private _teleportationRingMaterial?: StandardMaterial;
  153. private _tmpRay = new Ray(new Vector3(), new Vector3());
  154. private _tmpVector = new Vector3();
  155. private _tmpQuaternion = new Quaternion();
  156. /**
  157. * The module's name
  158. */
  159. public static readonly Name = WebXRFeatureName.TELEPORTATION;
  160. /**
  161. * The (Babylon) version of this module.
  162. * This is an integer representing the implementation version.
  163. * This number does not correspond to the webxr specs version
  164. */
  165. public static readonly Version = 1;
  166. /**
  167. * Is movement backwards enabled
  168. */
  169. public backwardsMovementEnabled = true;
  170. /**
  171. * Distance to travel when moving backwards
  172. */
  173. public backwardsTeleportationDistance: number = 0.7;
  174. /**
  175. * The distance from the user to the inspection point in the direction of the controller
  176. * A higher number will allow the user to move further
  177. * defaults to 5 (meters, in xr units)
  178. */
  179. public parabolicCheckRadius: number = 5;
  180. /**
  181. * Should the module support parabolic ray on top of direct ray
  182. * If enabled, the user will be able to point "at the sky" and move according to predefined radius distance
  183. * Very helpful when moving between floors / different heights
  184. */
  185. public parabolicRayEnabled: boolean = true;
  186. /**
  187. * The second type of ray - straight line.
  188. * Should it be enabled or should the parabolic line be the only one.
  189. */
  190. public straightRayEnabled: boolean = true;
  191. /**
  192. * How much rotation should be applied when rotating right and left
  193. */
  194. public rotationAngle: number = Math.PI / 8;
  195. private _rotationEnabled: boolean = true;
  196. /**
  197. * Is rotation enabled when moving forward?
  198. * Disabling this feature will prevent the user from deciding the direction when teleporting
  199. */
  200. public get rotationEnabled(): boolean {
  201. return this._rotationEnabled;
  202. }
  203. /**
  204. * Sets wether rotation is enabled or not
  205. * @param enabled is rotation enabled when teleportation is shown
  206. */
  207. public set rotationEnabled(enabled: boolean) {
  208. this._rotationEnabled = enabled;
  209. if (this._options.teleportationTargetMesh) {
  210. const children = this._options.teleportationTargetMesh.getChildMeshes(false, (node) => node.name === "rotationCone");
  211. if (children[0]) {
  212. children[0].setEnabled(enabled);
  213. }
  214. }
  215. }
  216. /**
  217. * Exposes the currently set teleportation target mesh.
  218. */
  219. public get teleportationTargetMesh(): Nullable<AbstractMesh> {
  220. return this._options.teleportationTargetMesh || null;
  221. }
  222. /**
  223. * constructs a new anchor system
  224. * @param _xrSessionManager an instance of WebXRSessionManager
  225. * @param _options configuration object for this feature
  226. */
  227. constructor(_xrSessionManager: WebXRSessionManager, private _options: IWebXRTeleportationOptions) {
  228. super(_xrSessionManager);
  229. // create default mesh if not provided
  230. if (!this._options.teleportationTargetMesh) {
  231. this._createDefaultTargetMesh();
  232. }
  233. this._floorMeshes = this._options.floorMeshes || [];
  234. this._snapToPositions = this._options.snapPositions || [];
  235. this._setTargetMeshVisibility(false);
  236. }
  237. /**
  238. * Get the snapPointsOnly flag
  239. */
  240. public get snapPointsOnly(): boolean {
  241. return !!this._options.snapPointsOnly;
  242. }
  243. /**
  244. * Sets the snapPointsOnly flag
  245. * @param snapToPoints should teleportation be exclusively to snap points
  246. */
  247. public set snapPointsOnly(snapToPoints: boolean) {
  248. this._options.snapPointsOnly = snapToPoints;
  249. }
  250. /**
  251. * Add a new mesh to the floor meshes array
  252. * @param mesh the mesh to use as floor mesh
  253. */
  254. public addFloorMesh(mesh: AbstractMesh) {
  255. this._floorMeshes.push(mesh);
  256. }
  257. /**
  258. * Add a new snap-to point to fix teleportation to this position
  259. * @param newSnapPoint The new Snap-To point
  260. */
  261. public addSnapPoint(newSnapPoint: Vector3) {
  262. this._snapToPositions.push(newSnapPoint);
  263. }
  264. public attach(): boolean {
  265. if (!super.attach()) {
  266. return false;
  267. }
  268. // Safety reset
  269. this._currentTeleportationControllerId = "";
  270. this._options.xrInput.controllers.forEach(this._attachController);
  271. this._addNewAttachObserver(this._options.xrInput.onControllerAddedObservable, this._attachController);
  272. this._addNewAttachObserver(this._options.xrInput.onControllerRemovedObservable, (controller) => {
  273. // REMOVE the controller
  274. this._detachController(controller.uniqueId);
  275. });
  276. return true;
  277. }
  278. public detach(): boolean {
  279. if (!super.detach()) {
  280. return false;
  281. }
  282. Object.keys(this._controllers).forEach((controllerId) => {
  283. this._detachController(controllerId);
  284. });
  285. this._setTargetMeshVisibility(false);
  286. this._currentTeleportationControllerId = "";
  287. this._controllers = {};
  288. return true;
  289. }
  290. public dispose(): void {
  291. super.dispose();
  292. this._options.teleportationTargetMesh && this._options.teleportationTargetMesh.dispose(false, true);
  293. }
  294. /**
  295. * Remove a mesh from the floor meshes array
  296. * @param mesh the mesh to remove
  297. */
  298. public removeFloorMesh(mesh: AbstractMesh) {
  299. const index = this._floorMeshes.indexOf(mesh);
  300. if (index !== -1) {
  301. this._floorMeshes.splice(index, 1);
  302. }
  303. }
  304. /**
  305. * Remove a mesh from the floor meshes array using its name
  306. * @param name the mesh name to remove
  307. */
  308. public removeFloorMeshByName(name: string) {
  309. const mesh = this._xrSessionManager.scene.getMeshByName(name);
  310. if (mesh) {
  311. this.removeFloorMesh(mesh);
  312. }
  313. }
  314. /**
  315. * This function will iterate through the array, searching for this point or equal to it. It will then remove it from the snap-to array
  316. * @param snapPointToRemove the point (or a clone of it) to be removed from the array
  317. * @returns was the point found and removed or not
  318. */
  319. public removeSnapPoint(snapPointToRemove: Vector3): boolean {
  320. // check if the object is in the array
  321. let index = this._snapToPositions.indexOf(snapPointToRemove);
  322. // if not found as an object, compare to the points
  323. if (index === -1) {
  324. for (let i = 0; i < this._snapToPositions.length; ++i) {
  325. // equals? index is i, break the loop
  326. if (this._snapToPositions[i].equals(snapPointToRemove)) {
  327. index = i;
  328. break;
  329. }
  330. }
  331. }
  332. // index is not -1? remove the object
  333. if (index !== -1) {
  334. this._snapToPositions.splice(index, 1);
  335. return true;
  336. }
  337. return false;
  338. }
  339. /**
  340. * This function sets a selection feature that will be disabled when
  341. * the forward ray is shown and will be reattached when hidden.
  342. * This is used to remove the selection rays when moving.
  343. * @param selectionFeature the feature to disable when forward movement is enabled
  344. */
  345. public setSelectionFeature(selectionFeature: Nullable<IWebXRFeature>) {
  346. this._selectionFeature = selectionFeature;
  347. }
  348. protected _onXRFrame(_xrFrame: XRFrame) {
  349. const frame = this._xrSessionManager.currentFrame;
  350. const scene = this._xrSessionManager.scene;
  351. if (!this.attach || !frame) {
  352. return;
  353. }
  354. // render target if needed
  355. const targetMesh = this._options.teleportationTargetMesh;
  356. if (this._currentTeleportationControllerId) {
  357. if (!targetMesh) {
  358. return;
  359. }
  360. targetMesh.rotationQuaternion = targetMesh.rotationQuaternion || new Quaternion();
  361. const controllerData = this._controllers[this._currentTeleportationControllerId];
  362. if (controllerData && controllerData.teleportationState.forward) {
  363. // set the rotation
  364. Quaternion.RotationYawPitchRollToRef(controllerData.teleportationState.currentRotation + controllerData.teleportationState.baseRotation, 0, 0, targetMesh.rotationQuaternion);
  365. // set the ray and position
  366. let hitPossible = false;
  367. controllerData.xrController.getWorldPointerRayToRef(this._tmpRay);
  368. if (this.straightRayEnabled) {
  369. // first check if direct ray possible
  370. // pick grounds that are LOWER only. upper will use parabolic path
  371. let pick = scene.pickWithRay(this._tmpRay, (o) => {
  372. // check for mesh-blockers
  373. if (this._options.pickBlockerMeshes && this._options.pickBlockerMeshes.indexOf(o) !== -1) {
  374. return true;
  375. }
  376. const index = this._floorMeshes.indexOf(o);
  377. if (index === -1) {
  378. return false;
  379. }
  380. return this._floorMeshes[index].absolutePosition.y < this._options.xrInput.xrCamera.position.y;
  381. });
  382. if (pick && pick.pickedMesh && this._options.pickBlockerMeshes && this._options.pickBlockerMeshes.indexOf(pick.pickedMesh) !== -1) {
  383. return;
  384. } else if (pick && pick.pickedPoint) {
  385. hitPossible = true;
  386. this._setTargetMeshPosition(pick.pickedPoint);
  387. this._setTargetMeshVisibility(true);
  388. this._showParabolicPath(pick);
  389. }
  390. }
  391. // straight ray is still the main ray, but disabling the straight line will force parabolic line.
  392. if (this.parabolicRayEnabled && !hitPossible) {
  393. // radius compensation according to pointer rotation around X
  394. const xRotation = controllerData.xrController.pointer.rotationQuaternion!.toEulerAngles().x;
  395. const compensation = 1 + (Math.PI / 2 - Math.abs(xRotation));
  396. // check parabolic ray
  397. const radius = this.parabolicCheckRadius * compensation;
  398. this._tmpRay.origin.addToRef(this._tmpRay.direction.scale(radius * 2), this._tmpVector);
  399. this._tmpVector.y = this._tmpRay.origin.y;
  400. this._tmpRay.origin.addInPlace(this._tmpRay.direction.scale(radius));
  401. this._tmpVector.subtractToRef(this._tmpRay.origin, this._tmpRay.direction);
  402. this._tmpRay.direction.normalize();
  403. let pick = scene.pickWithRay(this._tmpRay, (o) => {
  404. // check for mesh-blockers
  405. if (this._options.pickBlockerMeshes && this._options.pickBlockerMeshes.indexOf(o) !== -1) {
  406. return true;
  407. }
  408. return this._floorMeshes.indexOf(o) !== -1;
  409. });
  410. if (pick && pick.pickedMesh && this._options.pickBlockerMeshes && this._options.pickBlockerMeshes.indexOf(pick.pickedMesh) !== -1) {
  411. return;
  412. } else if (pick && pick.pickedPoint) {
  413. hitPossible = true;
  414. this._setTargetMeshPosition(pick.pickedPoint);
  415. this._setTargetMeshVisibility(true);
  416. this._showParabolicPath(pick);
  417. }
  418. }
  419. // if needed, set visible:
  420. this._setTargetMeshVisibility(hitPossible);
  421. } else {
  422. this._setTargetMeshVisibility(false);
  423. }
  424. } else {
  425. this._setTargetMeshVisibility(false);
  426. }
  427. }
  428. private _attachController = (xrController: WebXRInputSource) => {
  429. if (this._controllers[xrController.uniqueId] || (this._options.forceHandedness && xrController.inputSource.handedness !== this._options.forceHandedness)) {
  430. // already attached
  431. return;
  432. }
  433. this._controllers[xrController.uniqueId] = {
  434. xrController,
  435. teleportationState: {
  436. forward: false,
  437. backwards: false,
  438. rotating: false,
  439. currentRotation: 0,
  440. baseRotation: 0,
  441. },
  442. };
  443. const controllerData = this._controllers[xrController.uniqueId];
  444. // motion controller only available to gamepad-enabled input sources.
  445. if (controllerData.xrController.inputSource.targetRayMode === "tracked-pointer" && controllerData.xrController.inputSource.gamepad) {
  446. // motion controller support
  447. const initMotionController = () => {
  448. if (xrController.motionController) {
  449. const movementController = xrController.motionController.getComponentOfType(WebXRControllerComponent.THUMBSTICK_TYPE) || xrController.motionController.getComponentOfType(WebXRControllerComponent.TOUCHPAD_TYPE);
  450. if (!movementController || this._options.useMainComponentOnly) {
  451. // use trigger to move on long press
  452. const mainComponent = xrController.motionController.getMainComponent();
  453. if (!mainComponent) {
  454. return;
  455. }
  456. controllerData.teleportationComponent = mainComponent;
  457. controllerData.onButtonChangedObserver = mainComponent.onButtonStateChangedObservable.add(() => {
  458. // did "pressed" changed?
  459. if (mainComponent.changes.pressed) {
  460. if (mainComponent.changes.pressed.current) {
  461. // simulate "forward" thumbstick push
  462. controllerData.teleportationState.forward = true;
  463. this._currentTeleportationControllerId = controllerData.xrController.uniqueId;
  464. controllerData.teleportationState.baseRotation = this._options.xrInput.xrCamera.rotationQuaternion.toEulerAngles().y;
  465. controllerData.teleportationState.currentRotation = 0;
  466. const timeToSelect = this._options.timeToTeleport || 3000;
  467. setAndStartTimer({
  468. timeout: timeToSelect,
  469. contextObservable: this._xrSessionManager.onXRFrameObservable,
  470. breakCondition: () => !mainComponent.pressed,
  471. onEnded: () => {
  472. if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId && controllerData.teleportationState.forward) {
  473. this._teleportForward(xrController.uniqueId);
  474. }
  475. },
  476. });
  477. } else {
  478. controllerData.teleportationState.forward = false;
  479. this._currentTeleportationControllerId = "";
  480. }
  481. }
  482. });
  483. } else {
  484. controllerData.teleportationComponent = movementController;
  485. // use thumbstick (or touchpad if thumbstick not available)
  486. controllerData.onAxisChangedObserver = movementController.onAxisValueChangedObservable.add((axesData) => {
  487. if (axesData.y <= 0.7 && controllerData.teleportationState.backwards) {
  488. controllerData.teleportationState.backwards = false;
  489. }
  490. if (axesData.y > 0.7 && !controllerData.teleportationState.forward && this.backwardsMovementEnabled && !this.snapPointsOnly) {
  491. // teleport backwards
  492. // General gist: Go Back N units, cast a ray towards the floor. If collided, move.
  493. if (!controllerData.teleportationState.backwards) {
  494. controllerData.teleportationState.backwards = true;
  495. // teleport backwards ONCE
  496. this._tmpQuaternion.copyFrom(this._options.xrInput.xrCamera.rotationQuaternion!);
  497. this._tmpQuaternion.toEulerAnglesToRef(this._tmpVector);
  498. // get only the y rotation
  499. this._tmpVector.x = 0;
  500. this._tmpVector.z = 0;
  501. // get the quaternion
  502. Quaternion.FromEulerVectorToRef(this._tmpVector, this._tmpQuaternion);
  503. this._tmpVector.set(0, 0, this.backwardsTeleportationDistance * (this._xrSessionManager.scene.useRightHandedSystem ? 1.0 : -1.0));
  504. this._tmpVector.rotateByQuaternionToRef(this._tmpQuaternion, this._tmpVector);
  505. this._tmpVector.addInPlace(this._options.xrInput.xrCamera.position);
  506. this._tmpRay.origin.copyFrom(this._tmpVector);
  507. // This will prevent the user from "falling" to a lower platform!
  508. // TODO - should this be a flag? 'allow falling to lower platforms'?
  509. this._tmpRay.length = this._options.xrInput.xrCamera.realWorldHeight + 0.1;
  510. // Right handed system had here "1" instead of -1. This is unneeded.
  511. this._tmpRay.direction.set(0, -1, 0);
  512. let pick = this._xrSessionManager.scene.pickWithRay(this._tmpRay, (o) => {
  513. return this._floorMeshes.indexOf(o) !== -1;
  514. });
  515. // pick must exist, but stay safe
  516. if (pick && pick.pickedPoint) {
  517. // Teleport the users feet to where they targeted. Ignore the Y axis.
  518. // If the "falling to lower platforms" feature is implemented the Y axis should be set here as well
  519. this._options.xrInput.xrCamera.position.x = pick.pickedPoint.x;
  520. this._options.xrInput.xrCamera.position.z = pick.pickedPoint.z;
  521. }
  522. }
  523. }
  524. if (axesData.y < -0.7 && !this._currentTeleportationControllerId && !controllerData.teleportationState.rotating) {
  525. controllerData.teleportationState.forward = true;
  526. this._currentTeleportationControllerId = controllerData.xrController.uniqueId;
  527. controllerData.teleportationState.baseRotation = this._options.xrInput.xrCamera.rotationQuaternion.toEulerAngles().y;
  528. }
  529. if (axesData.x) {
  530. if (!controllerData.teleportationState.forward) {
  531. if (!controllerData.teleportationState.rotating && Math.abs(axesData.x) > 0.7) {
  532. // rotate in the right direction positive is right
  533. controllerData.teleportationState.rotating = true;
  534. const rotation = this.rotationAngle * (axesData.x > 0 ? 1 : -1) * (this._xrSessionManager.scene.useRightHandedSystem ? -1 : 1);
  535. this._options.xrInput.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, rotation, 0));
  536. }
  537. } else {
  538. if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId) {
  539. // set the rotation of the forward movement
  540. if (this.rotationEnabled) {
  541. setTimeout(() => {
  542. controllerData.teleportationState.currentRotation = Math.atan2(axesData.x, axesData.y * (this._xrSessionManager.scene.useRightHandedSystem ? 1 : -1));
  543. });
  544. } else {
  545. controllerData.teleportationState.currentRotation = 0;
  546. }
  547. }
  548. }
  549. } else {
  550. controllerData.teleportationState.rotating = false;
  551. }
  552. if (axesData.x === 0 && axesData.y === 0) {
  553. if (controllerData.teleportationState.forward) {
  554. this._teleportForward(xrController.uniqueId);
  555. }
  556. }
  557. });
  558. }
  559. }
  560. };
  561. if (xrController.motionController) {
  562. initMotionController();
  563. } else {
  564. xrController.onMotionControllerInitObservable.addOnce(() => {
  565. initMotionController();
  566. });
  567. }
  568. } else {
  569. this._xrSessionManager.scene.onPointerObservable.add((pointerInfo) => {
  570. if (pointerInfo.type === PointerEventTypes.POINTERDOWN) {
  571. controllerData.teleportationState.forward = true;
  572. this._currentTeleportationControllerId = controllerData.xrController.uniqueId;
  573. controllerData.teleportationState.baseRotation = this._options.xrInput.xrCamera.rotationQuaternion.toEulerAngles().y;
  574. controllerData.teleportationState.currentRotation = 0;
  575. const timeToSelect = this._options.timeToTeleport || 3000;
  576. setAndStartTimer({
  577. timeout: timeToSelect,
  578. contextObservable: this._xrSessionManager.onXRFrameObservable,
  579. onEnded: () => {
  580. if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId && controllerData.teleportationState.forward) {
  581. this._teleportForward(xrController.uniqueId);
  582. }
  583. },
  584. });
  585. } else if (pointerInfo.type === PointerEventTypes.POINTERUP) {
  586. controllerData.teleportationState.forward = false;
  587. this._currentTeleportationControllerId = "";
  588. }
  589. });
  590. }
  591. };
  592. private _createDefaultTargetMesh() {
  593. // set defaults
  594. this._options.defaultTargetMeshOptions = this._options.defaultTargetMeshOptions || {};
  595. const sceneToRenderTo = this._options.useUtilityLayer ? this._options.customUtilityLayerScene || UtilityLayerRenderer.DefaultUtilityLayer.utilityLayerScene : this._xrSessionManager.scene;
  596. const teleportationTarget = GroundBuilder.CreateGround("teleportationTarget", { width: 2, height: 2, subdivisions: 2 }, sceneToRenderTo);
  597. teleportationTarget.isPickable = false;
  598. const length = 512;
  599. const dynamicTexture = new DynamicTexture("teleportationPlaneDynamicTexture", length, sceneToRenderTo, true);
  600. dynamicTexture.hasAlpha = true;
  601. const context = dynamicTexture.getContext();
  602. const centerX = length / 2;
  603. const centerY = length / 2;
  604. const radius = 200;
  605. context.beginPath();
  606. context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
  607. context.fillStyle = this._options.defaultTargetMeshOptions.teleportationFillColor || "#444444";
  608. context.fill();
  609. context.lineWidth = 10;
  610. context.strokeStyle = this._options.defaultTargetMeshOptions.teleportationBorderColor || "#FFFFFF";
  611. context.stroke();
  612. context.closePath();
  613. dynamicTexture.update();
  614. const teleportationCircleMaterial = new StandardMaterial("teleportationPlaneMaterial", sceneToRenderTo);
  615. teleportationCircleMaterial.diffuseTexture = dynamicTexture;
  616. teleportationTarget.material = teleportationCircleMaterial;
  617. const torus = TorusBuilder.CreateTorus(
  618. "torusTeleportation",
  619. {
  620. diameter: 0.75,
  621. thickness: 0.1,
  622. tessellation: 20,
  623. },
  624. sceneToRenderTo
  625. );
  626. torus.isPickable = false;
  627. torus.parent = teleportationTarget;
  628. if (!this._options.defaultTargetMeshOptions.disableAnimation) {
  629. const animationInnerCircle = new Animation("animationInnerCircle", "position.y", 30, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CYCLE);
  630. const keys = [];
  631. keys.push({
  632. frame: 0,
  633. value: 0,
  634. });
  635. keys.push({
  636. frame: 30,
  637. value: 0.4,
  638. });
  639. keys.push({
  640. frame: 60,
  641. value: 0,
  642. });
  643. animationInnerCircle.setKeys(keys);
  644. const easingFunction = new SineEase();
  645. easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);
  646. animationInnerCircle.setEasingFunction(easingFunction);
  647. torus.animations = [];
  648. torus.animations.push(animationInnerCircle);
  649. sceneToRenderTo.beginAnimation(torus, 0, 60, true);
  650. }
  651. const cone = CylinderBuilder.CreateCylinder("rotationCone", { diameterTop: 0, tessellation: 4 }, sceneToRenderTo);
  652. cone.isPickable = false;
  653. cone.scaling.set(0.5, 0.12, 0.2);
  654. cone.rotate(Axis.X, Math.PI / 2);
  655. cone.position.z = 0.6;
  656. cone.parent = torus;
  657. if (this._options.defaultTargetMeshOptions.torusArrowMaterial) {
  658. torus.material = this._options.defaultTargetMeshOptions.torusArrowMaterial;
  659. cone.material = this._options.defaultTargetMeshOptions.torusArrowMaterial;
  660. } else {
  661. const torusConeMaterial = new StandardMaterial("torusConsMat", sceneToRenderTo);
  662. torusConeMaterial.disableLighting = !!this._options.defaultTargetMeshOptions.disableLighting;
  663. if (torusConeMaterial.disableLighting) {
  664. torusConeMaterial.emissiveColor = new Color3(0.3, 0.3, 1.0);
  665. } else {
  666. torusConeMaterial.diffuseColor = new Color3(0.3, 0.3, 1.0);
  667. }
  668. torusConeMaterial.alpha = 0.9;
  669. torus.material = torusConeMaterial;
  670. cone.material = torusConeMaterial;
  671. this._teleportationRingMaterial = torusConeMaterial;
  672. }
  673. if (this._options.renderingGroupId !== undefined) {
  674. teleportationTarget.renderingGroupId = this._options.renderingGroupId;
  675. torus.renderingGroupId = this._options.renderingGroupId;
  676. cone.renderingGroupId = this._options.renderingGroupId;
  677. }
  678. this._options.teleportationTargetMesh = teleportationTarget;
  679. }
  680. private _detachController(xrControllerUniqueId: string) {
  681. const controllerData = this._controllers[xrControllerUniqueId];
  682. if (!controllerData) {
  683. return;
  684. }
  685. if (controllerData.teleportationComponent) {
  686. if (controllerData.onAxisChangedObserver) {
  687. controllerData.teleportationComponent.onAxisValueChangedObservable.remove(controllerData.onAxisChangedObserver);
  688. }
  689. if (controllerData.onButtonChangedObserver) {
  690. controllerData.teleportationComponent.onButtonStateChangedObservable.remove(controllerData.onButtonChangedObserver);
  691. }
  692. }
  693. // remove from the map
  694. delete this._controllers[xrControllerUniqueId];
  695. }
  696. private _findClosestSnapPointWithRadius(realPosition: Vector3, radius: number = this._options.snapToPositionRadius || 0.8) {
  697. let closestPoint: Nullable<Vector3> = null;
  698. let closestDistance = Number.MAX_VALUE;
  699. if (this._snapToPositions.length) {
  700. const radiusSquared = radius * radius;
  701. this._snapToPositions.forEach((position) => {
  702. const dist = Vector3.DistanceSquared(position, realPosition);
  703. if (dist <= radiusSquared && dist < closestDistance) {
  704. closestDistance = dist;
  705. closestPoint = position;
  706. }
  707. });
  708. }
  709. return closestPoint;
  710. }
  711. private _setTargetMeshPosition(newPosition: Vector3) {
  712. if (!this._options.teleportationTargetMesh) {
  713. return;
  714. }
  715. const snapPosition = this._findClosestSnapPointWithRadius(newPosition);
  716. this._snappedToPoint = !!snapPosition;
  717. if (this.snapPointsOnly && !this._snappedToPoint && this._teleportationRingMaterial) {
  718. this._teleportationRingMaterial.diffuseColor.set(1.0, 0.3, 0.3);
  719. } else if (this.snapPointsOnly && this._snappedToPoint && this._teleportationRingMaterial) {
  720. this._teleportationRingMaterial.diffuseColor.set(0.3, 0.3, 1.0);
  721. }
  722. this._options.teleportationTargetMesh.position.copyFrom(snapPosition || newPosition);
  723. this._options.teleportationTargetMesh.position.y += 0.01;
  724. }
  725. private _setTargetMeshVisibility(visible: boolean) {
  726. if (!this._options.teleportationTargetMesh) {
  727. return;
  728. }
  729. if (this._options.teleportationTargetMesh.isVisible === visible) {
  730. return;
  731. }
  732. this._options.teleportationTargetMesh.isVisible = visible;
  733. this._options.teleportationTargetMesh.getChildren(undefined, false).forEach((m) => {
  734. (<any>m).isVisible = visible;
  735. });
  736. if (!visible) {
  737. if (this._quadraticBezierCurve) {
  738. this._quadraticBezierCurve.dispose();
  739. this._quadraticBezierCurve = null;
  740. }
  741. if (this._selectionFeature) {
  742. this._selectionFeature.attach();
  743. }
  744. } else {
  745. if (this._selectionFeature) {
  746. this._selectionFeature.detach();
  747. }
  748. }
  749. }
  750. private _showParabolicPath(pickInfo: PickingInfo) {
  751. if (!pickInfo.pickedPoint) {
  752. return;
  753. }
  754. const controllerData = this._controllers[this._currentTeleportationControllerId];
  755. const quadraticBezierVectors = Curve3.CreateQuadraticBezier(controllerData.xrController.pointer.absolutePosition, pickInfo.ray!.origin, pickInfo.pickedPoint, 25);
  756. if (!this._options.generateRayPathMesh) {
  757. this._quadraticBezierCurve = LinesBuilder.CreateLines("teleportation path line", { points: quadraticBezierVectors.getPoints(), instance: this._quadraticBezierCurve as LinesMesh, updatable: true });
  758. } else {
  759. this._quadraticBezierCurve = this._options.generateRayPathMesh(quadraticBezierVectors.getPoints());
  760. }
  761. this._quadraticBezierCurve.isPickable = false;
  762. }
  763. private _teleportForward(controllerId: string) {
  764. const controllerData = this._controllers[controllerId];
  765. if (!controllerData || !controllerData.teleportationState.forward) {
  766. return;
  767. }
  768. controllerData.teleportationState.forward = false;
  769. this._currentTeleportationControllerId = "";
  770. if (this.snapPointsOnly && !this._snappedToPoint) {
  771. return;
  772. }
  773. // do the movement forward here
  774. if (this._options.teleportationTargetMesh && this._options.teleportationTargetMesh.isVisible) {
  775. const height = this._options.xrInput.xrCamera.realWorldHeight;
  776. this._options.xrInput.xrCamera.onBeforeCameraTeleport.notifyObservers(this._options.xrInput.xrCamera.position);
  777. this._options.xrInput.xrCamera.position.copyFrom(this._options.teleportationTargetMesh.position);
  778. this._options.xrInput.xrCamera.position.y += height;
  779. this._options.xrInput.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, controllerData.teleportationState.currentRotation - (this._xrSessionManager.scene.useRightHandedSystem ? Math.PI : 0), 0));
  780. this._options.xrInput.xrCamera.onAfterCameraTeleport.notifyObservers(this._options.xrInput.xrCamera.position);
  781. }
  782. }
  783. }
  784. WebXRFeaturesManager.AddWebXRFeature(
  785. WebXRMotionControllerTeleportation.Name,
  786. (xrSessionManager, options) => {
  787. return () => new WebXRMotionControllerTeleportation(xrSessionManager, options);
  788. },
  789. WebXRMotionControllerTeleportation.Version,
  790. true
  791. );