@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.
309 lines • 13.9 kB
JavaScript
import { WebXRFeatureName, WebXRFeaturesManager } from "../webXRFeaturesManager.js";
import { Observable } from "../../Misc/observable.js";
import { Matrix, Vector3, Quaternion } from "../../Maths/math.vector.js";
import { WebXRAbstractFeature } from "./WebXRAbstractFeature.js";
import { Tools } from "../../Misc/tools.js";
let AnchorIdProvider = 0;
/**
* An implementation of the anchor system for WebXR.
* For further information see https://github.com/immersive-web/anchors/
*/
export class WebXRAnchorSystem extends WebXRAbstractFeature {
/**
* Set the reference space to use for anchor creation, when not using a hit test.
* Will default to the session's reference space if not defined
*/
set referenceSpaceForFrameAnchors(referenceSpace) {
this._referenceSpaceForFrameAnchors = referenceSpace;
}
/**
* constructs a new anchor system
* @param _xrSessionManager an instance of WebXRSessionManager
* @param _options configuration object for this feature
*/
constructor(_xrSessionManager, _options = {}) {
super(_xrSessionManager);
this._options = _options;
this._lastFrameDetected = new Set();
this._trackedAnchors = [];
this._futureAnchors = [];
/**
* Observers registered here will be executed when a new anchor was added to the session
*/
this.onAnchorAddedObservable = new Observable();
/**
* Observers registered here will be executed when an anchor was removed from the session
*/
this.onAnchorRemovedObservable = new Observable();
/**
* Observers registered here will be executed when an existing anchor updates
* This can execute N times every frame
*/
this.onAnchorUpdatedObservable = new Observable();
this._tmpVector = new Vector3();
this._tmpQuaternion = new Quaternion();
this.xrNativeFeatureName = "anchors";
if (this._options.clearAnchorsOnSessionInit) {
this._xrSessionManager.onXRSessionInit.add(() => {
this._trackedAnchors.length = 0;
this._futureAnchors.length = 0;
this._lastFrameDetected.clear();
});
}
}
_populateTmpTransformation(position, rotationQuaternion) {
this._tmpVector.copyFrom(position);
this._tmpQuaternion.copyFrom(rotationQuaternion);
if (!this._xrSessionManager.scene.useRightHandedSystem) {
this._tmpVector.z *= -1;
this._tmpQuaternion.z *= -1;
this._tmpQuaternion.w *= -1;
}
return {
position: this._tmpVector,
rotationQuaternion: this._tmpQuaternion,
};
}
/**
* Create a new anchor point using a hit test result at a specific point in the scene
* An anchor is tracked only after it is added to the trackerAnchors in xrFrame. The promise returned here does not yet guaranty that.
* Use onAnchorAddedObservable to get newly added anchors if you require tracking guaranty.
*
* @param hitTestResult The hit test result to use for this anchor creation
* @param position an optional position offset for this anchor
* @param rotationQuaternion an optional rotation offset for this anchor
* @returns A promise that fulfills when babylon has created the corresponding WebXRAnchor object and tracking has begun
*/
async addAnchorPointUsingHitTestResultAsync(hitTestResult, position = new Vector3(), rotationQuaternion = new Quaternion()) {
// convert to XR space (right handed) if needed
this._populateTmpTransformation(position, rotationQuaternion);
// the matrix that we'll use
const m = new XRRigidTransform({ x: this._tmpVector.x, y: this._tmpVector.y, z: this._tmpVector.z }, { x: this._tmpQuaternion.x, y: this._tmpQuaternion.y, z: this._tmpQuaternion.z, w: this._tmpQuaternion.w });
if (!hitTestResult.xrHitResult.createAnchor) {
this.detach();
throw new Error("Anchors not enabled in this environment/browser");
}
else {
try {
const nativeAnchor = await hitTestResult.xrHitResult.createAnchor(m);
return await new Promise((resolve, reject) => {
this._futureAnchors.push({
nativeAnchor,
resolved: false,
submitted: true,
xrTransformation: m,
resolve,
reject,
});
});
}
catch (error) {
throw new Error(error);
}
}
}
/**
* Add a new anchor at a specific position and rotation
* This function will add a new anchor per default in the next available frame. Unless forced, the createAnchor function
* will be called in the next xrFrame loop to make sure that the anchor can be created correctly.
* An anchor is tracked only after it is added to the trackerAnchors in xrFrame. The promise returned here does not yet guaranty that.
* Use onAnchorAddedObservable to get newly added anchors if you require tracking guaranty.
*
* @param position the position in which to add an anchor
* @param rotationQuaternion an optional rotation for the anchor transformation
* @param forceCreateInCurrentFrame force the creation of this anchor in the current frame. Must be called inside xrFrame loop!
* @returns A promise that fulfills when babylon has created the corresponding WebXRAnchor object and tracking has begun
*/
async addAnchorAtPositionAndRotationAsync(position, rotationQuaternion = new Quaternion(), forceCreateInCurrentFrame = false) {
// convert to XR space (right handed) if needed
this._populateTmpTransformation(position, rotationQuaternion);
// the matrix that we'll use
const xrTransformation = new XRRigidTransform({ x: this._tmpVector.x, y: this._tmpVector.y, z: this._tmpVector.z }, { x: this._tmpQuaternion.x, y: this._tmpQuaternion.y, z: this._tmpQuaternion.z, w: this._tmpQuaternion.w });
const xrAnchor = forceCreateInCurrentFrame && this.attached && this._xrSessionManager.currentFrame
? await this._createAnchorAtTransformationAsync(xrTransformation, this._xrSessionManager.currentFrame)
: undefined;
// add the transformation to the future anchors list
return await new Promise((resolve, reject) => {
this._futureAnchors.push({
nativeAnchor: xrAnchor,
resolved: false,
submitted: false,
xrTransformation,
resolve,
reject,
});
});
}
/**
* Get the list of anchors currently being tracked by the system
*/
get anchors() {
return this._trackedAnchors;
}
/**
* detach this feature.
* Will usually be called by the features manager
*
* @returns true if successful.
*/
detach() {
if (!super.detach()) {
return false;
}
if (!this._options.doNotRemoveAnchorsOnSessionEnded) {
while (this._trackedAnchors.length) {
const toRemove = this._trackedAnchors.pop();
if (toRemove && !toRemove._removed) {
// as the xr frame loop is removed, we need to notify manually
this.onAnchorRemovedObservable.notifyObservers(toRemove);
toRemove._removed = true;
// no need to call the remove fn as the anchor is already removed from the session
}
}
}
return true;
}
/**
* Dispose this feature and all of the resources attached
*/
dispose() {
this._futureAnchors.length = 0;
super.dispose();
this.onAnchorAddedObservable.clear();
this.onAnchorRemovedObservable.clear();
this.onAnchorUpdatedObservable.clear();
}
_onXRFrame(frame) {
if (!this.attached || !frame) {
return;
}
const trackedAnchors = frame.trackedAnchors;
if (trackedAnchors) {
const toRemove = this._trackedAnchors
.filter((anchor) => anchor._removed)
.map((anchor) => {
return this._trackedAnchors.indexOf(anchor);
});
let idxTracker = 0;
for (const index of toRemove) {
const anchor = this._trackedAnchors.splice(index - idxTracker, 1)[0];
anchor.xrAnchor.delete();
this.onAnchorRemovedObservable.notifyObservers(anchor);
idxTracker++;
}
// now check for new ones
trackedAnchors.forEach((xrAnchor) => {
if (!this._lastFrameDetected.has(xrAnchor)) {
const newAnchor = {
id: AnchorIdProvider++,
xrAnchor: xrAnchor,
remove: () => {
newAnchor._removed = true;
},
};
const anchor = this._updateAnchorWithXRFrame(xrAnchor, newAnchor, frame);
this._trackedAnchors.push(anchor);
this.onAnchorAddedObservable.notifyObservers(anchor);
// search for the future anchor promise that matches this
const results = this._futureAnchors.filter((futureAnchor) => futureAnchor.nativeAnchor === xrAnchor);
const result = results[0];
if (result) {
result.resolve(anchor);
result.resolved = true;
}
}
else {
const index = this._findIndexInAnchorArray(xrAnchor);
const anchor = this._trackedAnchors[index];
try {
// anchors update every frame
this._updateAnchorWithXRFrame(xrAnchor, anchor, frame);
if (anchor.attachedNode) {
anchor.attachedNode.rotationQuaternion = anchor.attachedNode.rotationQuaternion || new Quaternion();
anchor.transformationMatrix.decompose(anchor.attachedNode.scaling, anchor.attachedNode.rotationQuaternion, anchor.attachedNode.position);
}
this.onAnchorUpdatedObservable.notifyObservers(anchor);
}
catch (e) {
Tools.Warn(`Anchor could not be updated`);
}
}
});
this._lastFrameDetected = trackedAnchors;
}
// process future anchors
for (const futureAnchor of this._futureAnchors) {
if (!futureAnchor.resolved && !futureAnchor.submitted) {
// eslint-disable-next-line github/no-then
this._createAnchorAtTransformationAsync(futureAnchor.xrTransformation, frame).then((nativeAnchor) => {
futureAnchor.nativeAnchor = nativeAnchor;
}, (error) => {
futureAnchor.resolved = true;
futureAnchor.reject(error);
});
futureAnchor.submitted = true;
}
}
}
/**
* avoiding using Array.find for global support.
* @param xrAnchor the plane to find in the array
* @returns the index of the anchor in the array or -1 if not found
*/
_findIndexInAnchorArray(xrAnchor) {
for (let i = 0; i < this._trackedAnchors.length; ++i) {
if (this._trackedAnchors[i].xrAnchor === xrAnchor) {
return i;
}
}
return -1;
}
_updateAnchorWithXRFrame(xrAnchor, anchor, xrFrame) {
// matrix
const pose = xrFrame.getPose(xrAnchor.anchorSpace, this._xrSessionManager.referenceSpace);
if (pose) {
const mat = anchor.transformationMatrix || new Matrix();
Matrix.FromArrayToRef(pose.transform.matrix, 0, mat);
if (!this._xrSessionManager.scene.useRightHandedSystem) {
mat.toggleModelMatrixHandInPlace();
}
anchor.transformationMatrix = mat;
if (!this._options.worldParentNode) {
// Logger.Warn("Please provide a world parent node to apply world transformation");
}
else {
mat.multiplyToRef(this._options.worldParentNode.getWorldMatrix(), mat);
}
}
return anchor;
}
async _createAnchorAtTransformationAsync(xrTransformation, xrFrame) {
if (xrFrame.createAnchor) {
try {
return await xrFrame.createAnchor(xrTransformation, this._referenceSpaceForFrameAnchors ?? this._xrSessionManager.referenceSpace);
}
catch (error) {
throw new Error(error);
}
}
else {
this.detach();
throw new Error("Anchors are not enabled in your browser");
}
}
}
/**
* The module's name
*/
WebXRAnchorSystem.Name = WebXRFeatureName.ANCHOR_SYSTEM;
/**
* The (Babylon) version of this module.
* This is an integer representing the implementation version.
* This number does not correspond to the WebXR specs version
*/
WebXRAnchorSystem.Version = 1;
// register the plugin
WebXRFeaturesManager.AddWebXRFeature(WebXRAnchorSystem.Name, (xrSessionManager, options) => {
return () => new WebXRAnchorSystem(xrSessionManager, options);
}, WebXRAnchorSystem.Version);
//# sourceMappingURL=WebXRAnchorSystem.js.map