WebXRWalkingLocomotion.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import { TmpVectors, Vector2, Vector3 } from "../../Maths/math.vector.js";
  2. import { Logger } from "../../Misc/logger.js";
  3. import { Observable } from "../../Misc/observable.js";
  4. import { WebXRFeatureName, WebXRFeaturesManager } from "../webXRFeaturesManager.js";
  5. import { WebXRAbstractFeature } from "./WebXRAbstractFeature.js";
  6. class CircleBuffer {
  7. constructor(numSamples, initializer) {
  8. this._samples = [];
  9. this._idx = 0;
  10. for (let idx = 0; idx < numSamples; ++idx) {
  11. this._samples.push(initializer ? initializer() : Vector2.Zero());
  12. }
  13. }
  14. get length() {
  15. return this._samples.length;
  16. }
  17. push(x, y) {
  18. this._idx = (this._idx + this._samples.length - 1) % this._samples.length;
  19. this.at(0).copyFromFloats(x, y);
  20. }
  21. at(idx) {
  22. if (idx >= this._samples.length) {
  23. throw new Error("Index out of bounds");
  24. }
  25. return this._samples[(this._idx + idx) % this._samples.length];
  26. }
  27. }
  28. class FirstStepDetector {
  29. constructor() {
  30. this._samples = new CircleBuffer(20);
  31. this._entropy = 0;
  32. this.onFirstStepDetected = new Observable();
  33. }
  34. update(posX, posY, forwardX, forwardY) {
  35. this._samples.push(posX, posY);
  36. const origin = this._samples.at(0);
  37. this._entropy *= this._entropyDecayFactor;
  38. this._entropy += Vector2.Distance(origin, this._samples.at(1));
  39. if (this._entropy > this._entropyThreshold) {
  40. return;
  41. }
  42. let samePointIdx;
  43. for (samePointIdx = this._samePointCheckStartIdx; samePointIdx < this._samples.length; ++samePointIdx) {
  44. if (Vector2.DistanceSquared(origin, this._samples.at(samePointIdx)) < this._samePointSquaredDistanceThreshold) {
  45. break;
  46. }
  47. }
  48. if (samePointIdx === this._samples.length) {
  49. return;
  50. }
  51. let apexDistSquared = -1;
  52. let apexIdx = 0;
  53. for (let distSquared, idx = 1; idx < samePointIdx; ++idx) {
  54. distSquared = Vector2.DistanceSquared(origin, this._samples.at(idx));
  55. if (distSquared > apexDistSquared) {
  56. apexIdx = idx;
  57. apexDistSquared = distSquared;
  58. }
  59. }
  60. if (apexDistSquared < this._apexSquaredDistanceThreshold) {
  61. return;
  62. }
  63. const apex = this._samples.at(apexIdx);
  64. const axis = apex.subtract(origin);
  65. axis.normalize();
  66. const vec = TmpVectors.Vector2[0];
  67. let dot;
  68. let sample;
  69. let sumSquaredProjectionDistances = 0;
  70. for (let idx = 1; idx < samePointIdx; ++idx) {
  71. sample = this._samples.at(idx);
  72. sample.subtractToRef(origin, vec);
  73. dot = Vector2.Dot(axis, vec);
  74. sumSquaredProjectionDistances += vec.lengthSquared() - dot * dot;
  75. }
  76. if (sumSquaredProjectionDistances > samePointIdx * this._squaredProjectionDistanceThreshold) {
  77. return;
  78. }
  79. const forwardVec = TmpVectors.Vector3[0];
  80. forwardVec.set(forwardX, forwardY, 0);
  81. const axisVec = TmpVectors.Vector3[1];
  82. axisVec.set(axis.x, axis.y, 0);
  83. const isApexLeft = Vector3.Cross(forwardVec, axisVec).z > 0;
  84. const leftApex = origin.clone();
  85. const rightApex = origin.clone();
  86. apex.subtractToRef(origin, axis);
  87. if (isApexLeft) {
  88. axis.scaleAndAddToRef(this._axisToApexShrinkFactor, leftApex);
  89. axis.scaleAndAddToRef(this._axisToApexExtendFactor, rightApex);
  90. }
  91. else {
  92. axis.scaleAndAddToRef(this._axisToApexExtendFactor, leftApex);
  93. axis.scaleAndAddToRef(this._axisToApexShrinkFactor, rightApex);
  94. }
  95. this.onFirstStepDetected.notifyObservers({
  96. leftApex: leftApex,
  97. rightApex: rightApex,
  98. currentPosition: origin,
  99. currentStepDirection: isApexLeft ? "right" : "left",
  100. });
  101. }
  102. reset() {
  103. for (let idx = 0; idx < this._samples.length; ++idx) {
  104. this._samples.at(idx).copyFromFloats(0, 0);
  105. }
  106. }
  107. get _samePointCheckStartIdx() {
  108. return Math.floor(this._samples.length / 3);
  109. }
  110. get _samePointSquaredDistanceThreshold() {
  111. return 0.03 * 0.03;
  112. }
  113. get _apexSquaredDistanceThreshold() {
  114. return 0.09 * 0.09;
  115. }
  116. get _squaredProjectionDistanceThreshold() {
  117. return 0.03 * 0.03;
  118. }
  119. get _axisToApexShrinkFactor() {
  120. return 0.8;
  121. }
  122. get _axisToApexExtendFactor() {
  123. return -1.6;
  124. }
  125. get _entropyDecayFactor() {
  126. return 0.93;
  127. }
  128. get _entropyThreshold() {
  129. return 0.4;
  130. }
  131. }
  132. class WalkingTracker {
  133. constructor(leftApex, rightApex, currentPosition, currentStepDirection) {
  134. this._leftApex = new Vector2();
  135. this._rightApex = new Vector2();
  136. this._currentPosition = new Vector2();
  137. this._axis = new Vector2();
  138. this._axisLength = -1;
  139. this._forward = new Vector2();
  140. this._steppingLeft = false;
  141. this._t = -1;
  142. this._maxT = -1;
  143. this._maxTPosition = new Vector2();
  144. this._vitality = 0;
  145. this.onMovement = new Observable();
  146. this.onFootfall = new Observable();
  147. this._reset(leftApex, rightApex, currentPosition, currentStepDirection === "left");
  148. }
  149. _reset(leftApex, rightApex, currentPosition, steppingLeft) {
  150. this._leftApex.copyFrom(leftApex);
  151. this._rightApex.copyFrom(rightApex);
  152. this._steppingLeft = steppingLeft;
  153. if (this._steppingLeft) {
  154. this._leftApex.subtractToRef(this._rightApex, this._axis);
  155. this._forward.copyFromFloats(-this._axis.y, this._axis.x);
  156. }
  157. else {
  158. this._rightApex.subtractToRef(this._leftApex, this._axis);
  159. this._forward.copyFromFloats(this._axis.y, -this._axis.x);
  160. }
  161. this._axisLength = this._axis.length();
  162. this._forward.scaleInPlace(1 / this._axisLength);
  163. this._updateTAndVitality(currentPosition.x, currentPosition.y);
  164. this._maxT = this._t;
  165. this._maxTPosition.copyFrom(currentPosition);
  166. this._vitality = 1;
  167. }
  168. _updateTAndVitality(x, y) {
  169. this._currentPosition.copyFromFloats(x, y);
  170. if (this._steppingLeft) {
  171. this._currentPosition.subtractInPlace(this._rightApex);
  172. }
  173. else {
  174. this._currentPosition.subtractInPlace(this._leftApex);
  175. }
  176. const priorT = this._t;
  177. const dot = Vector2.Dot(this._currentPosition, this._axis);
  178. this._t = dot / (this._axisLength * this._axisLength);
  179. const projDistSquared = this._currentPosition.lengthSquared() - (dot / this._axisLength) * (dot / this._axisLength);
  180. // TODO: Extricate the magic.
  181. this._vitality *= 0.92 - 100 * Math.max(projDistSquared - 0.0016, 0) + Math.max(this._t - priorT, 0);
  182. }
  183. update(x, y) {
  184. if (this._vitality < this._vitalityThreshold) {
  185. return false;
  186. }
  187. const priorT = this._t;
  188. this._updateTAndVitality(x, y);
  189. if (this._t > this._maxT) {
  190. this._maxT = this._t;
  191. this._maxTPosition.copyFromFloats(x, y);
  192. }
  193. if (this._vitality < this._vitalityThreshold) {
  194. return false;
  195. }
  196. if (this._t > priorT) {
  197. this.onMovement.notifyObservers({ deltaT: this._t - priorT });
  198. if (priorT < 0.5 && this._t >= 0.5) {
  199. this.onFootfall.notifyObservers({ foot: this._steppingLeft ? "left" : "right" });
  200. }
  201. }
  202. if (this._t < 0.95 * this._maxT) {
  203. this._currentPosition.copyFromFloats(x, y);
  204. if (this._steppingLeft) {
  205. this._leftApex.copyFrom(this._maxTPosition);
  206. }
  207. else {
  208. this._rightApex.copyFrom(this._maxTPosition);
  209. }
  210. this._reset(this._leftApex, this._rightApex, this._currentPosition, !this._steppingLeft);
  211. }
  212. if (this._axisLength < 0.03) {
  213. return false;
  214. }
  215. return true;
  216. }
  217. get _vitalityThreshold() {
  218. return 0.1;
  219. }
  220. get forward() {
  221. return this._forward;
  222. }
  223. }
  224. class Walker {
  225. static get _MillisecondsPerUpdate() {
  226. // 15 FPS
  227. return 1000 / 15;
  228. }
  229. constructor(engine) {
  230. this._detector = new FirstStepDetector();
  231. this._walker = null;
  232. this._movement = new Vector2();
  233. this._millisecondsSinceLastUpdate = Walker._MillisecondsPerUpdate;
  234. this.movementThisFrame = Vector3.Zero();
  235. this._engine = engine;
  236. this._detector.onFirstStepDetected.add((event) => {
  237. if (!this._walker) {
  238. this._walker = new WalkingTracker(event.leftApex, event.rightApex, event.currentPosition, event.currentStepDirection);
  239. this._walker.onFootfall.add(() => {
  240. Logger.Log("Footfall!");
  241. });
  242. this._walker.onMovement.add((event) => {
  243. this._walker.forward.scaleAndAddToRef(0.024 * event.deltaT, this._movement);
  244. });
  245. }
  246. });
  247. }
  248. update(position, forward) {
  249. forward.y = 0;
  250. forward.normalize();
  251. // Enforce reduced framerate
  252. this._millisecondsSinceLastUpdate += this._engine.getDeltaTime();
  253. if (this._millisecondsSinceLastUpdate >= Walker._MillisecondsPerUpdate) {
  254. this._millisecondsSinceLastUpdate -= Walker._MillisecondsPerUpdate;
  255. this._detector.update(position.x, position.z, forward.x, forward.z);
  256. if (this._walker) {
  257. const updated = this._walker.update(position.x, position.z);
  258. if (!updated) {
  259. this._walker = null;
  260. }
  261. }
  262. this._movement.scaleInPlace(0.85);
  263. }
  264. this.movementThisFrame.set(this._movement.x, 0, this._movement.y);
  265. }
  266. }
  267. /**
  268. * A module that will enable VR locomotion by detecting when the user walks in place.
  269. */
  270. export class WebXRWalkingLocomotion extends WebXRAbstractFeature {
  271. /**
  272. * The module's name.
  273. */
  274. static get Name() {
  275. return WebXRFeatureName.WALKING_LOCOMOTION;
  276. }
  277. /**
  278. * The (Babylon) version of this module.
  279. * This is an integer representing the implementation version.
  280. * This number has no external basis.
  281. */
  282. static get Version() {
  283. return 1;
  284. }
  285. /**
  286. * The target to be articulated by walking locomotion.
  287. * When the walking locomotion feature detects walking in place, this element's
  288. * X and Z coordinates will be modified to reflect locomotion. This target should
  289. * be either the XR space's origin (i.e., the parent node of the WebXRCamera) or
  290. * the WebXRCamera itself. Note that the WebXRCamera path will modify the position
  291. * of the WebXRCamera directly and is thus discouraged.
  292. */
  293. get locomotionTarget() {
  294. return this._locomotionTarget;
  295. }
  296. /**
  297. * The target to be articulated by walking locomotion.
  298. * When the walking locomotion feature detects walking in place, this element's
  299. * X and Z coordinates will be modified to reflect locomotion. This target should
  300. * be either the XR space's origin (i.e., the parent node of the WebXRCamera) or
  301. * the WebXRCamera itself. Note that the WebXRCamera path will modify the position
  302. * of the WebXRCamera directly and is thus discouraged.
  303. */
  304. set locomotionTarget(locomotionTarget) {
  305. this._locomotionTarget = locomotionTarget;
  306. this._isLocomotionTargetWebXRCamera = this._locomotionTarget.getClassName() === "WebXRCamera";
  307. }
  308. /**
  309. * Construct a new Walking Locomotion feature.
  310. * @param sessionManager manager for the current XR session
  311. * @param options creation options, prominently including the vector target for locomotion
  312. */
  313. constructor(sessionManager, options) {
  314. super(sessionManager);
  315. this._up = new Vector3();
  316. this._forward = new Vector3();
  317. this._position = new Vector3();
  318. this._movement = new Vector3();
  319. this._sessionManager = sessionManager;
  320. this.locomotionTarget = options.locomotionTarget;
  321. if (this._isLocomotionTargetWebXRCamera) {
  322. Logger.Warn("Using walking locomotion directly on a WebXRCamera may have unintended interactions with other XR techniques. Using an XR space parent is highly recommended");
  323. }
  324. }
  325. /**
  326. * Checks whether this feature is compatible with the current WebXR session.
  327. * Walking locomotion is only compatible with "immersive-vr" sessions.
  328. * @returns true if compatible, false otherwise
  329. */
  330. isCompatible() {
  331. return this._sessionManager.sessionMode === undefined || this._sessionManager.sessionMode === "immersive-vr";
  332. }
  333. /**
  334. * Attaches the feature.
  335. * Typically called automatically by the features manager.
  336. * @returns true if attach succeeded, false otherwise
  337. */
  338. attach() {
  339. if (!this.isCompatible || !super.attach()) {
  340. return false;
  341. }
  342. this._walker = new Walker(this._sessionManager.scene.getEngine());
  343. return true;
  344. }
  345. /**
  346. * Detaches the feature.
  347. * Typically called automatically by the features manager.
  348. * @returns true if detach succeeded, false otherwise
  349. */
  350. detach() {
  351. if (!super.detach()) {
  352. return false;
  353. }
  354. this._walker = null;
  355. return true;
  356. }
  357. _onXRFrame(frame) {
  358. const pose = frame.getViewerPose(this._sessionManager.baseReferenceSpace);
  359. if (!pose) {
  360. return;
  361. }
  362. const handednessScalar = this.locomotionTarget.getScene().useRightHandedSystem ? 1 : -1;
  363. const m = pose.transform.matrix;
  364. this._up.copyFromFloats(m[4], m[5], handednessScalar * m[6]);
  365. this._forward.copyFromFloats(m[8], m[9], handednessScalar * m[10]);
  366. this._position.copyFromFloats(m[12], m[13], handednessScalar * m[14]);
  367. // Compute the nape position
  368. this._forward.scaleAndAddToRef(0.05, this._position);
  369. this._up.scaleAndAddToRef(-0.05, this._position);
  370. this._walker.update(this._position, this._forward);
  371. this._movement.copyFrom(this._walker.movementThisFrame);
  372. if (!this._isLocomotionTargetWebXRCamera) {
  373. Vector3.TransformNormalToRef(this._movement, this.locomotionTarget.getWorldMatrix(), this._movement);
  374. }
  375. this.locomotionTarget.position.addInPlace(this._movement);
  376. }
  377. }
  378. //register the plugin
  379. WebXRFeaturesManager.AddWebXRFeature(WebXRWalkingLocomotion.Name, (xrSessionManager, options) => {
  380. return () => new WebXRWalkingLocomotion(xrSessionManager, options);
  381. }, WebXRWalkingLocomotion.Version, false);
  382. //# sourceMappingURL=WebXRWalkingLocomotion.js.map