123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- import { TmpVectors, Vector2, Vector3 } from "../../Maths/math.vector.js";
- import { Logger } from "../../Misc/logger.js";
- import { Observable } from "../../Misc/observable.js";
- import { WebXRFeatureName, WebXRFeaturesManager } from "../webXRFeaturesManager.js";
- import { WebXRAbstractFeature } from "./WebXRAbstractFeature.js";
- class CircleBuffer {
- constructor(numSamples, initializer) {
- this._samples = [];
- this._idx = 0;
- for (let idx = 0; idx < numSamples; ++idx) {
- this._samples.push(initializer ? initializer() : Vector2.Zero());
- }
- }
- get length() {
- return this._samples.length;
- }
- push(x, y) {
- this._idx = (this._idx + this._samples.length - 1) % this._samples.length;
- this.at(0).copyFromFloats(x, y);
- }
- at(idx) {
- if (idx >= this._samples.length) {
- throw new Error("Index out of bounds");
- }
- return this._samples[(this._idx + idx) % this._samples.length];
- }
- }
- class FirstStepDetector {
- constructor() {
- this._samples = new CircleBuffer(20);
- this._entropy = 0;
- this.onFirstStepDetected = new Observable();
- }
- update(posX, posY, forwardX, forwardY) {
- this._samples.push(posX, posY);
- const origin = this._samples.at(0);
- this._entropy *= this._entropyDecayFactor;
- this._entropy += Vector2.Distance(origin, this._samples.at(1));
- if (this._entropy > this._entropyThreshold) {
- return;
- }
- let samePointIdx;
- for (samePointIdx = this._samePointCheckStartIdx; samePointIdx < this._samples.length; ++samePointIdx) {
- if (Vector2.DistanceSquared(origin, this._samples.at(samePointIdx)) < this._samePointSquaredDistanceThreshold) {
- break;
- }
- }
- if (samePointIdx === this._samples.length) {
- return;
- }
- let apexDistSquared = -1;
- let apexIdx = 0;
- for (let distSquared, idx = 1; idx < samePointIdx; ++idx) {
- distSquared = Vector2.DistanceSquared(origin, this._samples.at(idx));
- if (distSquared > apexDistSquared) {
- apexIdx = idx;
- apexDistSquared = distSquared;
- }
- }
- if (apexDistSquared < this._apexSquaredDistanceThreshold) {
- return;
- }
- const apex = this._samples.at(apexIdx);
- const axis = apex.subtract(origin);
- axis.normalize();
- const vec = TmpVectors.Vector2[0];
- let dot;
- let sample;
- let sumSquaredProjectionDistances = 0;
- for (let idx = 1; idx < samePointIdx; ++idx) {
- sample = this._samples.at(idx);
- sample.subtractToRef(origin, vec);
- dot = Vector2.Dot(axis, vec);
- sumSquaredProjectionDistances += vec.lengthSquared() - dot * dot;
- }
- if (sumSquaredProjectionDistances > samePointIdx * this._squaredProjectionDistanceThreshold) {
- return;
- }
- const forwardVec = TmpVectors.Vector3[0];
- forwardVec.set(forwardX, forwardY, 0);
- const axisVec = TmpVectors.Vector3[1];
- axisVec.set(axis.x, axis.y, 0);
- const isApexLeft = Vector3.Cross(forwardVec, axisVec).z > 0;
- const leftApex = origin.clone();
- const rightApex = origin.clone();
- apex.subtractToRef(origin, axis);
- if (isApexLeft) {
- axis.scaleAndAddToRef(this._axisToApexShrinkFactor, leftApex);
- axis.scaleAndAddToRef(this._axisToApexExtendFactor, rightApex);
- }
- else {
- axis.scaleAndAddToRef(this._axisToApexExtendFactor, leftApex);
- axis.scaleAndAddToRef(this._axisToApexShrinkFactor, rightApex);
- }
- this.onFirstStepDetected.notifyObservers({
- leftApex: leftApex,
- rightApex: rightApex,
- currentPosition: origin,
- currentStepDirection: isApexLeft ? "right" : "left",
- });
- }
- reset() {
- for (let idx = 0; idx < this._samples.length; ++idx) {
- this._samples.at(idx).copyFromFloats(0, 0);
- }
- }
- get _samePointCheckStartIdx() {
- return Math.floor(this._samples.length / 3);
- }
- get _samePointSquaredDistanceThreshold() {
- return 0.03 * 0.03;
- }
- get _apexSquaredDistanceThreshold() {
- return 0.09 * 0.09;
- }
- get _squaredProjectionDistanceThreshold() {
- return 0.03 * 0.03;
- }
- get _axisToApexShrinkFactor() {
- return 0.8;
- }
- get _axisToApexExtendFactor() {
- return -1.6;
- }
- get _entropyDecayFactor() {
- return 0.93;
- }
- get _entropyThreshold() {
- return 0.4;
- }
- }
- class WalkingTracker {
- constructor(leftApex, rightApex, currentPosition, currentStepDirection) {
- this._leftApex = new Vector2();
- this._rightApex = new Vector2();
- this._currentPosition = new Vector2();
- this._axis = new Vector2();
- this._axisLength = -1;
- this._forward = new Vector2();
- this._steppingLeft = false;
- this._t = -1;
- this._maxT = -1;
- this._maxTPosition = new Vector2();
- this._vitality = 0;
- this.onMovement = new Observable();
- this.onFootfall = new Observable();
- this._reset(leftApex, rightApex, currentPosition, currentStepDirection === "left");
- }
- _reset(leftApex, rightApex, currentPosition, steppingLeft) {
- this._leftApex.copyFrom(leftApex);
- this._rightApex.copyFrom(rightApex);
- this._steppingLeft = steppingLeft;
- if (this._steppingLeft) {
- this._leftApex.subtractToRef(this._rightApex, this._axis);
- this._forward.copyFromFloats(-this._axis.y, this._axis.x);
- }
- else {
- this._rightApex.subtractToRef(this._leftApex, this._axis);
- this._forward.copyFromFloats(this._axis.y, -this._axis.x);
- }
- this._axisLength = this._axis.length();
- this._forward.scaleInPlace(1 / this._axisLength);
- this._updateTAndVitality(currentPosition.x, currentPosition.y);
- this._maxT = this._t;
- this._maxTPosition.copyFrom(currentPosition);
- this._vitality = 1;
- }
- _updateTAndVitality(x, y) {
- this._currentPosition.copyFromFloats(x, y);
- if (this._steppingLeft) {
- this._currentPosition.subtractInPlace(this._rightApex);
- }
- else {
- this._currentPosition.subtractInPlace(this._leftApex);
- }
- const priorT = this._t;
- const dot = Vector2.Dot(this._currentPosition, this._axis);
- this._t = dot / (this._axisLength * this._axisLength);
- const projDistSquared = this._currentPosition.lengthSquared() - (dot / this._axisLength) * (dot / this._axisLength);
- // TODO: Extricate the magic.
- this._vitality *= 0.92 - 100 * Math.max(projDistSquared - 0.0016, 0) + Math.max(this._t - priorT, 0);
- }
- update(x, y) {
- if (this._vitality < this._vitalityThreshold) {
- return false;
- }
- const priorT = this._t;
- this._updateTAndVitality(x, y);
- if (this._t > this._maxT) {
- this._maxT = this._t;
- this._maxTPosition.copyFromFloats(x, y);
- }
- if (this._vitality < this._vitalityThreshold) {
- return false;
- }
- if (this._t > priorT) {
- this.onMovement.notifyObservers({ deltaT: this._t - priorT });
- if (priorT < 0.5 && this._t >= 0.5) {
- this.onFootfall.notifyObservers({ foot: this._steppingLeft ? "left" : "right" });
- }
- }
- if (this._t < 0.95 * this._maxT) {
- this._currentPosition.copyFromFloats(x, y);
- if (this._steppingLeft) {
- this._leftApex.copyFrom(this._maxTPosition);
- }
- else {
- this._rightApex.copyFrom(this._maxTPosition);
- }
- this._reset(this._leftApex, this._rightApex, this._currentPosition, !this._steppingLeft);
- }
- if (this._axisLength < 0.03) {
- return false;
- }
- return true;
- }
- get _vitalityThreshold() {
- return 0.1;
- }
- get forward() {
- return this._forward;
- }
- }
- class Walker {
- static get _MillisecondsPerUpdate() {
- // 15 FPS
- return 1000 / 15;
- }
- constructor(engine) {
- this._detector = new FirstStepDetector();
- this._walker = null;
- this._movement = new Vector2();
- this._millisecondsSinceLastUpdate = Walker._MillisecondsPerUpdate;
- this.movementThisFrame = Vector3.Zero();
- this._engine = engine;
- this._detector.onFirstStepDetected.add((event) => {
- if (!this._walker) {
- this._walker = new WalkingTracker(event.leftApex, event.rightApex, event.currentPosition, event.currentStepDirection);
- this._walker.onFootfall.add(() => {
- Logger.Log("Footfall!");
- });
- this._walker.onMovement.add((event) => {
- this._walker.forward.scaleAndAddToRef(0.024 * event.deltaT, this._movement);
- });
- }
- });
- }
- update(position, forward) {
- forward.y = 0;
- forward.normalize();
- // Enforce reduced framerate
- this._millisecondsSinceLastUpdate += this._engine.getDeltaTime();
- if (this._millisecondsSinceLastUpdate >= Walker._MillisecondsPerUpdate) {
- this._millisecondsSinceLastUpdate -= Walker._MillisecondsPerUpdate;
- this._detector.update(position.x, position.z, forward.x, forward.z);
- if (this._walker) {
- const updated = this._walker.update(position.x, position.z);
- if (!updated) {
- this._walker = null;
- }
- }
- this._movement.scaleInPlace(0.85);
- }
- this.movementThisFrame.set(this._movement.x, 0, this._movement.y);
- }
- }
- /**
- * A module that will enable VR locomotion by detecting when the user walks in place.
- */
- export class WebXRWalkingLocomotion extends WebXRAbstractFeature {
- /**
- * The module's name.
- */
- static get Name() {
- return WebXRFeatureName.WALKING_LOCOMOTION;
- }
- /**
- * The (Babylon) version of this module.
- * This is an integer representing the implementation version.
- * This number has no external basis.
- */
- static get Version() {
- return 1;
- }
- /**
- * The target to be articulated by walking locomotion.
- * When the walking locomotion feature detects walking in place, this element's
- * X and Z coordinates will be modified to reflect locomotion. This target should
- * be either the XR space's origin (i.e., the parent node of the WebXRCamera) or
- * the WebXRCamera itself. Note that the WebXRCamera path will modify the position
- * of the WebXRCamera directly and is thus discouraged.
- */
- get locomotionTarget() {
- return this._locomotionTarget;
- }
- /**
- * The target to be articulated by walking locomotion.
- * When the walking locomotion feature detects walking in place, this element's
- * X and Z coordinates will be modified to reflect locomotion. This target should
- * be either the XR space's origin (i.e., the parent node of the WebXRCamera) or
- * the WebXRCamera itself. Note that the WebXRCamera path will modify the position
- * of the WebXRCamera directly and is thus discouraged.
- */
- set locomotionTarget(locomotionTarget) {
- this._locomotionTarget = locomotionTarget;
- this._isLocomotionTargetWebXRCamera = this._locomotionTarget.getClassName() === "WebXRCamera";
- }
- /**
- * Construct a new Walking Locomotion feature.
- * @param sessionManager manager for the current XR session
- * @param options creation options, prominently including the vector target for locomotion
- */
- constructor(sessionManager, options) {
- super(sessionManager);
- this._up = new Vector3();
- this._forward = new Vector3();
- this._position = new Vector3();
- this._movement = new Vector3();
- this._sessionManager = sessionManager;
- this.locomotionTarget = options.locomotionTarget;
- if (this._isLocomotionTargetWebXRCamera) {
- 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");
- }
- }
- /**
- * Checks whether this feature is compatible with the current WebXR session.
- * Walking locomotion is only compatible with "immersive-vr" sessions.
- * @returns true if compatible, false otherwise
- */
- isCompatible() {
- return this._sessionManager.sessionMode === undefined || this._sessionManager.sessionMode === "immersive-vr";
- }
- /**
- * Attaches the feature.
- * Typically called automatically by the features manager.
- * @returns true if attach succeeded, false otherwise
- */
- attach() {
- if (!this.isCompatible || !super.attach()) {
- return false;
- }
- this._walker = new Walker(this._sessionManager.scene.getEngine());
- return true;
- }
- /**
- * Detaches the feature.
- * Typically called automatically by the features manager.
- * @returns true if detach succeeded, false otherwise
- */
- detach() {
- if (!super.detach()) {
- return false;
- }
- this._walker = null;
- return true;
- }
- _onXRFrame(frame) {
- const pose = frame.getViewerPose(this._sessionManager.baseReferenceSpace);
- if (!pose) {
- return;
- }
- const handednessScalar = this.locomotionTarget.getScene().useRightHandedSystem ? 1 : -1;
- const m = pose.transform.matrix;
- this._up.copyFromFloats(m[4], m[5], handednessScalar * m[6]);
- this._forward.copyFromFloats(m[8], m[9], handednessScalar * m[10]);
- this._position.copyFromFloats(m[12], m[13], handednessScalar * m[14]);
- // Compute the nape position
- this._forward.scaleAndAddToRef(0.05, this._position);
- this._up.scaleAndAddToRef(-0.05, this._position);
- this._walker.update(this._position, this._forward);
- this._movement.copyFrom(this._walker.movementThisFrame);
- if (!this._isLocomotionTargetWebXRCamera) {
- Vector3.TransformNormalToRef(this._movement, this.locomotionTarget.getWorldMatrix(), this._movement);
- }
- this.locomotionTarget.position.addInPlace(this._movement);
- }
- }
- //register the plugin
- WebXRFeaturesManager.AddWebXRFeature(WebXRWalkingLocomotion.Name, (xrSessionManager, options) => {
- return () => new WebXRWalkingLocomotion(xrSessionManager, options);
- }, WebXRWalkingLocomotion.Version, false);
- //# sourceMappingURL=WebXRWalkingLocomotion.js.map
|