Browse Source

Merge pull request #7434 from BabylonJS/master

Try New Continuous Deployment
sebavan 5 years ago
parent
commit
32fb769196

+ 95 - 0
azure-pipelines-cd.yml

@@ -0,0 +1,95 @@
+trigger:
+- preview
+
+jobs:
+- job: NetlifyStaticDeploy
+  displayName: 'Deploy'
+  pool:
+    vmImage: 'Ubuntu-16.04'
+    demands: npm
+  steps:
+  - script: 'npm install netlify-cli'
+    displayName: 'npm install netlify'
+  - script: 'npm install --prefix Playground/'
+    displayName: 'npm install playground'
+# Controllers
+  - script: 'npx netlify link --id=220a52f0-ea85-4244-8342-8ec149cd78d6'
+    displayName: 'netlify link babylonjscontrollers'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+  - script: 'npx netlify deploy --prod --dir=assets/meshes/controllers'
+    displayName: 'netlify deploy babylonjscontrollers'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+# Sandbox
+  - script: 'npx netlify link --id=4deda23f-f382-458b-897e-2f2111b5f263'
+    displayName: 'netlify link babylonjs-sandbox'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+  - script: 'npx netlify deploy --prod --dir=sandbox/'
+    displayName: 'netlify deploy babylonjs-sandbox'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+# playground
+  - script: 'npx netlify link --id=9161d5f0-b208-4b86-a281-9300386f6c94'
+    displayName: 'netlify link babylonjs-playground'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+  - script: 'npx netlify deploy --prod --dir=Playground/'
+    displayName: 'netlify deploy babylonjs-playground'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+# babylonjs-preview
+  - script: 'npx netlify link --id=e1c1c520-e3a4-4d5b-91e6-254f1bea1b6b'
+    displayName: 'netlify link babylonjs-preview'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+  - script: 'npx netlify deploy --prod --dir="dist/preview release"'
+    displayName: 'netlify deploy babylonjs-preview'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+# assets-babylonjs
+  - script: 'npx netlify link --id=bd58224a-5f56-42de-afd2-ffa72b85fd71'
+    displayName: 'netlify link assets-babylonjs'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+  - script: 'npx netlify deploy --prod --dir=assets/'
+    displayName: 'netlify deploy assets-babylonjs'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+# nodeeditor
+  - script: 'npm install --prefix Tools/Gulp && npx gulp nodeEditor --gulpfile ./Tools/Gulp/gulpfile.js'
+    displayName: 'gulp build nodeeditor'
+  - script: 'npx netlify link --id=d232f0dc-cdb2-473e-b0f5-91fb1dd1d398'
+    displayName: 'netlify link nodeeditor'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+  - script: 'npx netlify deploy --prod --dir=nodeEditor/public'
+    displayName: 'netlify deploy nodeeditor'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+# viewer-babylonjs
+  - script: 'npm install --prefix Tools/Gulp && npx gulp viewer --gulpfile ./Tools/Gulp/gulpfile.js'
+    displayName: 'gulp build viewer-babylonjs'
+  - script: 'npx netlify link --id=a32b113f-8187-43ab-9133-8844521f26f4'
+    displayName: 'netlify link viewer-babylonjs'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)
+  - script: 'npx netlify deploy --prod --dir=Viewer/dist/'
+    displayName: 'netlify deploy viewer-babylonjs'
+    env:
+      AZURE_PULLREQUESTID: $(System.PullRequest.PullRequestId)
+      NETLIFY_AUTH_TOKEN: $(babylon.netlify.authToken)

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

@@ -83,6 +83,7 @@
 - Added support for `Material.depthFunction` property ([Popov72](https://github.com/Popov72))
 - Added an optional config option `initialTab` ([ycw](https://github.com/ycw/))
 - Added support for ImportAnimations ([noalak](https://github.com/noalak/))
+- Added support for Cascaded Shadow Maps ([Popov72](https://github.com/Popov72))
 
 ### Tools
 
@@ -179,7 +180,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/))

+ 1 - 1
inspector/src/components/actionTabs/lines/optionsLineComponent.tsx

@@ -79,7 +79,7 @@ export class OptionsLineComponent extends React.Component<IOptionsLineComponentP
 
                 </div>
                 <div className="options">
-                    <select onChange={evt => this.updateValue(evt.target.value)} value={this.state.value || ""}>
+                    <select onChange={evt => this.updateValue(evt.target.value)} value={this.state.value ?? ""}>
                         {
                             this.props.options.map(option => {
                                 return (

+ 98 - 25
inspector/src/components/actionTabs/tabs/propertyGrids/lights/commonShadowLightPropertyGridComponent.tsx

@@ -9,27 +9,37 @@ import { LockObject } from "../lockObject";
 import { GlobalState } from '../../../../globalState';
 import { OptionsLineComponent } from '../../../lines/optionsLineComponent';
 import { ShadowGenerator } from 'babylonjs/Lights/Shadows/shadowGenerator';
+import { CascadedShadowGenerator } from 'babylonjs/Lights/Shadows/cascadedShadowGenerator';
 import { SliderLineComponent } from '../../../lines/sliderLineComponent';
 import { ButtonLineComponent } from '../../../lines/buttonLineComponent';
+import { DirectionalLight } from 'babylonjs/Lights/directionalLight';
 
 interface ICommonShadowLightPropertyGridComponentProps {
-    globalState: GlobalState,
-    light: IShadowLight,
-    lockObject: LockObject,
-    onPropertyChangedObservable?: Observable<PropertyChangedEvent>
+    globalState: GlobalState;
+    light: IShadowLight;
+    lockObject: LockObject;
+    onPropertyChangedObservable?: Observable<PropertyChangedEvent>;
 }
 
 export class CommonShadowLightPropertyGridComponent extends React.Component<ICommonShadowLightPropertyGridComponentProps> {
+    private _internals : { generatorType: number, mapSize: number };
+
     constructor(props: ICommonShadowLightPropertyGridComponentProps) {
         super(props);
+
+        this._internals = {
+            generatorType: 0,
+            mapSize: 1024,
+        };
     }
 
     createShadowGenerator() {
         const light = this.props.light;
         const scene = light.getScene();
-        let generator = new ShadowGenerator(512, light);
+        const internals = this._internals;
+        let generator = internals.generatorType === 0 ? new ShadowGenerator(internals.mapSize, light) : new CascadedShadowGenerator(internals.mapSize, light as DirectionalLight);
 
-        scene.meshes.forEach(m => {
+        scene.meshes.forEach((m) => {
             generator.addShadowCaster(m);
             m.receiveShadows = true;
         });
@@ -37,45 +47,104 @@ export class CommonShadowLightPropertyGridComponent extends React.Component<ICom
         this.forceUpdate();
     }
 
+    disposeShadowGenerator() {
+        const light = this.props.light;
+
+        light.getShadowGenerator()?.dispose();
+
+        this.forceUpdate();
+    }
+
     render() {
         const light = this.props.light;
-        const generator = light.getShadowGenerator() as ShadowGenerator || null;
-
-        var blurModeOptions = [
-            { label: "None", value: ShadowGenerator.FILTER_NONE },
-            { label: "PCF", value: ShadowGenerator.FILTER_PCF },
-            { label: "PCSS", value: ShadowGenerator.FILTER_PCSS },
-            { label: "Poisson", value: ShadowGenerator.FILTER_POISSONSAMPLING },
-            { label: "Exponential", value: ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP },
-            { label: "Blurred exponential", value: ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP },
-            { label: "Close exponential", value: ShadowGenerator.FILTER_CLOSEEXPONENTIALSHADOWMAP },
-            { label: "Blurred close exponential", value: ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP },
+        const internals = this._internals;
+        const generator = light.getShadowGenerator() as (ShadowGenerator | CascadedShadowGenerator) || null;
+        const csmGenerator = generator instanceof CascadedShadowGenerator;
+        const camera = light.getScene().activeCamera;
+
+        var typeGeneratorOptions = [
+            { label: "Shadow Generator", value: 0 }
         ];
 
+        if (light instanceof DirectionalLight) {
+            typeGeneratorOptions.push({ label: "Cascaded Shadow Generator", value: 1 });
+        }
+
+        var mapSizeOptions = [
+            { label: "2048x2048", value: 2048 },
+            { label: "1024x1024", value: 1024 },
+            { label: "512x512", value: 512 },
+            { label: "256x256", value: 256 },
+        ];
+
+        var blurModeOptions;
+
+        if (generator instanceof CascadedShadowGenerator) {
+            blurModeOptions = [
+                { label: "None", value: ShadowGenerator.FILTER_NONE },
+                { label: "PCF", value: ShadowGenerator.FILTER_PCF },
+                { label: "PCSS", value: ShadowGenerator.FILTER_PCSS },
+            ];
+        } else {
+            blurModeOptions = [
+                { label: "None", value: ShadowGenerator.FILTER_NONE },
+                { label: "PCF", value: ShadowGenerator.FILTER_PCF },
+                { label: "PCSS", value: ShadowGenerator.FILTER_PCSS },
+                { label: "Poisson", value: ShadowGenerator.FILTER_POISSONSAMPLING },
+                { label: "Exponential", value: ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP },
+                { label: "Blurred exponential", value: ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP },
+                { label: "Close exponential", value: ShadowGenerator.FILTER_CLOSEEXPONENTIALSHADOWMAP },
+                { label: "Blurred close exponential", value: ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP },
+            ];
+        }
+
         var filteringQualityOptions = [
             { label: "Low", value: ShadowGenerator.QUALITY_LOW },
             { label: "Medium", value: ShadowGenerator.QUALITY_MEDIUM },
-            { label: "High", value: ShadowGenerator.QUALITY_HIGH }
+            { label: "High", value: ShadowGenerator.QUALITY_HIGH },
         ];
 
+        var numCascadesOptions = [
+            { label: "2", value: 2 },
+            { label: "3", value: 3 },
+            { label: "4", value: 4 },
+        ];
+
+        const near = camera ? camera.minZ : 0, far = camera ? camera.maxZ : 0;
+
         let filter = generator ? generator.filter : 0;
 
         return (
             <div>
                 <LineContainerComponent globalState={this.props.globalState} title="SHADOWS">
                     <CheckBoxLineComponent label="Shadows enabled" target={light} propertyName="shadowEnabled" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                    <FloatLineComponent lockObject={this.props.lockObject} label="Shadows near plane" target={light} propertyName="shadowMinZ" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
-                    <FloatLineComponent lockObject={this.props.lockObject} label="Shadows far plane" target={light} propertyName="shadowMaxZ" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                    { !csmGenerator && <>
+                        <FloatLineComponent lockObject={this.props.lockObject} label="Shadows near plane" target={light} propertyName="shadowMinZ" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                        <FloatLineComponent lockObject={this.props.lockObject} label="Shadows far plane" target={light} propertyName="shadowMaxZ" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                    </> }
                 </LineContainerComponent>
                 {
                     generator == null &&
                     <LineContainerComponent globalState={this.props.globalState} title="SHADOW GENERATOR">
+                        <OptionsLineComponent label="Type" options={typeGeneratorOptions} target={internals} propertyName="generatorType" />
+                        <OptionsLineComponent label="Map size" options={mapSizeOptions} target={internals} propertyName="mapSize" />
                         <ButtonLineComponent label="Create generator" onClick={() => this.createShadowGenerator()} />
                     </LineContainerComponent>
                 }
                 {
                     generator !== null &&
                     <LineContainerComponent globalState={this.props.globalState} title="SHADOW GENERATOR">
+                        <ButtonLineComponent label="Dispose generator" onClick={() => this.disposeShadowGenerator()} />
+                        { csmGenerator && <>
+                            <OptionsLineComponent label="Num cascades" options={numCascadesOptions} target={generator} propertyName="numCascades" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <CheckBoxLineComponent label="Debug mode" target={generator} propertyName="debug" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <CheckBoxLineComponent label="Stabilize cascades" target={generator} propertyName="stabilizeCascades" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <SliderLineComponent label="Lambda" minimum={0} maximum={1.0} step={0.01} target={generator} propertyName="lambda" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <SliderLineComponent label="Cascade blend" minimum={0} maximum={1.0} step={0.01} target={generator} propertyName="cascadeBlendPercentage" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <CheckBoxLineComponent label="Depth clamp" target={generator} propertyName="depthClamp" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <CheckBoxLineComponent label="Auto-Calc depth bounds" target={generator} propertyName="autoCalcDepthBounds" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                            <SliderLineComponent label="Shadow MaxZ" minimum={near} maximum={far} step={0.5} target={generator} propertyName="shadowMaxZ" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                        </> }
                         <FloatLineComponent lockObject={this.props.lockObject} digits={4} step="0.0001" label="Bias" target={generator} propertyName="bias" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                         <FloatLineComponent lockObject={this.props.lockObject} label="Normal bias" target={generator} propertyName="normalBias" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                         <SliderLineComponent label="Darkness" target={generator} minimum={0} maximum={1} step={0.01} propertyName="darkness" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
@@ -98,28 +167,32 @@ export class CommonShadowLightPropertyGridComponent extends React.Component<ICom
                             <SliderLineComponent label="Penumbra ratio" minimum={0} maximum={0.5} step={0.001} target={generator} propertyName="contactHardeningLightSizeUVRatio" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                         }
                         {
-                            (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP) &&
+                            !csmGenerator && (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP) &&
                             <CheckBoxLineComponent label="Use kernel blur" target={generator} propertyName="useKernelBlur"
                                 onValueChanged={() => this.forceUpdate()}
                                 onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                         }
                         {
-                            (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP) &&
+                            (generator instanceof ShadowGenerator) && (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP) &&
                             !generator.useKernelBlur &&
                             <SliderLineComponent label="Blur box offset" target={generator} propertyName="blurBoxOffset" minimum={1} maximum={64} step={1} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />}
                         {
-                            (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP) &&
+                            (generator instanceof ShadowGenerator) && (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_BLURCLOSEEXPONENTIALSHADOWMAP) &&
                             generator.useKernelBlur &&
                             <SliderLineComponent label="Blur kernel" target={generator} propertyName="blurKernel" minimum={1} maximum={64} step={1} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                         }
                         {
-                            (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP) &&
+                            (generator instanceof ShadowGenerator) && (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP) &&
                             <FloatLineComponent lockObject={this.props.lockObject} label="Depth scale" target={generator} propertyName="depthScale" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                         }
                         {
-                            (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP) &&
+                            (generator instanceof ShadowGenerator) && (filter === ShadowGenerator.FILTER_BLUREXPONENTIALSHADOWMAP || filter === ShadowGenerator.FILTER_EXPONENTIALSHADOWMAP) &&
                             <SliderLineComponent label="Blur scale" target={generator} propertyName="blurScale" minimum={1} maximum={4} step={1} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                         }
+                        {
+                            csmGenerator && (filter === ShadowGenerator.FILTER_PCSS) &&
+                            <SliderLineComponent label="Penumbra darkness" minimum={0} maximum={1.0} step={0.01} target={generator} propertyName="penumbraDarkness" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                        }
                     </LineContainerComponent>
                 }
             </div>

+ 10 - 0
inspector/src/components/actionTabs/tabs/propertyGrids/lights/directionalLightPropertyGridComponent.tsx

@@ -9,6 +9,9 @@ import { Vector3LineComponent } from "../../../lines/vector3LineComponent";
 import { CommonShadowLightPropertyGridComponent } from "./commonShadowLightPropertyGridComponent";
 import { LockObject } from "../lockObject";
 import { GlobalState } from '../../../../globalState';
+import { CheckBoxLineComponent } from '../../../lines/checkBoxLineComponent';
+import { ShadowGenerator } from 'babylonjs/Lights/Shadows/shadowGenerator';
+import { CascadedShadowGenerator } from 'babylonjs/Lights/Shadows/cascadedShadowGenerator';
 
 interface IDirectionalLightPropertyGridComponentProps {
     globalState: GlobalState,
@@ -25,6 +28,10 @@ export class DirectionalLightPropertyGridComponent extends React.Component<IDire
     render() {
         const light = this.props.light;
 
+        const generator = light.getShadowGenerator() as (ShadowGenerator | CascadedShadowGenerator) || null;
+
+        const hideAutoCalcShadowZBounds = generator instanceof CascadedShadowGenerator;
+
         return (
             <div className="pane">
                 <CommonLightPropertyGridComponent globalState={this.props.globalState} lockObject={this.props.lockObject} light={light} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
@@ -33,6 +40,9 @@ export class DirectionalLightPropertyGridComponent extends React.Component<IDire
                     <Color3LineComponent label="Specular" target={light} propertyName="specular" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                     <Vector3LineComponent label="Position" target={light} propertyName="position" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
                     <Vector3LineComponent label="Direction" target={light} propertyName="direction" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                    { !hideAutoCalcShadowZBounds &&
+                        <CheckBoxLineComponent label="Auto Calc Shadow ZBounds" target={light} propertyName="autoCalcShadowZBounds" onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
+                    }
                 </LineContainerComponent>
                 <CommonShadowLightPropertyGridComponent globalState={this.props.globalState} lockObject={this.props.lockObject} light={light} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />
             </div>

+ 4 - 1
inspector/src/components/actionTabs/tabs/propertyGrids/materials/commonMaterialPropertyGridComponent.tsx

@@ -32,6 +32,8 @@ export class CommonMaterialPropertyGridComponent extends React.Component<ICommon
     render() {
         const material = this.props.material;
 
+        material.depthFunction = material.depthFunction ?? 0;
+
         var orientationOptions = [
             { label: "Clockwise", value: Material.ClockWiseSideOrientation },
             { label: "Counterclockwise", value: Material.CounterClockWiseSideOrientation }
@@ -55,6 +57,7 @@ export class CommonMaterialPropertyGridComponent extends React.Component<ICommon
         ];
 
         var depthfunctionOptions = [
+            { label: "<Engine Default>", value: 0 },
             { label: "Never", value: Engine.NEVER },
             { label: "Always", value: Engine.ALWAYS },
             { label: "Equal", value: Engine.EQUAL },
@@ -87,7 +90,7 @@ export class CommonMaterialPropertyGridComponent extends React.Component<ICommon
                     <ButtonLineComponent label="Dispose" onClick={() => {
                         material.dispose();
                         this.props.globalState.onSelectionChangedObservable.notifyObservers(null);
-                    }} />                       
+                    }} />
                 </LineContainerComponent>
                 <LineContainerComponent globalState={this.props.globalState} title="TRANSPARENCY">
                     <SliderLineComponent label="Alpha" target={material} propertyName="alpha" minimum={0} maximum={1} step={0.01} onPropertyChangedObservable={this.props.onPropertyChangedObservable} />

+ 1 - 0
readme.md

@@ -4,6 +4,7 @@ Getting started? Play directly with the Babylon.js API using our [playground](ht
 
 [![npm version](https://badge.fury.io/js/babylonjs.svg)](https://badge.fury.io/js/babylonjs)
 [![Build Status](https://dev.azure.com/babylonjs/ContinousIntegration/_apis/build/status/CI?branchName=master)](https://dev.azure.com/babylonjs/ContinousIntegration/_build/latest?definitionId=1&branchName=master)
+[![Preview Deployment Status](https://dev.azure.com/babylonjs/ContinousIntegration/_apis/build/status/CD?branchName=preview)](https://dev.azure.com/babylonjs/ContinousIntegration/_build/latest?definitionId=8&branchName=preview)
 [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/BabylonJS/Babylon.js.svg)](http://isitmaintained.com/project/BabylonJS/Babylon.js "Average time to resolve an issue")
 [![Percentage of issues still open](https://isitmaintained.com/badge/open/babylonJS/babylon.js.svg)](https://isitmaintained.com/project/babylonJS/babylon.js "Percentage of issues still open")
 [![Build Size](https://img.badgesize.io/BabylonJS/Babylon.js/master/dist/preview%20release/babylon.js.svg?compression=gzip)](https://img.badgesize.io/BabylonJS/Babylon.js/master/dist/preview%20release/babylon.js.svg?compression=gzip)

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

@@ -0,0 +1,84 @@
+import { IWebXRFeature } from '../webXRFeaturesManager';
+import { Observer, Observable, EventState } from '../../../Misc/observable';
+import { Nullable } from '../../../types';
+import { WebXRSessionManager } from '../webXRSessionManager';
+
+/**
+ * This is the base class for all WebXR features.
+ * Since most features require almost the same resources and callbacks, this class can be used to simplify the development
+ * Note that since the features manager is using the `IWebXRFeature` you are in no way obligated to use this class
+ */
+export abstract class WebXRAbstractFeature implements IWebXRFeature {
+
+    /**
+     * Construct a new (abstract) webxr feature
+     * @param _xrSessionManager the xr session manager for this feature
+     */
+    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();

+ 12 - 0
src/Lights/Shadows/cascadedShadowGenerator.ts

@@ -492,6 +492,16 @@ export class CascadedShadowGenerator implements IShadowGenerator {
         this._breaksAreDirty = true;
     }
 
+    /** Gets the minimal distance used in the cascade break computation */
+    public get minDistance(): number {
+        return this._minDistance;
+    }
+
+    /** Gets the maximal distance used in the cascade break computation */
+    public get maxDistance(): number {
+        return this._maxDistance;
+    }
+
     /**
      * Gets the class name of that object
      * @returns "ShadowGenerator"
@@ -766,6 +776,8 @@ export class CascadedShadowGenerator implements IShadowGenerator {
             return;
         }
 
+        this._autoCalcDepthBounds = value;
+
         if (!value) {
             if (this._depthReducer) {
                 this._depthReducer.deactivate();