浏览代码

WebXR Abstract feature
The `attach` and `detach` functions were modified to have better architecture
It is now possible to add callbacks that will automatically be removed when needed
`dispoe` works the same way, and so does `_attached`

Raanan Weber 5 年之前
父节点
当前提交
c9ed772a2d

+ 1 - 1
dist/preview release/what's new.md

@@ -179,7 +179,7 @@
 - Added support to teleport the camera at constant speed in the VRExperienceHelper class ([https://github.com/LeoRodz](https://github.com/LeoRodz))
 - VRExperienceHelper has now an XR fallback to force XR usage (Beta) ([RaananW](https://github.com/RaananW/))
 - Added option to change the teleportation easing function in the VRExperienceHelper class ([https://github.com/LeoRodz](https://github.com/LeoRodz))
-- Windows motion controller mapping corrected to XR ([RaananW](https://github.com/RaananW/))
+- Windows motion controller mapping corrected to XR (xr-standard) ([RaananW](https://github.com/RaananW/))
 - Pointer-Event simulation for screen target ray mode ([RaananW](https://github.com/RaananW/))
 - New observable that triggers when a session was initialized ([RaananW](https://github.com/RaananW/))
 - WebXR teleportation can now be disabled after initialized ([RaananW](https://github.com/RaananW/))

+ 75 - 0
src/Cameras/XR/features/WebXRAbstractFeature.ts

@@ -0,0 +1,75 @@
+import { IWebXRFeature } from '../webXRFeaturesManager';
+import { Observer, Observable, EventState } from '../../../Misc/observable';
+import { Nullable } from '../../../types';
+import { WebXRSessionManager } from '../webXRSessionManager';
+
+export abstract class WebXRAbstractFeature implements IWebXRFeature {
+
+    constructor(protected _xrSessionManager: WebXRSessionManager) {
+
+    }
+
+    private _attached: boolean = false;
+    private _removeOnDetach: {
+        observer: Nullable<Observer<any>>;
+        observable: Observable<any>;
+    }[] = [];
+
+    /**
+     * Is this feature attached
+     */
+    public get attached() {
+        return this._attached;
+    }
+
+    /**
+     * attach this feature
+     *
+     * @returns true if successful.
+     */
+    public attach(): boolean {
+        this._attached = true;
+        this._addNewAttachObserver(this._xrSessionManager.onXRFrameObservable, (frame) => this._onXRFrame(frame));
+        return true;
+    }
+
+    /**
+     * detach this feature.
+     *
+     * @returns true if successful.
+     */
+    public detach(): boolean {
+        this._attached = false;
+        this._removeOnDetach.forEach((toRemove) => {
+            toRemove.observable.remove(toRemove.observer);
+        });
+        return true;
+    }
+    /**
+     * Dispose this feature and all of the resources attached
+     */
+    public dispose(): void {
+        this.detach();
+    }
+
+    /**
+     * Code in this function will be executed on each xrFrame received from the browser.
+     * This function will not execute after the feature is detached.
+     * @param _xrFrame the current frame
+     */
+    protected _onXRFrame(_xrFrame: XRFrame): void {
+        // no-op
+    }
+
+    /**
+     * This is used to register callbacks that will automatically be removed when detach is called.
+     * @param observable the observable to which the observer will be attached
+     * @param callback the callback to register
+     */
+    protected _addNewAttachObserver<T>(observable: Observable<T>, callback: (eventData: T, eventState: EventState) => void) {
+        this._removeOnDetach.push({
+            observable,
+            observer: observable.add(callback)
+        });
+    }
+}

+ 42 - 56
src/Cameras/XR/features/WebXRAnchorSystem.ts

@@ -1,11 +1,11 @@
 import { IWebXRFeature, WebXRFeaturesManager } from '../webXRFeaturesManager';
 import { WebXRSessionManager } from '../webXRSessionManager';
-import { Observable, Observer } from '../../../Misc/observable';
+import { Observable } from '../../../Misc/observable';
 import { Matrix } from '../../../Maths/math.vector';
 import { TransformNode } from '../../../Meshes/transformNode';
 import { WebXRPlaneDetector } from './WebXRPlaneDetector';
-import { Nullable } from '../../../types';
 import { WebXRHitTestLegacy } from './WebXRHitTestLegacy';
+import { WebXRAbstractFeature } from './WebXRAbstractFeature';
 
 const Name = "xr-anchor-system";
 
@@ -54,7 +54,7 @@ let anchorIdProvider = 0;
  * will use the frame to create an anchor and not the session or a detected plane
  * For further information see https://github.com/immersive-web/anchors/
  */
-export class WebXRAnchorSystem implements IWebXRFeature {
+export class WebXRAnchorSystem extends WebXRAbstractFeature implements IWebXRFeature {
 
     /**
      * The module's name
@@ -81,27 +81,20 @@ export class WebXRAnchorSystem implements IWebXRFeature {
      */
     public onAnchorRemovedObservable: Observable<IWebXRAnchor> = new Observable();
 
-    private _attached: boolean = false;
-    /**
-     * Is this feature attached
-     */
-    public get attached() {
-        return this._attached;
-    }
     private _planeDetector: WebXRPlaneDetector;
     private _hitTestModule: WebXRHitTestLegacy;
 
     private _enabled: boolean = false;
     private _trackedAnchors: Array<IWebXRAnchor> = [];
     private _lastFrameDetected: XRAnchorSet = new Set();
-    private _observerTracked: Nullable<Observer<XRFrame>>;
 
     /**
      * constructs a new anchor system
      * @param _xrSessionManager an instance of WebXRSessionManager
      * @param _options configuration object for this feature
      */
-    constructor(private _xrSessionManager: WebXRSessionManager, private _options: IWebXRAnchorSystemOptions = {}) {
+    constructor(_xrSessionManager: WebXRSessionManager, private _options: IWebXRAnchorSystemOptions = {}) {
+        super(_xrSessionManager);
     }
 
     /**
@@ -129,47 +122,10 @@ export class WebXRAnchorSystem implements IWebXRFeature {
      * @returns true if successful.
      */
     attach(): boolean {
-        this._observerTracked = this._xrSessionManager.onXRFrameObservable.add(() => {
-            const frame = this._xrSessionManager.currentFrame;
-            if (!this._attached || !this._enabled || !frame) { return; }
-            // const timestamp = this.xrSessionManager.currentTimestamp;
-
-            const trackedAnchors = frame.trackedAnchors;
-            if (trackedAnchors && trackedAnchors.size) {
-                this._trackedAnchors.filter((anchor) => !trackedAnchors.has(anchor.xrAnchor)).map((anchor) => {
-                    const index = this._trackedAnchors.indexOf(anchor);
-                    this._trackedAnchors.splice(index, 1);
-                    this.onAnchorRemovedObservable.notifyObservers(anchor);
-                });
-                // now check for new ones
-                trackedAnchors.forEach((xrAnchor) => {
-                    if (!this._lastFrameDetected.has(xrAnchor)) {
-                        const newAnchor: Partial<IWebXRAnchor> = {
-                            id: anchorIdProvider++,
-                            xrAnchor: xrAnchor
-                        };
-                        const plane = this._updateAnchorWithXRFrame(xrAnchor, newAnchor, frame);
-                        this._trackedAnchors.push(plane);
-                        this.onAnchorAddedObservable.notifyObservers(plane);
-                    } else {
-                        // updated?
-                        if (xrAnchor.lastChangedTime === this._xrSessionManager.currentTimestamp) {
-                            let index = this._findIndexInAnchorArray(xrAnchor);
-                            const anchor = this._trackedAnchors[index];
-                            this._updateAnchorWithXRFrame(xrAnchor, anchor, frame);
-                            this.onAnchorUpdatedObservable.notifyObservers(anchor);
-                        }
-                    }
-                });
-                this._lastFrameDetected = trackedAnchors;
-            }
-        });
-
+        super.attach();
         if (this._options.addAnchorOnSelect) {
             this._xrSessionManager.session.addEventListener('select', this._onSelect, false);
         }
-
-        this._attached = true;
         return true;
     }
 
@@ -180,14 +136,10 @@ export class WebXRAnchorSystem implements IWebXRFeature {
      * @returns true if successful.
      */
     detach(): boolean {
-        this._attached = false;
+        super.detach();
 
         this._xrSessionManager.session.removeEventListener('select', this._onSelect);
 
-        if (this._observerTracked) {
-            this._xrSessionManager.onXRFrameObservable.remove(this._observerTracked);
-        }
-
         return true;
     }
 
@@ -195,12 +147,46 @@ export class WebXRAnchorSystem implements IWebXRFeature {
      * Dispose this feature and all of the resources attached
      */
     dispose(): void {
-        this.detach();
+        super.dispose();
         this.onAnchorAddedObservable.clear();
         this.onAnchorRemovedObservable.clear();
         this.onAnchorUpdatedObservable.clear();
     }
 
+    protected _onXRFrame(frame: XRFrame) {
+        if (!this.attached || !this._enabled || !frame) { return; }
+
+        const trackedAnchors = frame.trackedAnchors;
+        if (trackedAnchors && trackedAnchors.size) {
+            this._trackedAnchors.filter((anchor) => !trackedAnchors.has(anchor.xrAnchor)).map((anchor) => {
+                const index = this._trackedAnchors.indexOf(anchor);
+                this._trackedAnchors.splice(index, 1);
+                this.onAnchorRemovedObservable.notifyObservers(anchor);
+            });
+            // now check for new ones
+            trackedAnchors.forEach((xrAnchor) => {
+                if (!this._lastFrameDetected.has(xrAnchor)) {
+                    const newAnchor: Partial<IWebXRAnchor> = {
+                        id: anchorIdProvider++,
+                        xrAnchor: xrAnchor
+                    };
+                    const plane = this._updateAnchorWithXRFrame(xrAnchor, newAnchor, frame);
+                    this._trackedAnchors.push(plane);
+                    this.onAnchorAddedObservable.notifyObservers(plane);
+                } else {
+                    // updated?
+                    if (xrAnchor.lastChangedTime === this._xrSessionManager.currentTimestamp) {
+                        let index = this._findIndexInAnchorArray(xrAnchor);
+                        const anchor = this._trackedAnchors[index];
+                        this._updateAnchorWithXRFrame(xrAnchor, anchor, frame);
+                        this.onAnchorUpdatedObservable.notifyObservers(anchor);
+                    }
+                }
+            });
+            this._lastFrameDetected = trackedAnchors;
+        }
+    }
+
     private _onSelect = (event: XRInputSourceEvent) => {
         if (!this._options.addAnchorOnSelect) {
             return;

+ 7 - 20
src/Cameras/XR/features/WebXRBackgroundRemover.ts

@@ -2,6 +2,7 @@ import { WebXRFeaturesManager, IWebXRFeature } from "../webXRFeaturesManager";
 import { WebXRSessionManager } from '../webXRSessionManager';
 import { AbstractMesh } from '../../../Meshes/abstractMesh';
 import { Observable } from '../../../Misc/observable';
+import { WebXRAbstractFeature } from './WebXRAbstractFeature';
 
 const Name = "xr-background-remover";
 
@@ -36,7 +37,7 @@ export interface IWebXRBackgroundRemoverOptions {
 /**
  * A module that will automatically disable background meshes when entering AR and will enable them when leaving AR.
  */
-export class WebXRBackgroundRemover implements IWebXRFeature {
+export class WebXRBackgroundRemover extends WebXRAbstractFeature implements IWebXRFeature {
 
     /**
      * The module's name
@@ -54,25 +55,17 @@ export class WebXRBackgroundRemover implements IWebXRFeature {
      */
     public onBackgroundStateChangedObservable: Observable<boolean> = new Observable();
 
-    private _attached: boolean = false;
-    /**
-     * Is this feature attached
-     */
-    public get attached() {
-        return this._attached;
-    }
-
     /**
      * constructs a new background remover module
      * @param _xrSessionManager the session manager for this module
      * @param options read-only options to be used in this module
      */
-    constructor(private _xrSessionManager: WebXRSessionManager,
+    constructor(_xrSessionManager: WebXRSessionManager,
         /**
          * read-only options to be used in this module
          */
         public readonly options: IWebXRBackgroundRemoverOptions = {}) {
-
+        super(_xrSessionManager);
     }
 
     /**
@@ -83,10 +76,7 @@ export class WebXRBackgroundRemover implements IWebXRFeature {
      */
     attach(): boolean {
         this._setBackgroundState(false);
-
-        this._attached = true;
-
-        return true;
+        return super.attach();
     }
 
     /**
@@ -97,10 +87,7 @@ export class WebXRBackgroundRemover implements IWebXRFeature {
      */
     detach(): boolean {
         this._setBackgroundState(true);
-
-        this._attached = false;
-
-        return true;
+        return super.detach();
     }
 
     private _setBackgroundState(newState: boolean) {
@@ -138,7 +125,7 @@ export class WebXRBackgroundRemover implements IWebXRFeature {
      * Dispose this feature and all of the resources attached
      */
     dispose(): void {
-        this.detach();
+        super.dispose();
         this.onBackgroundStateChangedObservable.clear();
     }
 }

+ 45 - 74
src/Cameras/XR/features/WebXRControllerPointerSelection.ts

@@ -15,6 +15,7 @@ import { CylinderBuilder } from '../../../Meshes/Builders/cylinderBuilder';
 import { TorusBuilder } from '../../../Meshes/Builders/torusBuilder';
 import { Ray } from '../../../Culling/ray';
 import { PickingInfo } from '../../../Collisions/pickingInfo';
+import { WebXRAbstractFeature } from './WebXRAbstractFeature';
 
 const Name = "xr-controller-pointer-selection";
 
@@ -61,7 +62,7 @@ export interface IWebXRControllerPointerSelectionOptions {
 /**
  * A module that will enable pointer selection for motion controllers of XR Input Sources
  */
-export class WebXRControllerPointerSelection implements IWebXRFeature {
+export class WebXRControllerPointerSelection extends WebXRAbstractFeature implements IWebXRFeature {
 
     /**
      * The module's name
@@ -93,10 +94,6 @@ export class WebXRControllerPointerSelection implements IWebXRFeature {
 
     private static _idCounter = 0;
 
-    private _observerTracked: Nullable<Observer<XRFrame>>;
-    private _observerControllerAdded: Nullable<Observer<WebXRController>>;
-    private _observerControllerRemoved: Nullable<Observer<WebXRController>>;
-    private _attached: boolean = false;
     private _tmpRay = new Ray(new Vector3(), new Vector3());
 
     private _controllers: {
@@ -112,13 +109,6 @@ export class WebXRControllerPointerSelection implements IWebXRFeature {
         };
     } = {};
 
-    /**
-     * Is this feature attached
-     */
-    public get attached() {
-        return this._attached;
-    }
-
     private _scene: Scene;
 
     /**
@@ -126,7 +116,8 @@ export class WebXRControllerPointerSelection implements IWebXRFeature {
      * @param _xrSessionManager the session manager for this module
      * @param _options read-only options to be used in this module
      */
-    constructor(private _xrSessionManager: WebXRSessionManager, private readonly _options: IWebXRControllerPointerSelectionOptions) {
+    constructor(_xrSessionManager: WebXRSessionManager, private readonly _options: IWebXRControllerPointerSelectionOptions) {
+        super(_xrSessionManager);
         this._scene = this._xrSessionManager.scene;
     }
 
@@ -137,53 +128,15 @@ export class WebXRControllerPointerSelection implements IWebXRFeature {
      * @returns true if successful.
      */
     attach(): boolean {
+        super.attach();
 
         this._options.xrInput.controllers.forEach(this._attachController);
-        this._options.xrInput.onControllerAddedObservable.add(this._attachController);
-        this._options.xrInput.onControllerRemovedObservable.add((controller) => {
+        this._addNewAttachObserver(this._options.xrInput.onControllerAddedObservable, this._attachController);
+        this._addNewAttachObserver(this._options.xrInput.onControllerRemovedObservable, (controller) => {
             // REMOVE the controller
             this._detachController(controller.uniqueId);
         });
 
-        this._observerTracked = this._xrSessionManager.onXRFrameObservable.add(() => {
-            Object.keys(this._controllers).forEach((id) => {
-                const controllerData = this._controllers[id];
-
-                // Every frame check collisions/input
-                controllerData.xrController.getWorldPointerRayToRef(this._tmpRay);
-                controllerData.pick = this._scene.pickWithRay(this._tmpRay);
-
-                const pick = controllerData.pick;
-
-                if (pick && pick.pickedPoint && pick.hit) {
-                    // Update laser state
-                    this._updatePointerDistance(controllerData.laserPointer, pick.distance);
-
-                    // Update cursor state
-                    controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
-                    controllerData.selectionMesh.scaling.x = Math.sqrt(pick.distance);
-                    controllerData.selectionMesh.scaling.y = Math.sqrt(pick.distance);
-                    controllerData.selectionMesh.scaling.z = Math.sqrt(pick.distance);
-
-                    // To avoid z-fighting
-                    let pickNormal = this._convertNormalToDirectionOfRay(pick.getNormal(true), this._tmpRay);
-                    let deltaFighting = 0.001;
-                    controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
-                    if (pickNormal) {
-                        let axis1 = Vector3.Cross(Axis.Y, pickNormal);
-                        let axis2 = Vector3.Cross(pickNormal, axis1);
-                        Vector3.RotationFromAxisToRef(axis2, pickNormal, axis1, controllerData.selectionMesh.rotation);
-                        controllerData.selectionMesh.position.addInPlace(pickNormal.scale(deltaFighting));
-                    }
-                    controllerData.selectionMesh.isVisible = true;
-                } else {
-                    controllerData.selectionMesh.isVisible = false;
-                }
-            });
-        });
-
-        this._attached = true;
-
         return true;
     }
 
@@ -194,24 +147,12 @@ export class WebXRControllerPointerSelection implements IWebXRFeature {
      * @returns true if successful.
      */
     detach(): boolean {
-
-        if (this._observerTracked) {
-            this._xrSessionManager.onXRFrameObservable.remove(this._observerTracked);
-        }
+        super.detach();
 
         Object.keys(this._controllers).forEach((controllerId) => {
             this._detachController(controllerId);
         });
 
-        if (this._observerControllerAdded) {
-            this._options.xrInput.onControllerAddedObservable.remove(this._observerControllerAdded);
-        }
-        if (this._observerControllerRemoved) {
-            this._options.xrInput.onControllerRemovedObservable.remove(this._observerControllerRemoved);
-        }
-
-        this._attached = false;
-
         return true;
     }
 
@@ -232,6 +173,43 @@ export class WebXRControllerPointerSelection implements IWebXRFeature {
         return null;
     }
 
+    protected _onXRFrame(_xrFrame: XRFrame) {
+        Object.keys(this._controllers).forEach((id) => {
+            const controllerData = this._controllers[id];
+
+            // Every frame check collisions/input
+            controllerData.xrController.getWorldPointerRayToRef(this._tmpRay);
+            controllerData.pick = this._scene.pickWithRay(this._tmpRay);
+
+            const pick = controllerData.pick;
+
+            if (pick && pick.pickedPoint && pick.hit) {
+                // Update laser state
+                this._updatePointerDistance(controllerData.laserPointer, pick.distance);
+
+                // Update cursor state
+                controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
+                controllerData.selectionMesh.scaling.x = Math.sqrt(pick.distance);
+                controllerData.selectionMesh.scaling.y = Math.sqrt(pick.distance);
+                controllerData.selectionMesh.scaling.z = Math.sqrt(pick.distance);
+
+                // To avoid z-fighting
+                let pickNormal = this._convertNormalToDirectionOfRay(pick.getNormal(true), this._tmpRay);
+                let deltaFighting = 0.001;
+                controllerData.selectionMesh.position.copyFrom(pick.pickedPoint);
+                if (pickNormal) {
+                    let axis1 = Vector3.Cross(Axis.Y, pickNormal);
+                    let axis2 = Vector3.Cross(pickNormal, axis1);
+                    Vector3.RotationFromAxisToRef(axis2, pickNormal, axis1, controllerData.selectionMesh.rotation);
+                    controllerData.selectionMesh.position.addInPlace(pickNormal.scale(deltaFighting));
+                }
+                controllerData.selectionMesh.isVisible = true;
+            } else {
+                controllerData.selectionMesh.isVisible = false;
+            }
+        });
+    }
+
     private _attachController = (xrController: WebXRController) => {
         if (this._controllers[xrController.uniqueId]) {
             // already attached
@@ -472,13 +450,6 @@ export class WebXRControllerPointerSelection implements IWebXRFeature {
         _laserPointer.scaling.y = distance;
         _laserPointer.position.z = distance / 2;
     }
-
-    /**
-     * Dispose this feature and all of the resources attached
-     */
-    dispose(): void {
-        this.detach();
-    }
 }
 
 //register the plugin

+ 31 - 65
src/Cameras/XR/features/WebXRControllerTeleportation.ts

@@ -20,6 +20,7 @@ import { TorusBuilder } from '../../../Meshes/Builders/torusBuilder';
 import { PickingInfo } from '../../../Collisions/pickingInfo';
 import { Curve3 } from '../../../Maths/math.path';
 import { LinesBuilder } from '../../../Meshes/Builders/linesBuilder';
+import { WebXRAbstractFeature } from './WebXRAbstractFeature';
 
 const Name = "xr-controller-teleportation";
 
@@ -83,7 +84,7 @@ export interface IWebXRTeleportationOptions {
  * When enabled and attached, the feature will allow a user to move aroundand rotate in the scene using
  * the input of the attached controllers.
  */
-export class WebXRMotionControllerTeleportation implements IWebXRFeature {
+export class WebXRMotionControllerTeleportation extends WebXRAbstractFeature implements IWebXRFeature {
     /**
      * The module's name
      */
@@ -122,18 +123,6 @@ export class WebXRMotionControllerTeleportation implements IWebXRFeature {
      */
     public backwardsTeleportationDistance: number = 0.5;
 
-    private _observerTracked: Nullable<Observer<XRFrame>>;
-    private _observerControllerAdded: Nullable<Observer<WebXRController>>;
-    private _observerControllerRemoved: Nullable<Observer<WebXRController>>;
-
-    private _attached: boolean = false;
-    /**
-     * Is this feature attached
-     */
-    public get attached() {
-        return this._attached;
-    }
-
     /**
      * Add a new mesh to the floor meshes array
      * @param mesh the mesh to use as floor mesh
@@ -188,7 +177,8 @@ export class WebXRMotionControllerTeleportation implements IWebXRFeature {
      * @param _xrSessionManager an instance of WebXRSessionManager
      * @param _options configuration object for this feature
      */
-    constructor(private _xrSessionManager: WebXRSessionManager, private _options: IWebXRTeleportationOptions) {
+    constructor(_xrSessionManager: WebXRSessionManager, private _options: IWebXRTeleportationOptions) {
+        super(_xrSessionManager);
         // create default mesh if not provided
         if (!this._options.teleportationTargetMesh) {
             this.createDefaultTargetMesh();
@@ -209,25 +199,40 @@ export class WebXRMotionControllerTeleportation implements IWebXRFeature {
         this._selectionFeature = selectionFeature;
     }
 
-    /**
-     * attach this feature
-     * Will usually be called by the features manager
-     *
-     * @returns true if successful.
-     */
-    attach(): boolean {
+    public attach(): boolean {
+        super.attach();
 
         this._options.xrInput.controllers.forEach(this._attachController);
-        this._observerControllerAdded = this._options.xrInput.onControllerAddedObservable.add(this._attachController);
-        this._observerControllerRemoved = this._options.xrInput.onControllerRemovedObservable.add((controller) => {
+        this._addNewAttachObserver(this._options.xrInput.onControllerAddedObservable, this._attachController);
+        this._addNewAttachObserver(this._options.xrInput.onControllerRemovedObservable, (controller) => {
             // REMOVE the controller
             this._detachController(controller.uniqueId);
         });
 
-        this._observerTracked = this._xrSessionManager.onXRFrameObservable.add(() => {
-            const frame = this._xrSessionManager.currentFrame;
+        return true;
+    }
+
+    public detach(): boolean {
+        super.detach();
+
+        Object.keys(this._controllers).forEach((controllerId) => {
+            this._detachController(controllerId);
+        });
+
+        this.setTargetMeshVisibility(false);
+
+        return true;
+    }
+
+    public dispose(): void {
+        super.dispose();
+        this._options.teleportationTargetMesh && this._options.teleportationTargetMesh.dispose(false, true);
+    }
+
+    protected _onXRFrame(_xrFrame: XRFrame) {
+        const frame = this._xrSessionManager.currentFrame;
             const scene = this._xrSessionManager.scene;
-            if (!this._attached || !frame) { return; }
+            if (!this.attach || !frame) { return; }
 
             // render target if needed
             const targetMesh = this._options.teleportationTargetMesh;
@@ -283,45 +288,6 @@ export class WebXRMotionControllerTeleportation implements IWebXRFeature {
             } else {
                 this.setTargetMeshVisibility(false);
             }
-        });
-
-        this._attached = true;
-        return true;
-    }
-
-    /**
-     * detach this feature.
-     * Will usually be called by the features manager
-     *
-     * @returns true if successful.
-     */
-    detach(): boolean {
-        this._attached = false;
-
-        if (this._observerTracked) {
-            this._xrSessionManager.onXRFrameObservable.remove(this._observerTracked);
-        }
-
-        Object.keys(this._controllers).forEach((controllerId) => {
-            this._detachController(controllerId);
-        });
-
-        if (this._observerControllerAdded) {
-            this._options.xrInput.onControllerAddedObservable.remove(this._observerControllerAdded);
-        }
-        if (this._observerControllerRemoved) {
-            this._options.xrInput.onControllerRemovedObservable.remove(this._observerControllerRemoved);
-        }
-
-        return true;
-    }
-
-    /**
-     * Dispose this feature and all of the resources attached
-     */
-    dispose(): void {
-        this.detach();
-        this._options.teleportationTargetMesh && this._options.teleportationTargetMesh.dispose(false, true);
     }
 
     private _currentTeleportationControllerId: string;

+ 32 - 45
src/Cameras/XR/features/WebXRHitTestLegacy.ts

@@ -1,9 +1,9 @@
 import { IWebXRFeature, WebXRFeaturesManager } from '../webXRFeaturesManager';
 import { WebXRSessionManager } from '../webXRSessionManager';
-import { Observable, Observer } from '../../../Misc/observable';
+import { Observable } from '../../../Misc/observable';
 import { Vector3, Matrix } from '../../../Maths/math.vector';
 import { TransformNode } from '../../../Meshes/transformNode';
-import { Nullable } from '../../../types';
+import { WebXRAbstractFeature } from './WebXRAbstractFeature';
 
 /**
  * name of module (can be reused with other versions)
@@ -45,7 +45,7 @@ export interface IWebXRHitResult {
  * 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 implements IWebXRFeature {
+export class WebXRHitTestLegacy extends WebXRAbstractFeature implements IWebXRFeature {
 
     /**
      * The module's name
@@ -58,14 +58,6 @@ export class WebXRHitTestLegacy implements IWebXRFeature {
      */
     public static readonly Version = 1;
 
-    private _attached: boolean = false;
-    /**
-     * Is this feature attached
-     */
-    public get attached() {
-        return this._attached;
-    }
-
     /**
      * 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
@@ -104,18 +96,17 @@ export class WebXRHitTestLegacy implements IWebXRFeature {
     public onHitTestResultObservable: Observable<IWebXRHitResult[]> = new Observable();
 
     private _onSelectEnabled = false;
-    private _xrFrameObserver: Nullable<Observer<XRFrame>>;
     /**
      * 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(private _xrSessionManager: WebXRSessionManager,
+    constructor(_xrSessionManager: WebXRSessionManager,
         /**
          * options to use when constructing this feature
          */
         public readonly options: IWebXRHitTestOptions = {}) {
-
+        super(_xrSessionManager);
     }
 
     /**
@@ -130,34 +121,10 @@ export class WebXRHitTestLegacy implements IWebXRFeature {
      * @returns true if successful.
      */
     attach(): boolean {
+        super.attach();
         if (this.options.testOnPointerDownOnly) {
             this._xrSessionManager.session.addEventListener('select', this._onSelect, false);
-        } else {
-            // we are in XR space!
-            const origin = new Vector3(0, 0, 0);
-            // in XR space z-forward is negative
-            const direction = new Vector3(0, 0, -1);
-            const mat = new Matrix();
-            this._xrFrameObserver = this._xrSessionManager.onXRFrameObservable.add((frame) => {
-                // make sure we do nothing if (async) not attached
-                if (!this._attached) {
-                    return;
-                }
-                let pose = frame.getViewerPose(this._xrSessionManager.referenceSpace);
-                if (!pose) {
-                    return;
-                }
-                Matrix.FromArrayToRef(pose.transform.matrix, 0, mat);
-                Vector3.TransformCoordinatesFromFloatsToRef(0, 0, 0, mat, origin);
-                Vector3.TransformCoordinatesFromFloatsToRef(0, 0, -1, mat, direction);
-                direction.subtractInPlace(origin);
-                direction.normalize();
-                let ray = new XRRay((<DOMPointReadOnly>{ x: origin.x, y: origin.y, z: origin.z, w: 0 }),
-                    (<DOMPointReadOnly>{ x: direction.x, y: direction.y, z: direction.z, w: 0 }));
-                WebXRHitTestLegacy.XRHitTestWithRay(this._xrSessionManager.session, ray, this._xrSessionManager.referenceSpace).then(this._onHitTestResults);
-            });
         }
-        this._attached = true;
 
         return true;
     }
@@ -169,14 +136,10 @@ export class WebXRHitTestLegacy implements IWebXRFeature {
      * @returns true if successful.
      */
     detach(): boolean {
+        super.detach();
         // disable select
         this._onSelectEnabled = false;
         this._xrSessionManager.session.removeEventListener('select', this._onSelect);
-        if (this._xrFrameObserver) {
-            this._xrSessionManager.onXRFrameObservable.remove(this._xrFrameObserver);
-            this._xrFrameObserver = null;
-        }
-        this._attached = false;
         return true;
     }
 
@@ -200,6 +163,30 @@ export class WebXRHitTestLegacy implements IWebXRFeature {
         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((<DOMPointReadOnly>{ x: this._origin.x, y: this._origin.y, z: this._origin.z, w: 0 }),
+            (<DOMPointReadOnly>{ 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) {
@@ -212,7 +199,7 @@ export class WebXRHitTestLegacy implements IWebXRFeature {
      * Dispose this feature and all of the resources attached
      */
     dispose(): void {
-        this.detach();
+        super.dispose(); ;
         this.onHitTestResultObservable.clear();
     }
 }

+ 37 - 73
src/Cameras/XR/features/WebXRPlaneDetector.ts

@@ -1,9 +1,9 @@
 import { WebXRFeaturesManager, IWebXRFeature } from '../webXRFeaturesManager';
 import { TransformNode } from '../../../Meshes/transformNode';
 import { WebXRSessionManager } from '../webXRSessionManager';
-import { Observable, Observer } from '../../../Misc/observable';
+import { Observable } from '../../../Misc/observable';
 import { Vector3, Matrix } from '../../../Maths/math.vector';
-import { Nullable } from '../../../types';
+import { WebXRAbstractFeature } from './WebXRAbstractFeature';
 
 const Name = "xr-plane-detector";
 
@@ -47,7 +47,7 @@ let planeIdProvider = 0;
  * The plane detector is used to detect planes in the real world when in AR
  * For more information see https://github.com/immersive-web/real-world-geometry/
  */
-export class WebXRPlaneDetector implements IWebXRFeature {
+export class WebXRPlaneDetector extends WebXRAbstractFeature implements IWebXRFeature {
 
     /**
      * The module's name
@@ -74,24 +74,17 @@ export class WebXRPlaneDetector implements IWebXRFeature {
      */
     public onPlaneUpdatedObservable: Observable<IWebXRPlane> = new Observable();
 
-    private _attached: boolean = false;
-    /**
-     * Is this feature attached
-     */
-    public get attached() {
-        return this._attached;
-    }
     private _enabled: boolean = false;
     private _detectedPlanes: Array<IWebXRPlane> = [];
     private _lastFrameDetected: XRPlaneSet = new Set();
-    private _observerTracked: Nullable<Observer<XRFrame>>;
 
     /**
      * construct a new Plane Detector
      * @param _xrSessionManager an instance of xr Session manager
      * @param _options configuration to use when constructing this feature
      */
-    constructor(private _xrSessionManager: WebXRSessionManager, private _options: IWebXRPlaneDetectorOptions = {}) {
+    constructor(_xrSessionManager: WebXRSessionManager, private _options: IWebXRPlaneDetectorOptions = {}) {
+        super(_xrSessionManager);
         if (this._xrSessionManager.session) {
             this._xrSessionManager.session.updateWorldTrackingState({ planeDetectionState: { enabled: true } });
             this._enabled = true;
@@ -103,76 +96,47 @@ export class WebXRPlaneDetector implements IWebXRFeature {
         }
     }
 
-    /**
-     * attach this feature
-     * Will usually be called by the features manager
-     *
-     * @returns true if successful.
-     */
-    attach(): boolean {
+    protected _onXRFrame(frame: XRFrame) {
+        if (!this.attached || !this._enabled || !frame) { return; }
+        // const timestamp = this.xrSessionManager.currentTimestamp;
 
-        this._observerTracked = this._xrSessionManager.onXRFrameObservable.add(() => {
-            const frame = this._xrSessionManager.currentFrame;
-            if (!this._attached || !this._enabled || !frame) { return; }
-            // const timestamp = this.xrSessionManager.currentTimestamp;
-
-            const detectedPlanes = frame.worldInformation.detectedPlanes;
-            if (detectedPlanes && detectedPlanes.size) {
-                this._detectedPlanes.filter((plane) => !detectedPlanes.has(plane.xrPlane)).map((plane) => {
-                    const index = this._detectedPlanes.indexOf(plane);
-                    this._detectedPlanes.splice(index, 1);
-                    this.onPlaneRemovedObservable.notifyObservers(plane);
-                });
-                // now check for new ones
-                detectedPlanes.forEach((xrPlane) => {
-                    if (!this._lastFrameDetected.has(xrPlane)) {
-                        const newPlane: Partial<IWebXRPlane> = {
-                            id: planeIdProvider++,
-                            xrPlane: xrPlane,
-                            polygonDefinition: []
-                        };
-                        const plane = this._updatePlaneWithXRPlane(xrPlane, newPlane, frame);
-                        this._detectedPlanes.push(plane);
-                        this.onPlaneAddedObservable.notifyObservers(plane);
-                    } else {
-                        // updated?
-                        if (xrPlane.lastChangedTime === this._xrSessionManager.currentTimestamp) {
-                            let index = this.findIndexInPlaneArray(xrPlane);
-                            const plane = this._detectedPlanes[index];
-                            this._updatePlaneWithXRPlane(xrPlane, plane, frame);
-                            this.onPlaneUpdatedObservable.notifyObservers(plane);
-                        }
+        const detectedPlanes = frame.worldInformation.detectedPlanes;
+        if (detectedPlanes && detectedPlanes.size) {
+            this._detectedPlanes.filter((plane) => !detectedPlanes.has(plane.xrPlane)).map((plane) => {
+                const index = this._detectedPlanes.indexOf(plane);
+                this._detectedPlanes.splice(index, 1);
+                this.onPlaneRemovedObservable.notifyObservers(plane);
+            });
+            // now check for new ones
+            detectedPlanes.forEach((xrPlane) => {
+                if (!this._lastFrameDetected.has(xrPlane)) {
+                    const newPlane: Partial<IWebXRPlane> = {
+                        id: planeIdProvider++,
+                        xrPlane: xrPlane,
+                        polygonDefinition: []
+                    };
+                    const plane = this._updatePlaneWithXRPlane(xrPlane, newPlane, frame);
+                    this._detectedPlanes.push(plane);
+                    this.onPlaneAddedObservable.notifyObservers(plane);
+                } else {
+                    // updated?
+                    if (xrPlane.lastChangedTime === this._xrSessionManager.currentTimestamp) {
+                        let index = this.findIndexInPlaneArray(xrPlane);
+                        const plane = this._detectedPlanes[index];
+                        this._updatePlaneWithXRPlane(xrPlane, plane, frame);
+                        this.onPlaneUpdatedObservable.notifyObservers(plane);
                     }
-                });
-                this._lastFrameDetected = detectedPlanes;
-            }
-        });
-
-        this._attached = true;
-        return true;
-    }
-
-    /**
-     * detach this feature.
-     * Will usually be called by the features manager
-     *
-     * @returns true if successful.
-     */
-    detach(): boolean {
-        this._attached = false;
-
-        if (this._observerTracked) {
-            this._xrSessionManager.onXRFrameObservable.remove(this._observerTracked);
+                }
+            });
+            this._lastFrameDetected = detectedPlanes;
         }
-
-        return true;
     }
 
     /**
      * Dispose this feature and all of the resources attached
      */
     dispose(): void {
-        this.detach();
+        super.dispose();
         this.onPlaneAddedObservable.clear();
         this.onPlaneRemovedObservable.clear();
         this.onPlaneUpdatedObservable.clear();