Browse Source

Merge pull request #6568 from TrevorDev/xrVRParity

webXR helpers to get webVR parity
David Catuhe 6 years ago
parent
commit
37840a78ff

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

@@ -25,6 +25,7 @@
 - Effect renderer to render one or multiple shader effects to a texture ([TrevorDev](https://github.com/TrevorDev))
 - Added url parameters to web request modifiers ([PierreLeBlond](https://github.com/PierreLeBlond))
 - WebXR updated to spec as of June 27th ([TrevorDev](https://github.com/TrevorDev))
+- WebXR webVR parity helpers ([TrevorDev](https://github.com/TrevorDev))
 
 ### Engine
 - Morph targets now can morph UV channel as well ([Deltakosh](https://github.com/deltakosh/))

+ 4 - 0
src/Cameras/XR/index.ts

@@ -2,5 +2,9 @@ export * from "./webXRCamera";
 export * from "./webXREnterExitUI";
 export * from "./webXRExperienceHelper";
 export * from "./webXRInput";
+export * from "./webXRControllerTeleportation";
+export * from "./webXRControllerPointerSelection";
+export * from "./webXRControllerModelLoader";
+export * from "./webXRController";
 export * from "./webXRManagedOutputCanvas";
 export * from "./webXRSessionManager";

+ 116 - 0
src/Cameras/XR/webXRController.ts

@@ -0,0 +1,116 @@
+import { Nullable } from "../../types";
+import { Observable } from "../../Misc/observable";
+import { AbstractMesh } from "../../Meshes/abstractMesh";
+import { Matrix, Quaternion, Vector3 } from '../../Maths/math';
+import { Ray } from '../../Culling/ray';
+import { Scene } from '../../scene';
+/**
+ * Represents an XR input
+ */
+export class WebXRController {
+    /**
+     * Represents the part of the controller that is held. This may not exist if the controller is the head mounted display itself, if thats the case only the pointer from the head will be availible
+     */
+    public grip?: AbstractMesh;
+    /**
+     * Pointer which can be used to select objects or attach a visible laser to
+     */
+    public pointer: AbstractMesh;
+
+    /**
+     * Event that fires when the controller is removed/disposed
+     */
+    public onDisposeObservable = new Observable<{}>();
+
+    private _tmpMatrix = new Matrix();
+    private _tmpQuaternion = new Quaternion();
+    private _tmpVector = new Vector3();
+
+    /**
+     * Creates the controller
+     * @see https://doc.babylonjs.com/how_to/webxr
+     * @param scene the scene which the controller should be associated to
+     * @param inputSource the underlying input source for the controller
+     * @param parentContainer parent that the controller meshes should be children of
+     */
+    constructor(
+        private scene: Scene,
+        /** The underlying input source for the controller  */
+        public inputSource: XRInputSource,
+        private parentContainer: Nullable<AbstractMesh> = null)
+    {
+        this.pointer = new AbstractMesh("controllerPointer", scene);
+        if (parentContainer) {
+            parentContainer.addChild(this.pointer);
+        }
+
+        if (this.inputSource.gripSpace) {
+            this.grip = new AbstractMesh("controllerGrip", this.scene);
+            if (this.parentContainer) {
+                this.parentContainer.addChild(this.grip);
+            }
+        }
+    }
+
+    /**
+     * Updates the controller pose based on the given XRFrame
+     * @param xrFrame xr frame to update the pose with
+     * @param referenceSpace reference space to use
+     */
+    public updateFromXRFrame(xrFrame: XRFrame, referenceSpace: XRReferenceSpace) {
+        let pose = xrFrame.getPose(this.inputSource.targetRaySpace, referenceSpace);
+
+        // Update the pointer mesh
+        if (pose) {
+            Matrix.FromFloat32ArrayToRefScaled(pose.transform.matrix, 0, 1, this._tmpMatrix);
+            if (!this.pointer.getScene().useRightHandedSystem) {
+                this._tmpMatrix.toggleModelMatrixHandInPlace();
+            }
+            if (!this.pointer.rotationQuaternion) {
+                this.pointer.rotationQuaternion = new Quaternion();
+            }
+            this._tmpMatrix.decompose(this.pointer.scaling, this.pointer.rotationQuaternion!, this.pointer.position);
+        }
+
+        // Update the grip mesh if it exists
+        if (this.inputSource.gripSpace && this.grip) {
+            let pose = xrFrame.getPose(this.inputSource.gripSpace, referenceSpace);
+            if (pose) {
+                Matrix.FromFloat32ArrayToRefScaled(pose.transform.matrix, 0, 1, this._tmpMatrix);
+                if (!this.grip.getScene().useRightHandedSystem) {
+                    this._tmpMatrix.toggleModelMatrixHandInPlace();
+                }
+                if (!this.grip.rotationQuaternion) {
+                    this.grip.rotationQuaternion = new Quaternion();
+                }
+                this._tmpMatrix.decompose(this.grip.scaling, this.grip.rotationQuaternion!, this.grip.position);
+            }
+        }
+    }
+
+    /**
+     * Gets a world space ray coming from the controller
+     * @param result the resulting ray
+     */
+    public getWorldPointerRayToRef(result: Ray) {
+        // Force update to ensure picked point is synced with ray
+        let worldMatrix = this.pointer.computeWorldMatrix(true);
+        worldMatrix.decompose(undefined, this._tmpQuaternion, undefined);
+        this._tmpVector.set(0, 0, 1);
+        this._tmpVector.rotateByQuaternionToRef(this._tmpQuaternion, this._tmpVector);
+        result.origin = this.pointer.absolutePosition;
+        result.direction.copyFrom(this._tmpVector);
+        result.length = 1000;
+    }
+
+    /**
+     * Disposes of the object
+     */
+    dispose() {
+        if (this.grip) {
+            this.grip.dispose();
+        }
+        this.pointer.dispose();
+        this.onDisposeObservable.notifyObservers({});
+    }
+}

+ 44 - 0
src/Cameras/XR/webXRControllerModelLoader.ts

@@ -0,0 +1,44 @@
+import { Quaternion } from '../../Maths/math';
+import { WindowsMotionController } from '../../Gamepads/Controllers/windowsMotionController';
+import { OculusTouchController } from '../../Gamepads/Controllers/oculusTouchController';
+import { WebXRInput } from './webXRInput';
+
+/**
+ * Loads a controller model and adds it as a child of the WebXRControllers grip when the controller is created
+ */
+export class WebXRControllerModelLoader {
+    /**
+     * Creates the WebXRControllerModelLoader
+     * @param input xr input that creates the controllers
+     */
+    constructor(input: WebXRInput) {
+        input.onControllerAddedObservable.add((c) => {
+            if (c.inputSource.gamepad && c.inputSource.gamepad.id === "oculus-touch") {
+                let controllerModel = new OculusTouchController(c.inputSource.gamepad);
+                controllerModel.hand = c.inputSource.handedness;
+                controllerModel.isXR = true;
+                controllerModel.initControllerMesh(c.grip!.getScene(), (m) => {
+                    controllerModel.mesh!.parent = c.grip!;
+                    controllerModel.mesh!.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
+                });
+            }else if (c.inputSource.gamepad && c.inputSource.gamepad.id === "oculus-quest") {
+                OculusTouchController._IsQuest = true;
+                let controllerModel = new OculusTouchController(c.inputSource.gamepad);
+                controllerModel.hand = c.inputSource.handedness;
+                controllerModel.isXR = true;
+                controllerModel.initControllerMesh(c.grip!.getScene(), (m) => {
+                    controllerModel.mesh!.parent = c.grip!;
+                    controllerModel.mesh!.rotationQuaternion = Quaternion.FromEulerAngles(Math.PI / -4, Math.PI, 0);
+                });
+            }else {
+                let controllerModel = new WindowsMotionController(c.inputSource.gamepad);
+                controllerModel.hand = c.inputSource.handedness;
+                controllerModel.isXR = true;
+                controllerModel.initControllerMesh(c.grip!.getScene(), (m) => {
+                    controllerModel.mesh!.parent = c.grip!;
+                    controllerModel.mesh!.rotationQuaternion = Quaternion.FromEulerAngles(0, Math.PI, 0);
+                });
+            }
+        });
+    }
+}

+ 119 - 0
src/Cameras/XR/webXRControllerPointerSelection.ts

@@ -0,0 +1,119 @@
+import { Nullable } from "../../types";
+import {  Vector3, Color3, Axis } from '../../Maths/math';
+import { Mesh } from '../../Meshes/mesh';
+import { Ray } from '../../Culling/ray';
+import { StandardMaterial } from '../../Materials/standardMaterial';
+import { WebXRInput } from './webXRInput';
+
+/**
+ * Handles pointer input automatically for the pointer of XR controllers
+ */
+export class WebXRControllerPointerSelection {
+    private static _idCounter = 0;
+    private _tmpRay = new Ray(new Vector3(), new Vector3());
+
+    /**
+     * Creates a WebXRControllerPointerSelection
+     * @param input input manager to setup pointer selection
+     */
+    constructor(input: WebXRInput) {
+        input.onControllerAddedObservable.add((controller) => {
+            let scene = controller.pointer.getScene();
+
+            let laserPointer: Mesh;
+            let cursorMesh: Mesh;
+            let triggerDown = false;
+            let id: number;
+            id = WebXRControllerPointerSelection._idCounter++;
+
+            // Create a laser pointer for the XR controller
+            laserPointer = Mesh.CreateCylinder("laserPointer", 1, 0.0002, 0.004, 20, 1, scene, false);
+            laserPointer.parent = controller.pointer;
+            let laserPointerMaterial = new StandardMaterial("laserPointerMat", scene);
+            laserPointerMaterial.emissiveColor = new Color3(0.7, 0.7, 0.7);
+            laserPointerMaterial.alpha = 0.6;
+            laserPointer.material = laserPointerMaterial;
+            laserPointer.rotation.x = Math.PI / 2;
+            this._updatePointerDistance(laserPointer, 1);
+            laserPointer.isPickable = false;
+
+            // Create a gaze tracker for the  XR controlelr
+            cursorMesh = Mesh.CreateTorus("gazeTracker", 0.0035 * 3, 0.0025 * 3, 20, scene, false);
+            cursorMesh.bakeCurrentTransformIntoVertices();
+            cursorMesh.isPickable = false;
+            cursorMesh.isVisible = false;
+            let targetMat = new StandardMaterial("targetMat", scene);
+            targetMat.specularColor = Color3.Black();
+            targetMat.emissiveColor = new Color3(0.7, 0.7, 0.7);
+            targetMat.backFaceCulling = false;
+            cursorMesh.material = targetMat;
+
+            let renderObserver = scene.onBeforeRenderObservable.add(() => {
+                // Every frame check collisions/input
+                controller.getWorldPointerRayToRef(this._tmpRay);
+                let pick = scene.pickWithRay(this._tmpRay);
+                if (pick) {
+                    if (controller.inputSource.gamepad && controller.inputSource.gamepad.buttons[0] && controller.inputSource.gamepad.buttons[0].value > 0.7) {
+                        if (!triggerDown) {
+                            scene.simulatePointerDown(pick, { pointerId: id });
+                        }
+                        triggerDown = true;
+                    }else {
+                        if (triggerDown) {
+                            scene.simulatePointerUp(pick, { pointerId: id });
+                        }
+                        triggerDown = false;
+                    }
+                    scene.simulatePointerMove(pick, { pointerId: id });
+                }
+
+                if (pick && pick.pickedPoint && pick.hit) {
+                    // Update laser state
+                    this._updatePointerDistance(laserPointer, pick.distance);
+
+                    // Update cursor state
+                    cursorMesh.position.copyFrom(pick.pickedPoint);
+                    cursorMesh.scaling.x = Math.sqrt(pick.distance);
+                    cursorMesh.scaling.y = Math.sqrt(pick.distance);
+                    cursorMesh.scaling.z = Math.sqrt(pick.distance);
+
+                    // To avoid z-fighting
+                    let pickNormal = this._convertNormalToDirectionOfRay(pick.getNormal(), this._tmpRay);
+                    let deltaFighting = 0.002;
+                    cursorMesh.position.copyFrom(pick.pickedPoint);
+                    if (pickNormal) {
+                        let axis1 = Vector3.Cross(Axis.Y, pickNormal);
+                        let axis2 = Vector3.Cross(pickNormal, axis1);
+                        Vector3.RotationFromAxisToRef(axis2, pickNormal, axis1, cursorMesh.rotation);
+                        cursorMesh.position.addInPlace(pickNormal.scale(deltaFighting));
+                    }
+                    cursorMesh.isVisible = true;
+                }else {
+                    cursorMesh.isVisible = false;
+                }
+            });
+
+            controller.onDisposeObservable.addOnce(() => {
+                laserPointer.dispose();
+                cursorMesh.dispose();
+
+                scene.onBeforeRenderObservable.remove(renderObserver);
+            });
+        });
+    }
+
+    private _convertNormalToDirectionOfRay(normal: Nullable<Vector3>, ray: Ray) {
+        if (normal) {
+            let angle = Math.acos(Vector3.Dot(normal, ray.direction));
+            if (angle < Math.PI / 2) {
+                normal.scaleInPlace(-1);
+            }
+        }
+        return normal;
+    }
+
+    private _updatePointerDistance(_laserPointer: Mesh, distance: number = 100) {
+        _laserPointer.scaling.y = distance;
+        _laserPointer.position.z = distance / 2;
+    }
+}

+ 181 - 0
src/Cameras/XR/webXRControllerTeleportation.ts

@@ -0,0 +1,181 @@
+import { AbstractMesh } from "../../Meshes/abstractMesh";
+import { Quaternion, Vector3 } from '../../Maths/math';
+import { Mesh } from '../../Meshes/mesh';
+import { Ray } from '../../Culling/ray';
+import { StandardMaterial } from '../../Materials/standardMaterial';
+import { DynamicTexture } from '../../Materials/Textures/dynamicTexture';
+import { EasingFunction, SineEase } from '../../Animations/easing';
+import { Animation } from '../../Animations/animation';
+import { WebXRInput } from './webXRInput';
+
+/**
+ * Enables teleportation
+ */
+export class WebXRControllerTeleportation {
+    private _teleportationFillColor: string = "#444444";
+    private _teleportationBorderColor: string = "#FFFFFF";
+
+    private _tmpRay = new Ray(new Vector3(), new Vector3());
+    private _tmpVector = new Vector3();
+
+    /**
+     * Creates a WebXRControllerTeleportation
+     * @param input input manager to add teleportation to
+     * @param floorMeshes floormeshes which can be teleported to
+     */
+    constructor(input: WebXRInput, floorMeshes: Array<AbstractMesh> = []) {
+        input.onControllerAddedObservable.add((c) => {
+            let scene = c.pointer.getScene();
+
+            let forwardReadyToTeleport = false;
+            let backwardReadyToTeleport = false;
+            let leftReadyToTeleport = false;
+            let rightReadyToTeleport = false;
+
+            // Teleport target abd it's animation
+            let teleportationTarget = Mesh.CreateGround("teleportationTarget", 2, 2, 2, scene);
+            teleportationTarget.isPickable = false;
+            let length = 512;
+            let dynamicTexture = new DynamicTexture("DynamicTexture", length, scene, true);
+            dynamicTexture.hasAlpha = true;
+            let context = dynamicTexture.getContext();
+            let centerX = length / 2;
+            let centerY = length / 2;
+            let radius = 200;
+            context.beginPath();
+            context.arc(centerX, centerY, radius, 0, 2 * Math.PI, false);
+            context.fillStyle = this._teleportationFillColor;
+            context.fill();
+            context.lineWidth = 10;
+            context.strokeStyle = this._teleportationBorderColor;
+            context.stroke();
+            context.closePath();
+            dynamicTexture.update();
+            let teleportationCircleMaterial = new StandardMaterial("TextPlaneMaterial", scene);
+            teleportationCircleMaterial.diffuseTexture = dynamicTexture;
+            teleportationTarget.material = teleportationCircleMaterial;
+            let torus = Mesh.CreateTorus("torusTeleportation", 0.75, 0.1, 25, scene, false);
+            torus.isPickable = false;
+            torus.parent = teleportationTarget;
+            let animationInnerCircle = new Animation("animationInnerCircle", "position.y", 30, Animation.ANIMATIONTYPE_FLOAT, Animation.ANIMATIONLOOPMODE_CYCLE);
+            let keys = [];
+            keys.push({
+                frame: 0,
+                value: 0
+            });
+            keys.push({
+                frame: 30,
+                value: 0.4
+            });
+            keys.push({
+                frame: 60,
+                value: 0
+            });
+            animationInnerCircle.setKeys(keys);
+            let easingFunction = new SineEase();
+            easingFunction.setEasingMode(EasingFunction.EASINGMODE_EASEINOUT);
+            animationInnerCircle.setEasingFunction(easingFunction);
+            torus.animations = [];
+            torus.animations.push(animationInnerCircle);
+            scene.beginAnimation(torus, 0, 60, true);
+
+            // Handle user input on every frame
+            let renderObserver = scene.onBeforeRenderObservable.add(() => {
+                // Move the teleportationTarget to where the user is targetting to teleport to
+                if (forwardReadyToTeleport) {
+                    c.getWorldPointerRayToRef(this._tmpRay);
+                    let pick = scene.pickWithRay(this._tmpRay, (o) => {
+                        return floorMeshes.indexOf(o) !== -1;
+                    });
+                    if (pick && pick.pickedPoint) {
+                        // To avoid z-fighting
+                        teleportationTarget.position.copyFrom(pick.pickedPoint);
+                        teleportationTarget.position.y += 0.002;
+                    }
+                    teleportationTarget.isVisible = true;
+                    (<Mesh>teleportationTarget.getChildren()[0]).isVisible = true;
+                }else {
+                    teleportationTarget.isVisible = false;
+                    (<Mesh>teleportationTarget.getChildren()[0]).isVisible = false;
+                }
+
+                if (c.inputSource.gamepad) {
+                    if (c.inputSource.gamepad.axes[1]) {
+                        // Forward teleportation
+                        if (c.inputSource.gamepad.axes[1] < -0.7) {
+                            forwardReadyToTeleport = true;
+                        }else {
+                            if (forwardReadyToTeleport) {
+                                // Teleport the users feet to where they targetted
+                                this._tmpVector.copyFrom(teleportationTarget.position);
+                                this._tmpVector.y += input.baseExperience.camera.position.y;
+                                input.baseExperience.setPositionOfCameraUsingContainer(this._tmpVector);
+                            }
+                            forwardReadyToTeleport = false;
+                        }
+
+                        // Backward teleportation
+                        if (c.inputSource.gamepad.axes[1] > 0.7) {
+                            backwardReadyToTeleport = true;
+                        }else {
+                            if (backwardReadyToTeleport) {
+                                // Cast a ray down from behind the user
+                                let camMat = input.baseExperience.camera.computeWorldMatrix();
+                                let q = new Quaternion();
+                                camMat.decompose(undefined, q, this._tmpRay.origin);
+                                this._tmpVector.set(0, 0, -1);
+                                this._tmpVector.rotateByQuaternionToRef(q, this._tmpVector);
+                                this._tmpVector.y = 0;
+                                this._tmpVector.normalize();
+                                this._tmpVector.y = -1.5;
+                                this._tmpVector.normalize();
+                                this._tmpRay.direction.copyFrom(this._tmpVector);
+                                let pick = scene.pickWithRay(this._tmpRay, (o) => {
+                                    return floorMeshes.indexOf(o) !== -1;
+                                });
+
+                                if (pick && pick.pickedPoint) {
+                                    // Teleport the users feet to where they targetted
+                                    this._tmpVector.copyFrom(pick.pickedPoint);
+                                    this._tmpVector.y += input.baseExperience.camera.position.y;
+                                    input.baseExperience.setPositionOfCameraUsingContainer(this._tmpVector);
+                                }
+                            }
+                            backwardReadyToTeleport = false;
+                        }
+                    }
+
+                    if (c.inputSource.gamepad.axes[0]) {
+                        if (c.inputSource.gamepad.axes[0] < -0.7) {
+                            leftReadyToTeleport = true;
+                        }else {
+                            if (leftReadyToTeleport) {
+                                input.baseExperience.rotateCameraByQuaternionUsingContainer(Quaternion.FromEulerAngles(0, -Math.PI / 4, 0));
+                            }
+                            leftReadyToTeleport = false;
+                        }
+                        if (c.inputSource.gamepad.axes[0] > 0.7) {
+                            rightReadyToTeleport = true;
+                        }else {
+                            if (rightReadyToTeleport) {
+                                input.baseExperience.rotateCameraByQuaternionUsingContainer(Quaternion.FromEulerAngles(0, Math.PI / 4, 0));
+                            }
+                            rightReadyToTeleport = false;
+                        }
+                    }
+
+                }
+            });
+
+            c.onDisposeObservable.addOnce(() => {
+                teleportationTarget.dispose();
+                dynamicTexture.dispose();
+                teleportationCircleMaterial.dispose();
+                torus.dispose();
+
+                scene.onBeforeRenderObservable.remove(renderObserver);
+            });
+        });
+    }
+
+}

+ 104 - 0
src/Cameras/XR/webXRDefaultExperience.ts

@@ -0,0 +1,104 @@
+import { WebXRExperienceHelper } from "./webXRExperienceHelper";
+import { Scene } from '../../scene';
+import { WebXRInput } from './webXRInput';
+import { WebXRControllerModelLoader } from './webXRControllerModelLoader';
+import { WebXRControllerPointerSelection } from './webXRControllerPointerSelection';
+import { WebXRControllerTeleportation } from './webXRControllerTeleportation';
+import { WebXRManagedOutputCanvas } from './webXRManagedOutputCanvas';
+import { WebXREnterExitUI } from './webXREnterExitUI';
+import { AbstractMesh } from '../../Meshes/abstractMesh';
+/**
+ * Options for the default xr helper
+ */
+export class WebXRDefaultExperienceOptions {
+    /**
+     * Floor meshes that should be used for teleporting
+     */
+    public floorMeshes: Array<AbstractMesh>;
+}
+
+/**
+ * Default experience which provides a similar setup to the previous webVRExperience
+ */
+export class WebXRDefaultExperience {
+    /**
+     * Base experience
+     */
+    public baseExperience: WebXRExperienceHelper;
+    /**
+     * Input experience extension
+     */
+    public input: WebXRInput;
+    /**
+     * Loads the controller models
+     */
+    public controllerModelLoader: WebXRControllerModelLoader;
+    /**
+     * Enables laser pointer and selection
+     */
+    public pointerSelection: WebXRControllerPointerSelection;
+    /**
+     * Enables teleportation
+     */
+    public teleportation: WebXRControllerTeleportation;
+    /**
+     * Enables ui for enetering/exiting xr
+     */
+    public enterExitUI: WebXREnterExitUI;
+    /**
+     * Default output canvas xr should render to
+     */
+    public outputCanvas: WebXRManagedOutputCanvas;
+
+    /**
+     * Creates the default xr experience
+     * @param scene scene
+     * @param options options for basic configuration
+     * @returns resulting WebXRDefaultExperience
+     */
+    public static CreateAsync(scene: Scene, options: WebXRDefaultExperienceOptions) {
+        var result = new WebXRDefaultExperience();
+
+        // Create base experience
+        return WebXRExperienceHelper.CreateAsync(scene).then((xrHelper) => {
+            result.baseExperience = xrHelper;
+
+            // Add controller support
+            result.input = new WebXRInput(xrHelper);
+            result.controllerModelLoader = new WebXRControllerModelLoader(result.input);
+            result.pointerSelection = new WebXRControllerPointerSelection(result.input);
+            result.teleportation = new WebXRControllerTeleportation(result.input, options.floorMeshes);
+
+            // Create output canvas manager (this controls where the xr frames will be rendered)
+            result.outputCanvas = new WebXRManagedOutputCanvas(xrHelper, scene.getEngine().getRenderingCanvas() as HTMLCanvasElement);
+
+            // Create ui for entering/exiting xr
+            return WebXREnterExitUI.CreateAsync(scene, result.baseExperience, {webXRManagedOutputCanvas: result.outputCanvas});
+        }).then((ui) => {
+            result.enterExitUI = ui;
+            return result;
+        });
+    }
+
+    private constructor() {
+
+    }
+
+    /**
+     * DIsposes of the experience helper
+     */
+    public dispose() {
+        if (this.baseExperience) {
+            this.baseExperience.dispose();
+        }
+        if (this.input) {
+            this.input.dispose();
+        }
+        if (this.enterExitUI) {
+            this.enterExitUI.dispose();
+        }
+        if (this.outputCanvas) {
+            this.outputCanvas.dispose();
+        }
+    }
+}

+ 11 - 4
src/Cameras/XR/webXRExperienceHelper.ts

@@ -29,7 +29,7 @@ export enum WebXRState {
     NOT_IN_XR
 }
 /**
- * Helper class used to enable XR
+ * Base set of functionality needed to create an XR experince (WebXRSessionManager, Camera, StateManagement, etc.)
  * @see https://doc.babylonjs.com/how_to/webxr
  */
 export class WebXRExperienceHelper implements IDisposable {
@@ -89,8 +89,12 @@ export class WebXRExperienceHelper implements IDisposable {
     private constructor(private scene: Scene) {
         this.camera = new WebXRCamera("", scene);
         this.sessionManager = new WebXRSessionManager(scene);
-        this.container = new AbstractMesh("", scene);
+        this.container = new AbstractMesh("WebXR Container", scene);
         this.camera.parent = this.container;
+
+        scene.onDisposeObservable.add(() => {
+            this.exitXRAsync();
+        });
     }
 
     /**
@@ -144,11 +148,14 @@ export class WebXRExperienceHelper implements IDisposable {
                 // Restore scene settings
                 this.scene.autoClear = this._originalSceneAutoClear;
                 this.scene.activeCamera = this._nonVRCamera;
-                this.sessionManager.onXRFrameObservable.clear();
 
                 this._setState(WebXRState.NOT_IN_XR);
             });
-            this._setState(WebXRState.IN_XR);
+
+            // Wait until the first frame arrives before setting state to in xr
+            this.sessionManager.onXRFrameObservable.addOnce(() => {
+                this._setState(WebXRState.IN_XR);
+            });
         }).catch((e: any) => {
             console.log(e);
             console.log(e.message);

+ 32 - 105
src/Cameras/XR/webXRInput.ts

@@ -1,95 +1,8 @@
 import { Nullable } from "../../types";
 import { Observer, Observable } from "../../Misc/observable";
-import { IDisposable, Scene } from "../../scene";
-import { AbstractMesh } from "../../Meshes/abstractMesh";
-import { WebXRExperienceHelper } from "./webXRExperienceHelper";
-import { Matrix, Quaternion } from '../../Maths/math';
-/**
- * Represents an XR input
- */
-export class WebXRController {
-    /**
-     * Represents the part of the controller that is held. This may not exist if the controller is the head mounted display itself, if thats the case only the pointer from the head will be availible
-     */
-    public grip?: AbstractMesh;
-    /**
-     * Pointer which can be used to select objects or attach a visible laser to
-     */
-    public pointer: AbstractMesh;
-
-    private _tmpMatrix = new Matrix();
-
-    /**
-     * Creates the controller
-     * @see https://doc.babylonjs.com/how_to/webxr
-     * @param scene the scene which the controller should be associated to
-     * @param inputSource the underlying input source for the controller
-     * @param parentContainer parent that the controller meshes should be children of
-     */
-    constructor(
-        private scene: Scene,
-        /** The underlying input source for the controller  */
-        public inputSource: XRInputSource,
-        private parentContainer: Nullable<AbstractMesh> = null)
-    {
-        this.pointer = new AbstractMesh("controllerPointer", scene);
-        if (parentContainer) {
-            parentContainer.addChild(this.pointer);
-
-        }
-    }
-
-    /**
-     * Updates the controller pose based on the given XRFrame
-     * @param xrFrame xr frame to update the pose with
-     * @param referenceSpace reference space to use
-     */
-    public updateFromXRFrame(xrFrame: XRFrame, referenceSpace: XRReferenceSpace) {
-        var pose = xrFrame.getPose(this.inputSource.targetRaySpace, referenceSpace);
-        if (pose) {
-            Matrix.FromFloat32ArrayToRefScaled(pose.transform.matrix, 0, 1, this._tmpMatrix);
-            if (!this.pointer.getScene().useRightHandedSystem) {
-                this._tmpMatrix.toggleModelMatrixHandInPlace();
-            }
-            if (!this.pointer.rotationQuaternion) {
-                this.pointer.rotationQuaternion = new Quaternion();
-            }
-            this._tmpMatrix.decompose(this.pointer.scaling, this.pointer.rotationQuaternion!, this.pointer.position);
-        }
-
-        if (this.inputSource.gripSpace) {
-            if (!this.grip) {
-                this.grip = new AbstractMesh("controllerGrip", this.scene);
-                if (this.parentContainer) {
-                    this.parentContainer.addChild(this.grip);
-                }
-            }
-
-            var pose = xrFrame.getPose(this.inputSource.gripSpace, referenceSpace);
-            if (pose) {
-                Matrix.FromFloat32ArrayToRefScaled(pose.transform.matrix, 0, 1, this._tmpMatrix);
-                if (!this.grip.getScene().useRightHandedSystem) {
-                    this._tmpMatrix.toggleModelMatrixHandInPlace();
-                }
-                if (!this.grip.rotationQuaternion) {
-                    this.grip.rotationQuaternion = new Quaternion();
-                }
-                this._tmpMatrix.decompose(this.grip.scaling, this.grip.rotationQuaternion!, this.grip.position);
-            }
-        }
-
-    }
-
-    /**
-     * Disposes of the object
-     */
-    dispose() {
-        if (this.grip) {
-            this.grip.dispose();
-        }
-        this.pointer.dispose();
-    }
-}
+import { IDisposable } from "../../scene";
+import { WebXRExperienceHelper, WebXRState } from "./webXRExperienceHelper";
+import { WebXRController } from './webXRController';
 
 /**
  * XR input used to track XR inputs such as controllers/rays
@@ -100,6 +13,7 @@ export class WebXRInput implements IDisposable {
      */
     public controllers: Array<WebXRController> = [];
     private _frameObserver: Nullable<Observer<any>>;
+    private _stateObserver: Nullable<Observer<any>>;
     /**
      * Event when a controller has been connected/added
      */
@@ -111,23 +25,35 @@ export class WebXRInput implements IDisposable {
 
     /**
      * Initializes the WebXRInput
-     * @param helper experience helper which the input should be created for
+     * @param baseExperience experience helper which the input should be created for
      */
-    public constructor(private helper: WebXRExperienceHelper) {
-        this._frameObserver = helper.sessionManager.onXRFrameObservable.add(() => {
-            if (!helper.sessionManager.currentFrame) {
+    public constructor(
+        /**
+         * Base experience the input listens to
+         */
+        public baseExperience: WebXRExperienceHelper
+    ) {
+        // Remove controllers when exiting XR
+        this._stateObserver = baseExperience.onStateChangedObservable.add((s) => {
+            if (s === WebXRState.NOT_IN_XR) {
+                this._addAndRemoveControllers([], this.controllers.map((c) => {return c.inputSource; }));
+            }
+        });
+
+        this._frameObserver = baseExperience.sessionManager.onXRFrameObservable.add(() => {
+            if (!baseExperience.sessionManager.currentFrame) {
                 return;
             }
 
             // Start listing to input add/remove event
-            if (this.controllers.length == 0 && helper.sessionManager.session.inputSources) {
-                this._addAndRemoveControllers(helper.sessionManager.session.inputSources, []);
-                helper.sessionManager.session.addEventListener("inputsourceschange", this._onInputSourcesChange);
+            if (this.controllers.length == 0 && baseExperience.sessionManager.session.inputSources) {
+                this._addAndRemoveControllers(baseExperience.sessionManager.session.inputSources, []);
+                baseExperience.sessionManager.session.addEventListener("inputsourceschange", this._onInputSourcesChange);
             }
 
             // Update controller pose info
             this.controllers.forEach((controller) => {
-                controller.updateFromXRFrame(helper.sessionManager.currentFrame!, helper.sessionManager.referenceSpace);
+                controller.updateFromXRFrame(baseExperience.sessionManager.currentFrame!, baseExperience.sessionManager.referenceSpace);
             });
 
         });
@@ -139,18 +65,18 @@ export class WebXRInput implements IDisposable {
 
     private _addAndRemoveControllers(addInputs: Array<XRInputSource>, removeInputs: Array<XRInputSource>) {
         // Add controllers if they don't already exist
-        var sources = this.controllers.map((c) => {return c.inputSource; });
-        addInputs.forEach((input) => {
+        let sources = this.controllers.map((c) => {return c.inputSource; });
+        for (let input of addInputs) {
             if (sources.indexOf(input) === -1) {
-                var controller = new WebXRController(this.helper.camera._scene, input, this.helper.container);
+                let controller = new WebXRController(this.baseExperience.camera._scene, input, this.baseExperience.container);
                 this.controllers.push(controller);
                 this.onControllerAddedObservable.notifyObservers(controller);
             }
-        });
+        }
 
         // Remove and dispose of controllers to be disposed
-        var keepControllers: Array<WebXRController> = [];
-        var removedControllers: Array<WebXRController> = [];
+        let keepControllers: Array<WebXRController> = [];
+        let removedControllers: Array<WebXRController> = [];
         this.controllers.forEach((c) => {
             if (removeInputs.indexOf(c.inputSource) === -1) {
                 keepControllers.push(c);
@@ -173,6 +99,7 @@ export class WebXRInput implements IDisposable {
         this.controllers.forEach((c) => {
             c.dispose();
         });
-        this.helper.sessionManager.onXRFrameObservable.remove(this._frameObserver);
+        this.baseExperience.sessionManager.onXRFrameObservable.remove(this._frameObserver);
+        this.baseExperience.onStateChangedObservable.remove(this._stateObserver);
     }
 }

+ 4 - 1
src/Cameras/XR/webXRSessionManager.ts

@@ -138,7 +138,10 @@ export class WebXRSessionManager implements IDisposable {
      * @returns Promise which resolves after it exits XR
      */
     public exitXRAsync() {
-        return this.session.end();
+        if (this.session) {
+            this.session.end();
+        }
+        return new Promise(() => {});
     }
 
     /**

+ 1 - 1
src/Cameras/camera.ts

@@ -902,7 +902,7 @@ export class Camera extends Node {
     }
 
     /** @hidden */
-    public _isRightCamera = true;
+    public _isRightCamera = false;
     /**
      * Gets the right camera of a rig setup in case of Rigged Camera
      */

+ 23 - 8
src/Gamepads/Controllers/poseEnabledController.ts

@@ -131,6 +131,10 @@ export class PoseEnabledControllerHelper {
  * Defines the PoseEnabledController object that contains state of a vr capable controller
  */
 export class PoseEnabledController extends Gamepad implements PoseControlled {
+    /**
+     * If the controller is used in a webXR session
+     */
+    public isXR = false;
     // Represents device position and rotation in room space. Should only be used to help calculate babylon space values
     private _deviceRoomPosition = Vector3.Zero();
     private _deviceRoomRotationQuaternion = new Quaternion();
@@ -228,6 +232,9 @@ export class PoseEnabledController extends Gamepad implements PoseControlled {
      * Updates the state of the pose enbaled controller and mesh based on the current position and rotation of the controller
      */
     public update() {
+        if (this.isXR) {
+            return;
+        }
         super.update();
         this._updatePoseAndMesh();
     }
@@ -236,6 +243,9 @@ export class PoseEnabledController extends Gamepad implements PoseControlled {
      * Updates only the pose device and mesh without doing any button event checking
      */
     protected _updatePoseAndMesh() {
+        if (this.isXR) {
+            return;
+        }
         var pose: GamepadPose = this.browserGamepad.pose;
         this.updateFromDevice(pose);
 
@@ -283,6 +293,9 @@ export class PoseEnabledController extends Gamepad implements PoseControlled {
      * @param poseData raw pose fromthe device
      */
     updateFromDevice(poseData: DevicePose) {
+        if (this.isXR) {
+            return;
+        }
         if (poseData) {
             this.rawPose = poseData;
             if (poseData.position) {
@@ -335,15 +348,17 @@ export class PoseEnabledController extends Gamepad implements PoseControlled {
         }
 
         // Sync controller mesh and pointing pose node's state with controller, this is done to avoid a frame where position is 0,0,0 when attaching mesh
-        this._updatePoseAndMesh();
-        if (this._pointingPoseNode) {
-            var parents = [];
-            var obj: Node = this._pointingPoseNode;
-            while (obj.parent) {
-                parents.push(obj.parent);
-                obj = obj.parent;
+        if (!this.isXR) {
+            this._updatePoseAndMesh();
+            if (this._pointingPoseNode) {
+                var parents = [];
+                var obj: Node = this._pointingPoseNode;
+                while (obj.parent) {
+                    parents.push(obj.parent);
+                    obj = obj.parent;
+                }
+                parents.reverse().forEach((p) => { p.computeWorldMatrix(true); });
             }
-            parents.reverse().forEach((p) => { p.computeWorldMatrix(true); });
         }
 
         this._meshAttachedObservable.notifyObservers(mesh);

+ 8 - 15
src/Helpers/sceneHelpers.ts

@@ -12,16 +12,13 @@ import { IEnvironmentHelperOptions, EnvironmentHelper } from "./environmentHelpe
 import { FreeCamera } from "../Cameras/freeCamera";
 import { ArcRotateCamera } from "../Cameras/arcRotateCamera";
 import { TargetCamera } from "../Cameras/targetCamera";
-import { WebXRManagedOutputCanvas } from "../Cameras/XR/webXRManagedOutputCanvas";
-import { WebXRInput } from "../Cameras/XR/webXRInput";
-import { WebXREnterExitUI } from "../Cameras/XR/webXREnterExitUI";
-import { WebXRExperienceHelper } from "../Cameras/XR/webXRExperienceHelper";
 import { VRExperienceHelperOptions, VRExperienceHelper } from "../Cameras/VR/vrExperienceHelper";
 
 import "../Materials/Textures/Loaders/ddsTextureLoader";
 import "../Materials/Textures/Loaders/envTextureLoader";
 import "../Materials/Textures/Loaders/ktxTextureLoader";
 import "../Meshes/Builders/boxBuilder";
+import { WebXRDefaultExperience, WebXRDefaultExperienceOptions } from '../Cameras/XR/webXRDefaultExperience';
 
 /** @hidden */
 export var _forceSceneHelpersToBundle = true;
@@ -82,11 +79,12 @@ declare module "../scene" {
         createDefaultVRExperience(webVROptions?: VRExperienceHelperOptions): VRExperienceHelper;
 
         /**
-         * Creates a new XREXperienceHelper
+         * Creates a new WebXRDefaultExperience
          * @see http://doc.babylonjs.com/how_to/webxr
-         * @returns a promise for a new XREXperienceHelper
+         * @param options experience options
+         * @returns a promise for a new WebXRDefaultExperience
          */
-        createDefaultXRExperienceAsync(): Promise<WebXRExperienceHelper>;
+        createDefaultXRExperienceAsync(options: WebXRDefaultExperienceOptions): Promise<WebXRDefaultExperience>;
     }
 }
 
@@ -210,13 +208,8 @@ Scene.prototype.createDefaultVRExperience = function(webVROptions: VRExperienceH
     return new VRExperienceHelper(this, webVROptions);
 };
 
-Scene.prototype.createDefaultXRExperienceAsync = function(): Promise<WebXRExperienceHelper> {
-    return WebXRExperienceHelper.CreateAsync(this).then((helper) => {
-        var outputCanvas = new WebXRManagedOutputCanvas(helper);
-        return WebXREnterExitUI.CreateAsync(this, helper, { webXRManagedOutputCanvas: outputCanvas })
-            .then((ui) => {
-                new WebXRInput(helper);
-                return helper;
-            });
+Scene.prototype.createDefaultXRExperienceAsync = function(options: WebXRDefaultExperienceOptions): Promise<WebXRDefaultExperience> {
+    return WebXRDefaultExperience.CreateAsync(this, options).then((helper) => {
+        return helper;
     });
 };