WebXRControllerTeleportation.ts 33 KB

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