|
@@ -1,29 +1,26 @@
|
|
|
-import { WebXRFeatureName } from '../webXRFeaturesManager';
|
|
|
+import { WebXRFeatureName, WebXRFeaturesManager } from '../webXRFeaturesManager';
|
|
|
import { WebXRSessionManager } from '../webXRSessionManager';
|
|
|
import { Observable } from '../../Misc/observable';
|
|
|
-import { Matrix } from '../../Maths/math.vector';
|
|
|
+import { Matrix, Vector3, Quaternion } from '../../Maths/math.vector';
|
|
|
import { TransformNode } from '../../Meshes/transformNode';
|
|
|
-import { WebXRPlaneDetector } from './WebXRPlaneDetector';
|
|
|
-import { WebXRHitTestLegacy } from './WebXRHitTestLegacy';
|
|
|
import { WebXRAbstractFeature } from './WebXRAbstractFeature';
|
|
|
+import { IWebXRHitResult } from './WebXRHitTest';
|
|
|
+import { Tools } from '../../Misc/tools';
|
|
|
|
|
|
/**
|
|
|
* Configuration options of the anchor system
|
|
|
*/
|
|
|
export interface IWebXRAnchorSystemOptions {
|
|
|
/**
|
|
|
- * Should a new anchor be added every time a select event is triggered
|
|
|
- */
|
|
|
- addAnchorOnSelect?: boolean;
|
|
|
- /**
|
|
|
- * should the anchor system use plane detection.
|
|
|
- * If set to true, the plane-detection feature should be set using setPlaneDetector
|
|
|
- */
|
|
|
- usePlaneDetection?: boolean;
|
|
|
- /**
|
|
|
* a node that will be used to convert local to world coordinates
|
|
|
*/
|
|
|
worldParentNode?: TransformNode;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * If set to true a reference of the created anchors will be kept until the next session starts
|
|
|
+ * If not defined, anchors will be removed from the array when the feature is detached or the session ended.
|
|
|
+ */
|
|
|
+ doNotRemoveAnchorsOnSessionEnded?: boolean;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -42,45 +39,33 @@ export interface IWebXRAnchor {
|
|
|
* The native anchor object
|
|
|
*/
|
|
|
xrAnchor: XRAnchor;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * if defined, this object will be constantly updated by the anchor's position and rotation
|
|
|
+ */
|
|
|
+ attachedNode?: TransformNode;
|
|
|
+}
|
|
|
+
|
|
|
+interface IWebXRFutureAnchor {
|
|
|
+ resolve: (xrAnchor: XRAnchor) => void;
|
|
|
+ reject: (msg?: string) => void;
|
|
|
+ xrTransformation: XRRigidTransform;
|
|
|
}
|
|
|
|
|
|
let anchorIdProvider = 0;
|
|
|
|
|
|
/**
|
|
|
- * An implementation of the anchor system of WebXR.
|
|
|
- * Note that the current documented implementation is not available in any browser. Future implementations
|
|
|
- * will use the frame to create an anchor and not the session or a detected plane
|
|
|
+ * An implementation of the anchor system for WebXR.
|
|
|
* For further information see https://github.com/immersive-web/anchors/
|
|
|
*/
|
|
|
export class WebXRAnchorSystem extends WebXRAbstractFeature {
|
|
|
- private _enabled: boolean = false;
|
|
|
- private _hitTestModule: WebXRHitTestLegacy;
|
|
|
private _lastFrameDetected: XRAnchorSet = new Set();
|
|
|
- private _onSelect = (event: XRInputSourceEvent) => {
|
|
|
- if (!this._options.addAnchorOnSelect) {
|
|
|
- return;
|
|
|
- }
|
|
|
- const onResults = (results: XRHitResult[]) => {
|
|
|
- if (results.length) {
|
|
|
- const hitResult = results[0];
|
|
|
- const transform = new XRRigidTransform(hitResult.hitMatrix);
|
|
|
- // find the plane on which to add.
|
|
|
- this.addAnchorAtRigidTransformation(transform);
|
|
|
- }
|
|
|
- };
|
|
|
|
|
|
- // avoid the hit-test, if the hit-test module is defined
|
|
|
- if (this._hitTestModule && !this._hitTestModule.options.testOnPointerDownOnly) {
|
|
|
- onResults(this._hitTestModule.lastNativeXRHitResults);
|
|
|
- }
|
|
|
- WebXRHitTestLegacy.XRHitTestWithSelectEvent(event, this._xrSessionManager.referenceSpace).then(onResults);
|
|
|
+ private _trackedAnchors: Array<IWebXRAnchor> = [];
|
|
|
|
|
|
- // API will soon change, will need to use the plane
|
|
|
- this._planeDetector;
|
|
|
- }
|
|
|
+ private _referenceSpaceForFrameAnchors: XRReferenceSpace;
|
|
|
|
|
|
- private _planeDetector: WebXRPlaneDetector;
|
|
|
- private _trackedAnchors: Array<IWebXRAnchor> = [];
|
|
|
+ private _futureAnchors: IWebXRFutureAnchor[] = [];
|
|
|
|
|
|
/**
|
|
|
* The module's name
|
|
@@ -108,6 +93,14 @@ export class WebXRAnchorSystem extends WebXRAbstractFeature {
|
|
|
public onAnchorUpdatedObservable: Observable<IWebXRAnchor> = new Observable();
|
|
|
|
|
|
/**
|
|
|
+ * 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
|
|
|
+ */
|
|
|
+ public set referenceSpaceForFrameAnchors(referenceSpace: XRReferenceSpace) {
|
|
|
+ this._referenceSpaceForFrameAnchors = referenceSpace;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* constructs a new anchor system
|
|
|
* @param _xrSessionManager an instance of WebXRSessionManager
|
|
|
* @param _options configuration object for this feature
|
|
@@ -116,32 +109,79 @@ export class WebXRAnchorSystem extends WebXRAbstractFeature {
|
|
|
super(_xrSessionManager);
|
|
|
}
|
|
|
|
|
|
+ private _tmpVector = new Vector3();
|
|
|
+ private _tmpQuaternion = new Quaternion();
|
|
|
+
|
|
|
+ private _populateTmpTransformation(position: Vector3, rotationQuaternion: Quaternion) {
|
|
|
+ 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
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
- * Add anchor at a specific XR point.
|
|
|
+ * 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 xrRigidTransformation xr-coordinates where a new anchor should be added
|
|
|
- * @param anchorCreator the object o use to create an anchor with. either a session or a plane
|
|
|
- * @returns a promise the fulfills when the anchor was created
|
|
|
+ * @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 the XR anchor was registered in the system (but not necessarily added to the tracked anchors)
|
|
|
*/
|
|
|
- public addAnchorAtRigidTransformation(xrRigidTransformation: XRRigidTransform, anchorCreator?: XRAnchorCreator): Promise<XRAnchor> {
|
|
|
- const creator = anchorCreator || this._xrSessionManager.session;
|
|
|
- return creator.createAnchor(xrRigidTransformation, this._xrSessionManager.referenceSpace);
|
|
|
+ public async addAnchorPointUsingHitTestResultAsync(hitTestResult: IWebXRHitResult, position: Vector3 = new Vector3(), rotationQuaternion: Quaternion = new Quaternion()): Promise<XRAnchor> {
|
|
|
+ // convert to XR space (right handed) if needed
|
|
|
+ this._populateTmpTransformation(position, rotationQuaternion);
|
|
|
+ // the matrix that we'll use
|
|
|
+ const m = new XRRigidTransform({...this._tmpVector}, {...this._tmpQuaternion});
|
|
|
+ if (!hitTestResult.xrHitResult.createAnchor) {
|
|
|
+ throw new Error('Anchors not enabled in this browsed. Add "anchors" to optional features');
|
|
|
+ } else {
|
|
|
+ try {
|
|
|
+ return hitTestResult.xrHitResult.createAnchor(m);
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ throw new Error(error);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * attach this feature
|
|
|
- * Will usually be called by the features manager
|
|
|
+ * 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.
|
|
|
*
|
|
|
- * @returns true if successful.
|
|
|
+ * @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 the XR anchor was registered in the system (but not necessarily added to the tracked anchors)
|
|
|
*/
|
|
|
- public attach(): boolean {
|
|
|
- if (!super.attach()) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- if (this._options.addAnchorOnSelect) {
|
|
|
- this._xrSessionManager.session.addEventListener('select', this._onSelect, false);
|
|
|
+ public addAnchorAtPositionAndRotationAsync(position: Vector3, rotationQuaternion: Quaternion = new Quaternion(), forceCreateInCurrentFrame = false): Promise<XRAnchor> {
|
|
|
+ // convert to XR space (right handed) if needed
|
|
|
+ this._populateTmpTransformation(position, rotationQuaternion);
|
|
|
+ // the matrix that we'll use
|
|
|
+ const xrTransformation = new XRRigidTransform({...this._tmpVector}, {...this._tmpQuaternion});
|
|
|
+ if (forceCreateInCurrentFrame && this.attached && this._xrSessionManager.currentFrame) {
|
|
|
+ return this._createAnchorAtTransformation(xrTransformation, this._xrSessionManager.currentFrame);
|
|
|
+ } else {
|
|
|
+ // add the transformation to the future anchors list
|
|
|
+ return new Promise<XRAnchor>((resolve, reject) => {
|
|
|
+ this._futureAnchors.push({
|
|
|
+ xrTransformation,
|
|
|
+ resolve,
|
|
|
+ reject
|
|
|
+ });
|
|
|
+ });
|
|
|
}
|
|
|
- return true;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -155,7 +195,14 @@ export class WebXRAnchorSystem extends WebXRAbstractFeature {
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
- this._xrSessionManager.session.removeEventListener('select', this._onSelect);
|
|
|
+ if (!this._options.doNotRemoveAnchorsOnSessionEnded) {
|
|
|
+ while (this._trackedAnchors.length) {
|
|
|
+ const toRemove = this._trackedAnchors.pop();
|
|
|
+ if (toRemove) {
|
|
|
+ this.onAnchorRemovedObservable.notifyObservers(toRemove);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
return true;
|
|
|
}
|
|
@@ -170,33 +217,20 @@ export class WebXRAnchorSystem extends WebXRAbstractFeature {
|
|
|
this.onAnchorUpdatedObservable.clear();
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * If set, it will improve performance by using the current hit-test results instead of executing a new hit-test
|
|
|
- * @param hitTestModule the hit-test module to use.
|
|
|
- */
|
|
|
- public setHitTestModule(hitTestModule: WebXRHitTestLegacy) {
|
|
|
- this._hitTestModule = hitTestModule;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * set the plane detector to use in order to create anchors from frames
|
|
|
- * @param planeDetector the plane-detector module to use
|
|
|
- * @param enable enable plane-anchors. default is true
|
|
|
- */
|
|
|
- public setPlaneDetector(planeDetector: WebXRPlaneDetector, enable: boolean = true) {
|
|
|
- this._planeDetector = planeDetector;
|
|
|
- this._options.usePlaneDetection = enable;
|
|
|
- }
|
|
|
-
|
|
|
protected _onXRFrame(frame: XRFrame) {
|
|
|
- if (!this.attached || !this._enabled || !frame) { return; }
|
|
|
+ if (!this.attached || !frame) { return; }
|
|
|
|
|
|
const trackedAnchors = frame.trackedAnchors;
|
|
|
- if (trackedAnchors && trackedAnchors.size) {
|
|
|
- this._trackedAnchors.filter((anchor) => !trackedAnchors.has(anchor.xrAnchor)).map((anchor) => {
|
|
|
+ if (trackedAnchors) {
|
|
|
+ const toRemove = this._trackedAnchors.filter((anchor) => !trackedAnchors.has(anchor.xrAnchor)).map((anchor) => {
|
|
|
const index = this._trackedAnchors.indexOf(anchor);
|
|
|
- this._trackedAnchors.splice(index, 1);
|
|
|
+ return index;
|
|
|
+ });
|
|
|
+ let idxTracker = 0;
|
|
|
+ toRemove.forEach((index) => {
|
|
|
+ const anchor = this._trackedAnchors.splice(index - idxTracker, 1)[0];
|
|
|
this.onAnchorRemovedObservable.notifyObservers(anchor);
|
|
|
+ idxTracker--;
|
|
|
});
|
|
|
// now check for new ones
|
|
|
trackedAnchors.forEach((xrAnchor) => {
|
|
@@ -205,21 +239,39 @@ export class WebXRAnchorSystem extends WebXRAbstractFeature {
|
|
|
id: anchorIdProvider++,
|
|
|
xrAnchor: xrAnchor
|
|
|
};
|
|
|
- const plane = this._updateAnchorWithXRFrame(xrAnchor, newAnchor, frame);
|
|
|
- this._trackedAnchors.push(plane);
|
|
|
- this.onAnchorAddedObservable.notifyObservers(plane);
|
|
|
+ const anchor = this._updateAnchorWithXRFrame(xrAnchor, newAnchor, frame);
|
|
|
+ this._trackedAnchors.push(anchor);
|
|
|
+ this.onAnchorAddedObservable.notifyObservers(anchor);
|
|
|
} else {
|
|
|
- // updated?
|
|
|
- if (xrAnchor.lastChangedTime === this._xrSessionManager.currentTimestamp) {
|
|
|
- let index = this._findIndexInAnchorArray(xrAnchor);
|
|
|
- const anchor = this._trackedAnchors[index];
|
|
|
+ let 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
|
|
|
+ while (this._futureAnchors.length) {
|
|
|
+ const futureAnchor = this._futureAnchors.pop();
|
|
|
+ if (!futureAnchor) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (!frame.createAnchor) {
|
|
|
+ futureAnchor.reject('Anchors not enabled in this browser');
|
|
|
+ }
|
|
|
+ this._createAnchorAtTransformation(futureAnchor.xrTransformation, frame).then(futureAnchor.resolve, futureAnchor.reject);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -254,9 +306,22 @@ export class WebXRAnchorSystem extends WebXRAbstractFeature {
|
|
|
|
|
|
return <IWebXRAnchor>anchor;
|
|
|
}
|
|
|
+
|
|
|
+ private async _createAnchorAtTransformation(xrTransformation: XRRigidTransform, xrFrame: XRFrame) {
|
|
|
+ if (xrFrame.createAnchor) {
|
|
|
+ try {
|
|
|
+ return xrFrame.createAnchor(xrTransformation, this._referenceSpaceForFrameAnchors ?? this._xrSessionManager.referenceSpace);
|
|
|
+ }
|
|
|
+ catch (error) {
|
|
|
+ throw new Error(error);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ throw new Error('Anchors are not enabled in your browser');
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// register the plugin
|
|
|
-// WebXRFeaturesManager.AddWebXRFeature(WebXRAnchorSystem.Name, (xrSessionManager, options) => {
|
|
|
-// return () => new WebXRAnchorSystem(xrSessionManager, options);
|
|
|
-// }, WebXRAnchorSystem.Version);
|
|
|
+WebXRFeaturesManager.AddWebXRFeature(WebXRAnchorSystem.Name, (xrSessionManager, options) => {
|
|
|
+ return () => new WebXRAnchorSystem(xrSessionManager, options);
|
|
|
+}, WebXRAnchorSystem.Version);
|