diff --git a/src/framework/components/camera/component.js b/src/framework/components/camera/component.js index b670fb6c959..08dccf5873c 100644 --- a/src/framework/components/camera/component.js +++ b/src/framework/components/camera/component.js @@ -594,6 +594,8 @@ class CameraComponent extends Component { * * @param {object} [options] - Object with 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.imageTracking] - Set to true to attempt to enable {@link XrImageTracking}. + * @param {boolean} [options.planeDetection] - Set to true to attempt to enable {@link XrPlaneDetection}. * @param {callbacks.XrError} [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. diff --git a/src/index.js b/src/index.js index 7fb2424d173..36c3f5d39cc 100644 --- a/src/index.js +++ b/src/index.js @@ -299,6 +299,8 @@ export { XrHitTestSource } from './xr/xr-hit-test-source.js'; export { XrImageTracking } from './xr/xr-image-tracking.js'; export { XrTrackedImage } from './xr/xr-tracked-image.js'; export { XrDomOverlay } from './xr/xr-dom-overlay.js'; +export { XrPlaneDetection } from './xr/xr-plane-detection.js'; +export { XrPlane } from './xr/xr-plane.js'; // BACKWARDS COMPATIBILITY export * from './deprecated.js'; diff --git a/src/xr/xr-manager.js b/src/xr/xr-manager.js index db290687d85..ef4e89ed769 100644 --- a/src/xr/xr-manager.js +++ b/src/xr/xr-manager.js @@ -13,6 +13,7 @@ import { XrLightEstimation } from './xr-light-estimation.js'; import { XrImageTracking } from './xr-image-tracking.js'; import { XrDomOverlay } from './xr-dom-overlay.js'; import { XrDepthSensing } from './xr-depth-sensing.js'; +import { XrPlaneDetection } from './xr-plane-detection.js'; /** * @class @@ -57,6 +58,7 @@ class XrManager extends EventHandler { this.domOverlay = new XrDomOverlay(this); this.hitTest = new XrHitTest(this); this.imageTracking = new XrImageTracking(this); + this.planeDetection = new XrPlaneDetection(this); this.input = new XrInput(this); this.lightEstimation = new XrLightEstimation(this); @@ -171,7 +173,7 @@ class XrManager extends EventHandler { * * @example * button.on('click', function () { - * app.xr.start(camera, pc.XRTYPE_VR, pc.XRSPACE_LOCAL); + * app.xr.start(camera, pc.XRTYPE_VR, pc.XRSPACE_LOCALFLOOR); * }); * @example * button.on('click', function () { @@ -181,6 +183,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.imageTracking] - Set to true to attempt to enable {@link XrImageTracking}. + * @param {boolean} [options.planeDetection] - Set to true to attempt to enable {@link XrPlaneDetection}. * @param {callbacks.XrError} [options.callback] - Optional callback function called once session is started. The callback has one argument Error - it is null if successfully started XR session. * @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 sensing, can be 'cpu-optimized' or 'gpu-optimized' (XRDEPTHSENSINGUSAGE_*), defaults to 'cpu-optimized'. Most preferred and supported will be chosen by the underlying depth sensing system. @@ -226,8 +230,12 @@ class XrManager extends EventHandler { opts.optionalFeatures.push('light-estimation'); opts.optionalFeatures.push('hit-test'); - if (options && options.imageTracking && this.imageTracking.supported) { - opts.optionalFeatures.push('image-tracking'); + if (options) { + if (options.imageTracking && this.imageTracking.supported) + opts.optionalFeatures.push('image-tracking'); + + if (options.planeDetection) + opts.optionalFeatures.push('plane-detection'); } if (this.domOverlay.supported && this.domOverlay.root) { @@ -537,6 +545,9 @@ class XrManager extends EventHandler { if (this.imageTracking.supported) this.imageTracking.update(frame); + + if (this.planeDetection.supported) + this.planeDetection.update(frame); } this.fire('update', frame); diff --git a/src/xr/xr-plane-detection.js b/src/xr/xr-plane-detection.js new file mode 100644 index 00000000000..9d44469cd8e --- /dev/null +++ b/src/xr/xr-plane-detection.js @@ -0,0 +1,150 @@ +import { EventHandler } from '../core/event-handler.js'; +import { XrPlane } from './xr-plane.js'; + +/** + * @class + * @name XrPlaneDetection + * @classdesc Plane Detection provides the ability to detect real world surfaces based on estimations of the underlying AR system. + * @description Plane Detection provides the ability to detect real world surfaces based on estimations of the underlying AR system. + * @param {XrManager} manager - WebXR Manager. + * @property {boolean} supported True if Plane Detection is supported. + * @property {boolean} available True if Plane Detection is available. This property can be set to true only during a running session. + * @property {XrPlane[]|null} planes Array of {@link XrPlane} instances that contain individual plane information, or null if plane detection is not available. + * @example + * // start session with plane detection enabled + * app.xr.start(camera, pc.XRTYPE_VR, pc.XRSPACE_LOCALFLOOR, { + * planeDetection: true + * }); + * @example + * app.xr.planeDetection.on('add', function (plane) { + * // new plane been added + * }); + */ +class XrPlaneDetection extends EventHandler { + constructor(manager) { + super(); + + this._manager = manager; + this._supported = !! window.XRPlane; + this._available = false; + + // key - XRPlane (native plane does not have ID's) + // value - XrPlane + this._planesIndex = new Map(); + + this._planes = null; + + if (this._supported) { + this._manager.on('end', this._onSessionEnd, this); + } + } + + /** + * @event + * @name XrPlaneDetection#available + * @description Fired when plane detection becomes available. + */ + + /** + * @event + * @name XrPlaneDetection#unavailable + * @description Fired when plane detection becomes unavailable. + */ + + /** + * @event + * @name XrPlaneDetection#add + * @description Fired when new {@link XrPlane} is added to the list. + * @param {XrPlane} plane - Plane that has been added. + * @example + * app.xr.planeDetection.on('add', function (plane) { + * // new plane is added + * }); + */ + + /** + * @event + * @name XrPlaneDetection#remove + * @description Fired when a {@link XrPlane} is removed from the list. + * @param {XrPlane} plane - Plane that has been removed. + * @example + * app.xr.planeDetection.on('remove', function (plane) { + * // new plane is removed + * }); + */ + + _onSessionEnd() { + for (let i = 0; i < this._planes.length; i++) { + this._planes[i].destroy(); + } + this._planesIndex.clear(); + this._planes = null; + + if (this._available) { + this._available = false; + this.fire('unavailable'); + } + } + + update(frame) { + let detectedPlanes; + + if (! this._available) { + try { + detectedPlanes = frame.detectedPlanes; + this._planes = []; + this._available = true; + this.fire('available'); + } catch (ex) { + return; + } + } else { + detectedPlanes = frame.detectedPlanes; + } + + // iterate through indexed planes + for (const [xrPlane, plane] of this._planesIndex) { + if (detectedPlanes.has(xrPlane)) + continue; + + // if indexed plane is not listed in detectedPlanes anymore + // then remove it + this._planesIndex.delete(xrPlane); + this._planes.splice(this._planes.indexOf(plane), 1); + plane.destroy(); + this.fire('remove', plane); + } + + // iterate through detected planes + for (const xrPlane of detectedPlanes) { + let plane = this._planesIndex.get(xrPlane); + + if (! plane) { + // detected plane is not indexed + // then create new XrPlane + plane = new XrPlane(this, xrPlane); + this._planesIndex.set(xrPlane, plane); + this._planes.push(plane); + plane.update(frame); + this.fire('add', plane); + } else { + // if already indexed, just update + plane.update(frame); + } + } + } + + get supported() { + return this._supported; + } + + get available() { + return this._available; + } + + get planes() { + return this._planes; + } +} + +export { XrPlaneDetection }; diff --git a/src/xr/xr-plane.js b/src/xr/xr-plane.js new file mode 100644 index 00000000000..1e2051d6349 --- /dev/null +++ b/src/xr/xr-plane.js @@ -0,0 +1,133 @@ +import { EventHandler } from '../core/event-handler.js'; +import { Vec3 } from '../math/vec3.js'; +import { Quat } from '../math/quat.js'; + +let ids = 0; + +/** + * @class + * @name XrPlane + * @classdesc Detected Plane instance that provides position, rotation and polygon points. Plane is a subject to change during its lifetime. + * @description Detected Plane instance that provides position, rotation and polygon points. Plane is a subject to change during its lifetime. + * @param {XrPlaneDetection} planeDetection - Plane detection system. + * @param {object} xrPlane - XRPlane that is instantiated by WebXR system. + * @property {number} id Unique identifier of a plane. + * @property {string|null} orientation Plane's specific orientation (horizontal or vertical) or null if orientation is anything else. + */ +class XrPlane extends EventHandler { + constructor(planeDetection, xrPlane) { + super(); + + this._id = ++ids; + + this._planeDetection = planeDetection; + this._manager = this._planeDetection._manager; + + this._xrPlane = xrPlane; + this._lastChangedTime = this._xrPlane.lastChangedTime; + this._orientation = this._xrPlane.orientation; + + this._position = new Vec3(); + this._rotation = new Quat(); + } + + /** + * @event + * @name XrPlane#remove + * @description Fired when {@link XrPlane} is removed. + * @example + * plane.once('remove', function () { + * // plane is not available anymore + * }); + */ + + /** + * @event + * @name XrPlane#change + * @description Fired when {@link XrPlane} attributes such as: orientation and/or points have been changed. Position and rotation can change at any time without triggering a `change` event. + * @example + * plane.on('change', function () { + * // plane has been changed + * }); + */ + + destroy() { + this.fire('remove'); + } + + update(frame) { + const pose = frame.getPose(this._xrPlane.planeSpace, this._manager._referenceSpace); + if (pose) { + this._position.copy(pose.transform.position); + this._rotation.copy(pose.transform.orientation); + } + + // has not changed + if (this._lastChangedTime !== this._xrPlane.lastChangedTime) { + this._lastChangedTime = this._xrPlane.lastChangedTime; + + // attributes have been changed + this.fire('change'); + } + } + + /** + * @function + * @name XrPlane#getPosition + * @description Get the world space position of a plane. + * @returns {Vec3} The world space position of a plane. + */ + getPosition() { + return this._position; + } + + /** + * @function + * @name XrPlane#getRotation + * @description Get the world space rotation of a plane. + * @returns {Quat} The world space rotation of a plane. + */ + getRotation() { + return this._rotation; + } + + get id() { + return this.id; + } + + get orientation() { + return this._orientation; + } + + /** + * @name XrPlane#points + * @type {object[]} + * @description Array of DOMPointReadOnly objects. DOMPointReadOnly is an object with `x y z` properties that defines a local point of a plane's polygon. + * @example + * // prepare reusable objects + * var vecA = new pc.Vec3(); + * var vecB = new pc.Vec3(); + * var color = new pc.Color(1, 1, 1); + * + * // update Mat4 to plane position and rotation + * transform.setTRS(plane.getPosition(), plane.getRotation(), pc.Vec3.ONE); + * + * // draw lines between points + * for (var i = 0; i < plane.points.length; i++) { + * vecA.copy(plane.points[i]); + * vecB.copy(plane.points[(i + 1) % plane.points.length]); + * + * // transform from planes local to world coords + * transform.transformPoint(vecA, vecA); + * transform.transformPoint(vecB, vecB); + * + * // render line + * app.renderLine(vecA, vecB, color); + * } + */ + get points() { + return this._xrPlane.polygon; + } +} + +export { XrPlane };