@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
384 lines • 15.1 kB
JavaScript
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;
// eslint-disable-next-line @typescript-eslint/naming-convention
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;
}
// eslint-disable-next-line @typescript-eslint/naming-convention
_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