import { IWebXRFeature, WebXRFeaturesManager } from '../webXRFeaturesManager'; import { WebXRSessionManager } from '../webXRSessionManager'; import { Observable } from '../../../Misc/observable'; import { Vector3, Matrix } from '../../../Maths/math.vector'; import { TransformNode } from '../../../Meshes/transformNode'; import { WebXRAbstractFeature } from './WebXRAbstractFeature'; /** * name of module (can be reused with other versions) */ const WebXRHitTestModuleName = "xr-hit-test"; // the plugin is registered at the end of the file /** * Options used for hit testing */ export interface IWebXRHitTestOptions { /** * Only test when user interacted with the scene. Default - hit test every frame */ testOnPointerDownOnly?: boolean; /** * The node to use to transform the local results to world coordinates */ worldParentNode?: TransformNode; } /** * Interface defining the babylon result of raycasting/hit-test */ export interface IWebXRHitResult { /** * The native hit test result */ xrHitResult: XRHitResult; /** * Transformation matrix that can be applied to a node that will put it in the hit point location */ transformationMatrix: Matrix; } /** * The currently-working hit-test module. * Hit test (or raycasting) is used to interact with the real world. * For further information read here - https://github.com/immersive-web/hit-test */ export class WebXRHitTestLegacy extends WebXRAbstractFeature implements IWebXRFeature { /** * The module's name */ public static readonly Name = WebXRHitTestModuleName; /** * The (Babylon) version of this module. * This is an integer representing the implementation version. * This number does not correspond to the webxr specs version */ public static readonly Version = 1; /** * Execute a hit test on the current running session using a select event returned from a transient input (such as touch) * @param event the (select) event to use to select with * @param referenceSpace the reference space to use for this hit test * @returns a promise that resolves with an array of native XR hit result in xr coordinates system */ public static XRHitTestWithSelectEvent(event: XRInputSourceEvent, referenceSpace: XRReferenceSpace): Promise { let targetRayPose = event.frame.getPose(event.inputSource.targetRaySpace, referenceSpace); if (!targetRayPose) { return Promise.resolve([]); } let targetRay = new XRRay(targetRayPose.transform); return this.XRHitTestWithRay(event.frame.session, targetRay, referenceSpace); } /** * execute a hit test with an XR Ray * * @param xrSession a native xrSession that will execute this hit test * @param xrRay the ray (position and direction) to use for raycasting * @param referenceSpace native XR reference space to use for the hit-test * @param filter filter function that will filter the results * @returns a promise that resolves with an array of native XR hit result in xr coordinates system */ public static XRHitTestWithRay(xrSession: XRSession, xrRay: XRRay, referenceSpace: XRReferenceSpace, filter?: (result: XRHitResult) => boolean): Promise { return xrSession.requestHitTest(xrRay, referenceSpace).then((results) => { const filterFunction = filter || ((result) => !!result.hitMatrix); return results.filter(filterFunction); }); } /** * Triggered when new babylon (transformed) hit test results are available */ public onHitTestResultObservable: Observable = new Observable(); private _onSelectEnabled = false; /** * Creates a new instance of the (legacy version) hit test feature * @param _xrSessionManager an instance of WebXRSessionManager * @param options options to use when constructing this feature */ constructor(_xrSessionManager: WebXRSessionManager, /** * options to use when constructing this feature */ public readonly options: IWebXRHitTestOptions = {}) { super(_xrSessionManager); } /** * Populated with the last native XR Hit Results */ public lastNativeXRHitResults: XRHitResult[] = []; /** * attach this feature * Will usually be called by the features manager * * @returns true if successful. */ attach(): boolean { super.attach(); if (this.options.testOnPointerDownOnly) { this._xrSessionManager.session.addEventListener('select', this._onSelect, false); } return true; } /** * detach this feature. * Will usually be called by the features manager * * @returns true if successful. */ detach(): boolean { super.detach(); // disable select this._onSelectEnabled = false; this._xrSessionManager.session.removeEventListener('select', this._onSelect); return true; } private _onHitTestResults = (xrResults: XRHitResult[]) => { const mats = xrResults.map((result) => { let mat = Matrix.FromArray(result.hitMatrix); if (!this._xrSessionManager.scene.useRightHandedSystem) { mat.toggleModelMatrixHandInPlace(); } // if (this.options.coordinatesSpace === Space.WORLD) { if (this.options.worldParentNode) { mat.multiplyToRef(this.options.worldParentNode.getWorldMatrix(), mat); } return { xrHitResult: result, transformationMatrix: mat }; }); this.lastNativeXRHitResults = xrResults; this.onHitTestResultObservable.notifyObservers(mats); } private _origin = new Vector3(0, 0, 0); // in XR space z-forward is negative private _direction = new Vector3(0, 0, -1); private _mat = new Matrix(); protected _onXRFrame(frame: XRFrame) { // make sure we do nothing if (async) not attached if (!this.attached || this.options.testOnPointerDownOnly) { return; } let pose = frame.getViewerPose(this._xrSessionManager.referenceSpace); if (!pose) { return; } Matrix.FromArrayToRef(pose.transform.matrix, 0, this._mat); Vector3.TransformCoordinatesFromFloatsToRef(0, 0, 0, this._mat, this._origin); Vector3.TransformCoordinatesFromFloatsToRef(0, 0, -1, this._mat, this._direction); this._direction.subtractInPlace(this._origin); this._direction.normalize(); let ray = new XRRay(({ x: this._origin.x, y: this._origin.y, z: this._origin.z, w: 0 }), ({ x: this._direction.x, y: this._direction.y, z: this._direction.z, w: 0 })); WebXRHitTestLegacy.XRHitTestWithRay(this._xrSessionManager.session, ray, this._xrSessionManager.referenceSpace).then(this._onHitTestResults); } // can be done using pointerdown event, and xrSessionManager.currentFrame private _onSelect = (event: XRInputSourceEvent) => { if (!this._onSelectEnabled) { return; } WebXRHitTestLegacy.XRHitTestWithSelectEvent(event, this._xrSessionManager.referenceSpace); } /** * Dispose this feature and all of the resources attached */ dispose(): void { super.dispose(); this.onHitTestResultObservable.clear(); } } //register the plugin versions WebXRFeaturesManager.AddWebXRFeature(WebXRHitTestLegacy.Name, (xrSessionManager, options) => { return () => new WebXRHitTestLegacy(xrSessionManager, options); }, WebXRHitTestLegacy.Version, true);