Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/framework/components/camera/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
17 changes: 14 additions & 3 deletions src/xr/xr-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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 () {
Expand All @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
150 changes: 150 additions & 0 deletions src/xr/xr-plane-detection.js
Original file line number Diff line number Diff line change
@@ -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 };
133 changes: 133 additions & 0 deletions src/xr/xr-plane.js
Original file line number Diff line number Diff line change
@@ -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 };