WebXRControllerTeleportation.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  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 { WebXRController } from '../webXRController';
  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. /**
  25. * The options container for the teleportation module
  26. */
  27. export interface IWebXRTeleportationOptions {
  28. /**
  29. * Babylon XR Input class for controller
  30. */
  31. xrInput: WebXRInput;
  32. /**
  33. * A list of meshes to use as floor meshes.
  34. * Meshes can be added and removed after initializing the feature using the
  35. * addFloorMesh and removeFloorMesh functions
  36. * If empty, rotation will still work
  37. */
  38. floorMeshes?: AbstractMesh[];
  39. /**
  40. * Provide your own teleportation mesh instead of babylon's wonderful doughnut.
  41. * If you want to support rotation, make sure your mesh has a direction indicator.
  42. *
  43. * When left untouched, the default mesh will be initialized.
  44. */
  45. teleportationTargetMesh?: AbstractMesh;
  46. /**
  47. * Values to configure the default target mesh
  48. */
  49. defaultTargetMeshOptions?: {
  50. /**
  51. * Fill color of the teleportation area
  52. */
  53. teleportationFillColor?: string;
  54. /**
  55. * Border color for the teleportation area
  56. */
  57. teleportationBorderColor?: string;
  58. /**
  59. * Override the default material of the torus and arrow
  60. */
  61. torusArrowMaterial?: Material;
  62. /**
  63. * Disable the mesh's animation sequence
  64. */
  65. disableAnimation?: boolean;
  66. };
  67. /**
  68. * Disable using the thumbstick and use the main component (usuallly trigger) on long press.
  69. * This will be automatically true if the controller doesnt have a thumbstick or touchpad.
  70. */
  71. useMainComponentOnly?: boolean;
  72. /**
  73. * If main component is used (no thumbstick), how long should the "long press" take before teleporting
  74. */
  75. timeToTeleport?: number;
  76. }
  77. /**
  78. * This is a teleportation feature to be used with webxr-enabled motion controllers.
  79. * When enabled and attached, the feature will allow a user to move aroundand rotate in the scene using
  80. * the input of the attached controllers.
  81. */
  82. export class WebXRMotionControllerTeleportation extends WebXRAbstractFeature {
  83. /**
  84. * The module's name
  85. */
  86. public static readonly Name = WebXRFeatureName.TELEPORTATION;
  87. /**
  88. * The (Babylon) version of this module.
  89. * This is an integer representing the implementation version.
  90. * This number does not correspond to the webxr specs version
  91. */
  92. public static readonly Version = 1;
  93. /**
  94. * Is rotation enabled when moving forward?
  95. * Disabling this feature will prevent the user from deciding the direction when teleporting
  96. */
  97. public rotationEnabled: boolean = true;
  98. /**
  99. * Should the module support parabolic ray on top of direct ray
  100. * If enabled, the user will be able to point "at the sky" and move according to predefined radius distance
  101. * Very helpful when moving between floors / different heights
  102. */
  103. public parabolicRayEnabled: boolean = true;
  104. /**
  105. * The distance from the user to the inspection point in the direction of the controller
  106. * A higher number will allow the user to move further
  107. * defaults to 5 (meters, in xr units)
  108. */
  109. public parabolicCheckRadius: number = 5;
  110. /**
  111. * How much rotation should be applied when rotating right and left
  112. */
  113. public rotationAngle: number = Math.PI / 8;
  114. /**
  115. * Distance to travel when moving backwards
  116. */
  117. public backwardsTeleportationDistance: number = 0.5;
  118. /**
  119. * Add a new mesh to the floor meshes array
  120. * @param mesh the mesh to use as floor mesh
  121. */
  122. public addFloorMesh(mesh: AbstractMesh) {
  123. this._floorMeshes.push(mesh);
  124. }
  125. /**
  126. * Remove a mesh from the floor meshes array
  127. * @param mesh the mesh to remove
  128. */
  129. public removeFloorMesh(mesh: AbstractMesh) {
  130. const index = this._floorMeshes.indexOf(mesh);
  131. if (index !== -1) {
  132. this._floorMeshes.splice(index, 1);
  133. }
  134. }
  135. /**
  136. * Remove a mesh from the floor meshes array using its name
  137. * @param name the mesh name to remove
  138. */
  139. public removeFloorMeshByName(name: string) {
  140. const mesh = this._xrSessionManager.scene.getMeshByName(name);
  141. if (mesh) {
  142. this.removeFloorMesh(mesh);
  143. }
  144. }
  145. private _tmpRay = new Ray(new Vector3(), new Vector3());
  146. private _tmpVector = new Vector3();
  147. private _floorMeshes: AbstractMesh[];
  148. private _controllers: {
  149. [controllerUniqueId: string]: {
  150. xrController: WebXRController;
  151. teleportationComponent?: WebXRControllerComponent;
  152. teleportationState: {
  153. forward: boolean;
  154. backwards: boolean;
  155. currentRotation: number;
  156. baseRotation: number;
  157. rotating: boolean;
  158. }
  159. onAxisChangedObserver?: Nullable<Observer<IWebXRMotionControllerAxesValue>>;
  160. onButtonChangedObserver?: Nullable<Observer<WebXRControllerComponent>>;
  161. };
  162. } = {};
  163. /**
  164. * constructs a new anchor system
  165. * @param _xrSessionManager an instance of WebXRSessionManager
  166. * @param _options configuration object for this feature
  167. */
  168. constructor(_xrSessionManager: WebXRSessionManager, private _options: IWebXRTeleportationOptions) {
  169. super(_xrSessionManager);
  170. // create default mesh if not provided
  171. if (!this._options.teleportationTargetMesh) {
  172. this.createDefaultTargetMesh();
  173. }
  174. this._floorMeshes = this._options.floorMeshes || [];
  175. this.setTargetMeshVisibility(false);
  176. }
  177. private _selectionFeature: IWebXRFeature;
  178. /**
  179. * This function sets a selection feature that will be disabled when
  180. * the forward ray is shown and will be reattached when hidden.
  181. * This is used to remove the selection rays when moving.
  182. * @param selectionFeature the feature to disable when forward movement is enabled
  183. */
  184. public setSelectionFeature(selectionFeature: IWebXRFeature) {
  185. this._selectionFeature = selectionFeature;
  186. }
  187. public attach(): boolean {
  188. if (!super.attach()) {
  189. return false;
  190. }
  191. this._options.xrInput.controllers.forEach(this._attachController);
  192. this._addNewAttachObserver(this._options.xrInput.onControllerAddedObservable, this._attachController);
  193. this._addNewAttachObserver(this._options.xrInput.onControllerRemovedObservable, (controller) => {
  194. // REMOVE the controller
  195. this._detachController(controller.uniqueId);
  196. });
  197. return true;
  198. }
  199. public detach(): boolean {
  200. if (!super.detach()) {
  201. return false;
  202. }
  203. Object.keys(this._controllers).forEach((controllerId) => {
  204. this._detachController(controllerId);
  205. });
  206. this.setTargetMeshVisibility(false);
  207. return true;
  208. }
  209. public dispose(): void {
  210. super.dispose();
  211. this._options.teleportationTargetMesh && this._options.teleportationTargetMesh.dispose(false, true);
  212. }
  213. protected _onXRFrame(_xrFrame: XRFrame) {
  214. const frame = this._xrSessionManager.currentFrame;
  215. const scene = this._xrSessionManager.scene;
  216. if (!this.attach || !frame) { return; }
  217. // render target if needed
  218. const targetMesh = this._options.teleportationTargetMesh;
  219. if (this._currentTeleportationControllerId) {
  220. if (!targetMesh) {
  221. return;
  222. }
  223. targetMesh.rotationQuaternion = targetMesh.rotationQuaternion || new Quaternion();
  224. const controllerData = this._controllers[this._currentTeleportationControllerId];
  225. if (controllerData.teleportationState.forward) {
  226. // set the rotation
  227. Quaternion.RotationYawPitchRollToRef(controllerData.teleportationState.currentRotation + controllerData.teleportationState.baseRotation, 0, 0, targetMesh.rotationQuaternion);
  228. // set the ray and position
  229. let hitPossible = false;
  230. // first check if direct ray possible
  231. controllerData.xrController.getWorldPointerRayToRef(this._tmpRay);
  232. let pick = scene.pickWithRay(this._tmpRay, (o) => {
  233. return this._floorMeshes.indexOf(o) !== -1;
  234. });
  235. if (pick && pick.pickedPoint) {
  236. hitPossible = true;
  237. this.setTargetMeshPosition(pick.pickedPoint);
  238. this.setTargetMeshVisibility(true);
  239. this.showParabolicPath(pick);
  240. } else {
  241. if (this.parabolicRayEnabled) {
  242. // check parabolic ray
  243. const radius = this.parabolicCheckRadius;
  244. this._tmpRay.origin.addToRef(this._tmpRay.direction.scale(radius * 2), this._tmpVector);
  245. this._tmpVector.y = this._tmpRay.origin.y;
  246. this._tmpRay.origin.addInPlace(this._tmpRay.direction.scale(radius));
  247. this._tmpVector.subtractToRef(this._tmpRay.origin, this._tmpRay.direction);
  248. this._tmpRay.direction.normalize();
  249. let pick = scene.pickWithRay(this._tmpRay, (o) => {
  250. return this._floorMeshes.indexOf(o) !== -1;
  251. });
  252. if (pick && pick.pickedPoint) {
  253. hitPossible = true;
  254. this.setTargetMeshPosition(pick.pickedPoint);
  255. this.setTargetMeshVisibility(true);
  256. this.showParabolicPath(pick);
  257. }
  258. }
  259. }
  260. // if needed, set visible:
  261. this.setTargetMeshVisibility(hitPossible);
  262. } else {
  263. this.setTargetMeshVisibility(false);
  264. }
  265. } else {
  266. this.setTargetMeshVisibility(false);
  267. }
  268. }
  269. private _currentTeleportationControllerId: string;
  270. private _attachController = (xrController: WebXRController) => {
  271. if (this._controllers[xrController.uniqueId]) {
  272. // already attached
  273. return;
  274. }
  275. this._controllers[xrController.uniqueId] = {
  276. xrController,
  277. teleportationState: {
  278. forward: false,
  279. backwards: false,
  280. rotating: false,
  281. currentRotation: 0,
  282. baseRotation: 0
  283. }
  284. };
  285. const controllerData = this._controllers[xrController.uniqueId];
  286. // motion controller support
  287. if (xrController.motionController) {
  288. const movementController = xrController.motionController.getComponent(WebXRControllerComponent.THUMBSTICK) || xrController.motionController.getComponent(WebXRControllerComponent.TOUCHPAD);
  289. if (!movementController || this._options.useMainComponentOnly) {
  290. // use trigger to move on long press
  291. const mainComponent = xrController.motionController.getMainComponent();
  292. if (!mainComponent) {
  293. return;
  294. }
  295. controllerData.onButtonChangedObserver = mainComponent.onButtonStateChanged.add(() => {
  296. // did "pressed" changed?
  297. if (mainComponent.changes.pressed) {
  298. if (mainComponent.changes.pressed.current) {
  299. // simulate "forward" thumbstick push
  300. controllerData.teleportationState.forward = true;
  301. this._currentTeleportationControllerId = controllerData.xrController.uniqueId;
  302. controllerData.teleportationState.baseRotation = this._options.xrInput.xrCamera.rotationQuaternion.toEulerAngles().y;
  303. controllerData.teleportationState.currentRotation = 0;
  304. const timeToSelect = this._options.timeToTeleport || 3000;
  305. let timer = 0;
  306. const observer = this._xrSessionManager.onXRFrameObservable.add(() => {
  307. if (!mainComponent.pressed) {
  308. this._xrSessionManager.onXRFrameObservable.remove(observer);
  309. return;
  310. }
  311. timer += this._xrSessionManager.scene.getEngine().getDeltaTime();
  312. if (timer >= timeToSelect && this._currentTeleportationControllerId === controllerData.xrController.uniqueId && controllerData.teleportationState.forward) {
  313. this._teleportForward(xrController.uniqueId);
  314. }
  315. // failsafe
  316. if (timer >= timeToSelect) {
  317. this._xrSessionManager.onXRFrameObservable.remove(observer);
  318. }
  319. });
  320. } else {
  321. controllerData.teleportationState.forward = false;
  322. this._currentTeleportationControllerId = "";
  323. }
  324. }
  325. });
  326. } else {
  327. controllerData.onButtonChangedObserver = movementController.onButtonStateChanged.add(() => {
  328. if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId && controllerData.teleportationState.forward && !movementController.touched) {
  329. this._teleportForward(xrController.uniqueId);
  330. }
  331. });
  332. // use thumbstick (or touchpad if thumbstick not available)
  333. controllerData.onAxisChangedObserver = movementController.onAxisValueChanged.add((axesData) => {
  334. if (axesData.y <= 0.7 && controllerData.teleportationState.backwards) {
  335. //if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId) {
  336. controllerData.teleportationState.backwards = false;
  337. //this._currentTeleportationControllerId = "";
  338. //}
  339. }
  340. if (axesData.y > 0.7 && !controllerData.teleportationState.forward) {
  341. // teleport backwards
  342. if (!controllerData.teleportationState.backwards) {
  343. controllerData.teleportationState.backwards = true;
  344. // teleport backwards ONCE
  345. this._tmpVector.set(0, 0, -this.backwardsTeleportationDistance!);
  346. this._tmpVector.addInPlace(this._options.xrInput.xrCamera.position);
  347. this._tmpRay.origin.copyFrom(this._tmpVector);
  348. this._tmpRay.direction.set(0, -1, 0);
  349. let pick = this._xrSessionManager.scene.pickWithRay(this._tmpRay, (o) => {
  350. return this._floorMeshes.indexOf(o) !== -1;
  351. });
  352. // pick must exist, but stay safe
  353. if (pick && pick.pickedPoint) {
  354. // Teleport the users feet to where they targeted
  355. this._options.xrInput.xrCamera.position.addInPlace(pick.pickedPoint);
  356. }
  357. }
  358. }
  359. if (axesData.y < -0.7 && !this._currentTeleportationControllerId && !controllerData.teleportationState.rotating) {
  360. controllerData.teleportationState.forward = true;
  361. this._currentTeleportationControllerId = controllerData.xrController.uniqueId;
  362. controllerData.teleportationState.baseRotation = this._options.xrInput.xrCamera.rotationQuaternion.toEulerAngles().y;
  363. }
  364. if (axesData.x) {
  365. if (!controllerData.teleportationState.forward) {
  366. if (!controllerData.teleportationState.rotating && Math.abs(axesData.x) > 0.7) {
  367. // rotate in the right direction positive is right
  368. controllerData.teleportationState.rotating = true;
  369. const rotation = this.rotationAngle * (axesData.x > 0 ? 1 : -1);
  370. this._options.xrInput.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, rotation, 0));
  371. }
  372. } else {
  373. if (this._currentTeleportationControllerId === controllerData.xrController.uniqueId) {
  374. // set the rotation of the forward movement
  375. if (this.rotationEnabled) {
  376. setTimeout(() => {
  377. controllerData.teleportationState.currentRotation = Math.atan2(axesData.x, -axesData.y);
  378. });
  379. } else {
  380. controllerData.teleportationState.currentRotation = 0;
  381. }
  382. }
  383. }
  384. } else {
  385. controllerData.teleportationState.rotating = false;
  386. }
  387. });
  388. }
  389. }
  390. }
  391. private _teleportForward(controllerId: string) {
  392. const controllerData = this._controllers[controllerId];
  393. controllerData.teleportationState.forward = false;
  394. this._currentTeleportationControllerId = "";
  395. // do the movement forward here
  396. if (this._options.teleportationTargetMesh && this._options.teleportationTargetMesh.isVisible) {
  397. const height = this._options.xrInput.xrCamera.position.y - this._options.teleportationTargetMesh.position.y;
  398. this._options.xrInput.xrCamera.position.copyFrom(this._options.teleportationTargetMesh.position);
  399. this._options.xrInput.xrCamera.position.y += height;
  400. this._options.xrInput.xrCamera.rotationQuaternion.multiplyInPlace(Quaternion.FromEulerAngles(0, controllerData.teleportationState.currentRotation, 0));
  401. }
  402. }
  403. private _detachController(xrControllerUniqueId: string) {
  404. const controllerData = this._controllers[xrControllerUniqueId];
  405. if (!controllerData) { return; }
  406. if (controllerData.teleportationComponent) {
  407. if (controllerData.onAxisChangedObserver) {
  408. controllerData.teleportationComponent.onAxisValueChanged.remove(controllerData.onAxisChangedObserver);
  409. }
  410. if (controllerData.onButtonChangedObserver) {
  411. controllerData.teleportationComponent.onButtonStateChanged.remove(controllerData.onButtonChangedObserver);
  412. }
  413. }
  414. // remove from the map
  415. delete this._controllers[xrControllerUniqueId];
  416. }
  417. private createDefaultTargetMesh() {
  418. // set defaults
  419. this._options.defaultTargetMeshOptions = this._options.defaultTargetMeshOptions || {};
  420. const scene = this._xrSessionManager.scene;
  421. let teleportationTarget = GroundBuilder.CreateGround("teleportationTarget", { width: 2, height: 2, subdivisions: 2 }, scene);
  422. teleportationTarget.isPickable = false;
  423. let length = 512;
  424. let dynamicTexture = new DynamicTexture("DynamicTexture", length, scene, true);
  425. dynamicTexture.hasAlpha = true;
  426. let context = dynamicTexture.getContext();
  427. let centerX = length / 2;
  428. let centerY = length / 2;
  429. let radius = 200;
  430. context.beginPath();
  431. context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
  432. context.fillStyle = this._options.defaultTargetMeshOptions.teleportationFillColor || "#444444";
  433. context.fill();
  434. context.lineWidth = 10;
  435. context.strokeStyle = this._options.defaultTargetMeshOptions.teleportationBorderColor || "#FFFFFF";
  436. context.stroke();
  437. context.closePath();
  438. dynamicTexture.update();
  439. let teleportationCircleMaterial = new StandardMaterial("TextPlaneMaterial", scene);
  440. teleportationCircleMaterial.diffuseTexture = dynamicTexture;
  441. teleportationTarget.material = teleportationCircleMaterial;
  442. let torus = TorusBuilder.CreateTorus("torusTeleportation", {
  443. diameter: 0.75,
  444. thickness: 0.1,
  445. tessellation: 20
  446. }, scene);
  447. torus.isPickable = false;
  448. torus.parent = teleportationTarget;
  449. if (!this._options.defaultTargetMeshOptions.disableAnimation) {
  450. let animationInnerCircle = new Animation("animationInnerCircle", "position.y", 30, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CYCLE);
  451. let keys = [];
  452. keys.push({
  453. frame: 0,
  454. value: 0
  455. });
  456. keys.push({
  457. frame: 30,
  458. value: 0.4
  459. });
  460. keys.push({
  461. frame: 60,
  462. value: 0
  463. });
  464. animationInnerCircle.setKeys(keys);
  465. let easingFunction = new SineEase();
  466. easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);
  467. animationInnerCircle.setEasingFunction(easingFunction);
  468. torus.animations = [];
  469. torus.animations.push(animationInnerCircle);
  470. scene.beginAnimation(torus, 0, 60, true);
  471. }
  472. var cone = CylinderBuilder.CreateCylinder("cone", { diameterTop: 0, tessellation: 4 }, scene);
  473. cone.isPickable = false;
  474. cone.scaling.set(0.5, 0.12, 0.2);
  475. cone.rotate(Axis.X, Math.PI / 2);
  476. cone.position.z = 0.6;
  477. cone.parent = torus;
  478. if (this._options.defaultTargetMeshOptions.torusArrowMaterial) {
  479. torus.material = this._options.defaultTargetMeshOptions.torusArrowMaterial;
  480. cone.material = this._options.defaultTargetMeshOptions.torusArrowMaterial;
  481. }
  482. this._options.teleportationTargetMesh = teleportationTarget;
  483. }
  484. private setTargetMeshVisibility(visible: boolean) {
  485. if (!this._options.teleportationTargetMesh) { return; }
  486. if (this._options.teleportationTargetMesh.isVisible === visible) { return; }
  487. this._options.teleportationTargetMesh.isVisible = visible;
  488. this._options.teleportationTargetMesh.getChildren(undefined, false).forEach((m) => { (<any>(m)).isVisible = visible; });
  489. if (!visible) {
  490. if (this._quadraticBezierCurve) {
  491. this._quadraticBezierCurve.dispose();
  492. }
  493. if (this._selectionFeature) {
  494. this._selectionFeature.attach();
  495. }
  496. } else {
  497. if (this._selectionFeature) {
  498. this._selectionFeature.detach();
  499. }
  500. }
  501. }
  502. private setTargetMeshPosition(newPosition: Vector3) {
  503. if (!this._options.teleportationTargetMesh) { return; }
  504. this._options.teleportationTargetMesh.position.copyFrom(newPosition);
  505. this._options.teleportationTargetMesh.position.y += 0.01;
  506. }
  507. private _quadraticBezierCurve: AbstractMesh;
  508. private showParabolicPath(pickInfo: PickingInfo) {
  509. if (!pickInfo.pickedPoint) { return; }
  510. const controllerData = this._controllers[this._currentTeleportationControllerId];
  511. const quadraticBezierVectors = Curve3.CreateQuadraticBezier(
  512. controllerData.xrController.pointer.absolutePosition,
  513. pickInfo.ray!.origin,
  514. pickInfo.pickedPoint,
  515. 25);
  516. if (this._quadraticBezierCurve) {
  517. this._quadraticBezierCurve.dispose();
  518. }
  519. this._quadraticBezierCurve = LinesBuilder.CreateLines("path line", { points: quadraticBezierVectors.getPoints() });
  520. this._quadraticBezierCurve.isPickable = false;
  521. }
  522. }
  523. WebXRFeaturesManager.AddWebXRFeature(WebXRMotionControllerTeleportation.Name, (xrSessionManager, options) => {
  524. return () => new WebXRMotionControllerTeleportation(xrSessionManager, options);
  525. }, WebXRMotionControllerTeleportation.Version, true);