diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index 423bed2cbcf..d61d47488ba 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -946,6 +946,7 @@ class CameraComponent extends Component { * @param {import('../../xr/xr-manager.js').XrErrorCallback} [options.callback] - Optional * callback function called once the session is started. The callback has one argument Error - * it is null if the XR session started successfully. + * @param {boolean} [options.anchors] - Optional boolean to attempt to enable {@link XrAnchors}. * @param {object} [options.depthSensing] - Optional object with depth sensing parameters to * attempt to enable {@link XrDepthSensing}. * @param {string} [options.depthSensing.usagePreference] - Optional usage preference for depth diff --git a/src/framework/xr/xr-anchor.js b/src/framework/xr/xr-anchor.js new file mode 100644 index 00000000000..3ed6cdf9a1f --- /dev/null +++ b/src/framework/xr/xr-anchor.js @@ -0,0 +1,119 @@ +import { EventHandler } from '../../core/event-handler.js'; + +import { Vec3 } from '../../core/math/vec3.js'; +import { Quat } from '../../core/math/quat.js'; + +/** + * An anchor keeps track of a position and rotation that is fixed relative to the real world. + * This allows the application to adjust the location of the virtual objects placed in the + * scene in a way that helps with maintaining the illusion that the placed objects are really + * present in the user’s environment. + * + * @augments EventHandler + * @category XR + */ +class XrAnchor extends EventHandler { + /** + * @type {Vec3} + * @private + */ + _position = new Vec3(); + + /** + * @type {Quat} + * @private + */ + _rotation = new Quat(); + + /** + * @param {import('./xr-anchors.js').XrAnchors} anchors - Anchor manager. + * @param {object} xrAnchor - native XRAnchor object that is provided by WebXR API + * @hideconstructor + */ + constructor(anchors, xrAnchor) { + super(); + + this._anchors = anchors; + this._xrAnchor = xrAnchor; + } + + /** + * Fired when an {@link XrAnchor} is destroyed. + * + * @event XrAnchor#destroy + * @example + * // once anchor is destroyed + * anchor.once('destroy', function () { + * // destroy its related entity + * entity.destroy(); + * }); + */ + + /** + * Fired when an {@link XrAnchor}'s position and/or rotation is changed. + * + * @event XrAnchor#change + * @example + * anchor.on('change', function () { + * // anchor has been updated + * entity.setPosition(anchor.getPosition()); + * entity.setRotation(anchor.getRotation()); + * }); + */ + + /** + * Destroy an anchor. + */ + destroy() { + if (!this._xrAnchor) return; + this._anchors._index.delete(this._xrAnchor); + + const ind = this._anchors._list.indexOf(this); + if (ind !== -1) this._anchors._list.splice(ind, 1); + + this._xrAnchor.delete(); + this._xrAnchor = null; + + this.fire('destroy'); + this._anchors.fire('destroy', this); + } + + /** + * @param {*} frame - XRFrame from requestAnimationFrame callback. + * @ignore + */ + update(frame) { + if (!this._xrAnchor) + return; + + const pose = frame.getPose(this._xrAnchor.anchorSpace, this._anchors.manager._referenceSpace); + if (pose) { + if (this._position.equals(pose.transform.position) && this._rotation.equals(pose.transform.orientation)) + return; + + this._position.copy(pose.transform.position); + this._rotation.copy(pose.transform.orientation); + this.fire('change'); + } + } + + /** + * Get the world space position of an anchor. + * + * @returns {Vec3} The world space position of an anchor. + */ + getPosition() { + return this._position; + } + + /** + * Get the world space rotation of an anchor. + * + * @returns {Quat} The world space rotation of an anchor. + */ + getRotation() { + return this._rotation; + } +} + +export { XrAnchor }; diff --git a/src/framework/xr/xr-anchors.js b/src/framework/xr/xr-anchors.js new file mode 100644 index 00000000000..c273878a12d --- /dev/null +++ b/src/framework/xr/xr-anchors.js @@ -0,0 +1,236 @@ +import { EventHandler } from '../../core/event-handler.js'; +import { platform } from '../../core/platform.js'; +import { XrAnchor } from './xr-anchor.js'; + +/** + * Callback used by {@link XrAnchors#create}. + * + * @callback XrAnchorCreate + * @param {Error|null} err - The Error object if failed to create an anchor or null. + * @param {XrAnchor|null} anchor - The anchor that is tracked against real world geometry. + */ + +/** + * Anchors provide an ability to specify a point in the world that needs to be updated to + * correctly reflect the evolving understanding of the world by the underlying AR system, + * such that the anchor remains aligned with the same place in the physical world. + * Anchors tend to persist better relative to the real world, especially during a longer + * session with lots of movement. + * + * ```javascript + * app.xr.start(camera, pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, { + * anchors: true + * }); + * ``` + * @augments EventHandler + * @category XR + */ +class XrAnchors extends EventHandler { + /** + * @type {boolean} + * @private + */ + _supported = platform.browser && !!window.XRAnchor; + + /** + * List of anchor creation requests. + * + * @type {Array} + * @private + */ + _creationQueue = []; + + /** + * Index of XrAnchors, with XRAnchor (native handle) used as a key. + * + * @type {Map} + * @ignore + */ + _index = new Map(); + + /** + * @type {Array} + * @ignore + */ + _list = []; + + /** + * Map of callbacks to XRAnchors so that we can call its callback once + * an anchor is updated with a pose for the first time. + * + * @type {Map} + * @private + */ + _callbacksAnchors = new Map(); + + /** + * @param {import('./xr-manager.js').XrManager} manager - WebXR Manager. + * @hideconstructor + */ + constructor(manager) { + super(); + + this.manager = manager; + + if (this._supported) { + this.manager.on('end', this._onSessionEnd, this); + } + } + + /** + * Fired when anchor failed to be created. + * + * @event XrAnchors#error + * @param {Error} error - Error object related to a failure of anchors. + */ + + /** + * Fired when a new {@link XrAnchor} is added. + * + * @event XrAnchors#add + * @param {XrAnchor} anchor - Anchor that has been added. + * @example + * app.xr.anchors.on('add', function (anchor) { + * // new anchor is added + * }); + */ + + /** + * Fired when an {@link XrAnchor} is destroyed. + * + * @event XrAnchors#destroy + * @param {XrAnchor} anchor - Anchor that has been destroyed. + * @example + * app.xr.anchors.on('destroy', function (anchor) { + * // anchor that is destroyed + * }); + */ + + /** @private */ + _onSessionEnd() { + // clear anchor creation queue + for (let i = 0; i < this._creationQueue.length; i++) { + if (!this._creationQueue[i].callback) + continue; + + this._creationQueue[i].callback(new Error('session ended'), null); + } + this._creationQueue.length = 0; + + // destroy all anchors + if (this._list) { + let i = this._list.length; + while (i--) { + this._list[i].destroy(); + } + this._list.length = 0; + } + } + + /** + * Create anchor with position, rotation and a callback. + * + * @param {import('../../core/math/vec3.js').Vec3} position - Position for an anchor. + * @param {import('../../core/math/quat.js').Quat} [rotation] - Rotation for an anchor. + * @param {XrAnchorCreate} [callback] - Callback to fire when anchor was created or failed to be created. + * @example + * app.xr.anchors.create(position, rotation, function (err, anchor) { + * if (!err) { + * // new anchor has been created + * } + * }); + */ + create(position, rotation, callback) { + this._creationQueue.push({ + transform: new XRRigidTransform(position, rotation), // eslint-disable-line no-undef + callback: callback + }); + } + + /** + * @param {*} frame - XRFrame from requestAnimationFrame callback. + * @ignore + */ + update(frame) { + // check if need to create anchors + if (this._creationQueue.length) { + for (let i = 0; i < this._creationQueue.length; i++) { + const request = this._creationQueue[i]; + + frame.createAnchor(request.transform, this.manager._referenceSpace) + .then((xrAnchor) => { + if (request.callback) + this._callbacksAnchors.set(xrAnchor, request.callback); + }) + .catch((ex) => { + if (request.callback) + request.callback(ex, null); + + this.fire('error', ex); + }); + } + + this._creationQueue.length = 0; + } + + // check if destroyed + for (const [xrAnchor, anchor] of this._index) { + if (frame.trackedAnchors.has(xrAnchor)) + continue; + + anchor.destroy(); + } + + // update existing anchors + for (let i = 0; i < this._list.length; i++) { + this._list[i].update(frame); + } + + // check if added + for (const xrAnchor of frame.trackedAnchors) { + if (this._index.has(xrAnchor)) + continue; + + try { + const tmp = xrAnchor.anchorSpace; // eslint-disable-line no-unused-vars + } catch (ex) { + // if anchorSpace is not available, then anchor is invalid + // and should not be created + continue; + } + + const anchor = new XrAnchor(this, xrAnchor); + this._index.set(xrAnchor, anchor); + this._list.push(anchor); + anchor.update(frame); + + const callback = this._callbacksAnchors.get(xrAnchor); + if (callback) { + this._callbacksAnchors.delete(xrAnchor); + callback(null, anchor); + } + + this.fire('add', anchor); + } + } + + /** + * True if Anchors are supported. + * + * @type {boolean} + */ + get supported() { + return this._supported; + } + + /** + * List of available {@link XrAnchor}s. + * + * @type {Array} + */ + get list() { + return this._list; + } +} + +export { XrAnchors }; diff --git a/src/framework/xr/xr-manager.js b/src/framework/xr/xr-manager.js index 0da070bd544..051ae143dc7 100644 --- a/src/framework/xr/xr-manager.js +++ b/src/framework/xr/xr-manager.js @@ -16,6 +16,7 @@ import { XrImageTracking } from './xr-image-tracking.js'; import { XrInput } from './xr-input.js'; import { XrLightEstimation } from './xr-light-estimation.js'; import { XrPlaneDetection } from './xr-plane-detection.js'; +import { XrAnchors } from './xr-anchors.js'; /** * Callback used by {@link XrManager#endXr} and {@link XrManager#startXr}. @@ -75,7 +76,7 @@ class XrManager extends EventHandler { /** * @type {XRReferenceSpace|null} - * @private + * @ignore */ _referenceSpace = null; @@ -211,6 +212,7 @@ class XrManager extends EventHandler { this.planeDetection = new XrPlaneDetection(this); this.input = new XrInput(this); this.lightEstimation = new XrLightEstimation(this); + this.anchors = new XrAnchors(this); // TODO // 1. HMD class with its params @@ -337,6 +339,8 @@ class XrManager extends EventHandler { * @param {object} [options] - Object with additional options for XR session initialization. * @param {string[]} [options.optionalFeatures] - Optional features for XRSession start. It is * used for getting access to additional WebXR spec extensions. + * @param {boolean} [options.anchors] - Set to true to attempt to enable + * {@link XrAnchors}. * @param {boolean} [options.imageTracking] - Set to true to attempt to enable * {@link XrImageTracking}. * @param {boolean} [options.planeDetection] - Set to true to attempt to enable @@ -361,6 +365,8 @@ class XrManager extends EventHandler { * @example * button.on('click', function () { * app.xr.start(camera, pc.XRTYPE_AR, pc.XRSPACE_LOCALFLOOR, { + * anchors: true, + * imageTracking: true, * depthSensing: { } * }); * }); @@ -418,6 +424,10 @@ class XrManager extends EventHandler { opts.domOverlay = { root: this.domOverlay.root }; } + if (options && options.anchors && this.anchors.supported) { + opts.optionalFeatures.push('anchors'); + } + if (options && options.depthSensing && this.depthSensing.supported) { opts.optionalFeatures.push('depth-sensing'); @@ -787,6 +797,9 @@ class XrManager extends EventHandler { if (this.imageTracking.supported) this.imageTracking.update(frame); + if (this.anchors.supported) + this.anchors.update(frame); + if (this.planeDetection.supported) this.planeDetection.update(frame); } diff --git a/src/index.js b/src/index.js index 06b0ecdb4ac..9d1e6b6a29c 100644 --- a/src/index.js +++ b/src/index.js @@ -328,6 +328,8 @@ export { XrHitTestSource } from './framework/xr/xr-hit-test-source.js'; export { XrImageTracking } from './framework/xr/xr-image-tracking.js'; export { XrTrackedImage } from './framework/xr/xr-tracked-image.js'; export { XrDomOverlay } from './framework/xr/xr-dom-overlay.js'; +export { XrAnchors } from './framework/xr/xr-anchors.js'; +export { XrAnchor } from './framework/xr/xr-anchor.js'; export { XrPlaneDetection } from './framework/xr/xr-plane-detection.js'; export { XrPlane } from './framework/xr/xr-plane.js';