Bläddra i källkod

Merge pull request #7400 from Popov72/csm-minmax-depth-redux

CSM improvement with min/max depth redux
David Catuhe 5 år sedan
förälder
incheckning
efcd60d556

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

@@ -53,6 +53,7 @@
 - PNG support for browsers not supporting SVG ([RaananW](https://github.com/RaananW/))
 - Device orientation event permissions for iOS 13+ ([RaananW](https://github.com/RaananW/))
 - Added `DirectionalLight.autoCalcShadowZBounds` to automatically compute the `shadowMinZ` and `shadowMaxZ` values ([Popov72](https://github.com/Popov72))
+- Added `CascadedShadowGenerator.autoCalcDepthBounds` to improve the shadow quality rendering ([Popov72](https://github.com/Popov72))
 - Improved cascade blending in CSM shadow technic ([Popov72](https://github.com/Popov72))
 
 ### Engine

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

@@ -27,6 +27,8 @@ import { IShadowGenerator } from './shadowGenerator';
 import { DirectionalLight } from '../directionalLight';
 
 import { BoundingInfo } from '../../Culling/boundingInfo';
+import { DepthRenderer } from '../../Rendering/depthRenderer';
+import { DepthReducer } from '../../Misc/depthReducer';
 
 interface ICascade {
     prevBreakDistance: number;
@@ -698,6 +700,89 @@ export class CascadedShadowGenerator implements IShadowGenerator {
         return cascadeNum >= 0 && cascadeNum < this._numCascades ? this._viewMatrices[cascadeNum] : null;
     }
 
+    private _depthRenderer: Nullable<DepthRenderer>;
+    /**
+     * Sets the depth renderer to use when autoCalcDepthBounds is enabled.
+     *
+     * Note that if no depth renderer is set, a new one will be automatically created internally when necessary.
+     *
+     * You should call this function if you already have a depth renderer enabled in your scene, to avoid
+     * doing multiple depth rendering each frame. If you provide your own depth renderer, make sure it stores linear depth!
+     * @param depthRenderer The depth renderer to use when autoCalcDepthBounds is enabled. If you pass null or don't call this function at all, a depth renderer will be automatically created
+     */
+    public setDepthRenderer(depthRenderer: Nullable<DepthRenderer>): void {
+        this._depthRenderer = depthRenderer;
+
+        if (this._depthReducer) {
+            this._depthReducer.setDepthRenderer(this._depthRenderer);
+        }
+    }
+
+    private _depthReducer: Nullable<DepthReducer>;
+    private _autoCalcDepthBounds = false;
+
+    /**
+     * Gets or sets the autoCalcDepthBounds property.
+     *
+     * When enabled, a depth rendering pass is first performed (with an internally created depth renderer or with the one
+     * you provide by calling setDepthRenderer). Then, a min/max reducing is applied on the depth map to compute the
+     * minimal and maximal depth of the map and those values are used as inputs for the setMinMaxDistance() function.
+     * It can greatly enhance the shadow quality, at the expense of more GPU works.
+     * When using this option, you should increase the value of the lambda parameter, and even set it to 1 for best results.
+     */
+    public get autoCalcDepthBounds(): boolean {
+        return this._autoCalcDepthBounds;
+    }
+
+    public set autoCalcDepthBounds(value: boolean) {
+        const camera = this._scene.activeCamera;
+
+        if (!camera) {
+            return;
+        }
+
+        if (!value) {
+            if (this._depthReducer) {
+                this._depthReducer.deactivate();
+            }
+            this.setMinMaxDistance(0, 1);
+            return;
+        }
+
+        if (!this._depthReducer) {
+            this._depthReducer = new DepthReducer(camera);
+            this._depthReducer.onAfterReductionPerformed.add((minmax: { min: number, max: number}) => {
+                let min = minmax.min, max = minmax.max;
+                if (min >= max) {
+                    min = 0;
+                    max = 1;
+                }
+                if (min != this._minDistance || max != this._maxDistance) {
+                    this.setMinMaxDistance(min, max);
+                }
+            });
+            this._depthReducer.setDepthRenderer(this._depthRenderer);
+        }
+
+        this._depthReducer.activate();
+    }
+
+    /**
+     * Defines the refresh rate of the min/max computation used when autoCalcDepthBounds is set to true
+     * Use 0 to compute just once, 1 to compute on every frame, 2 to compute every two frames and so on...
+     * Note that if you provided your own depth renderer through a call to setDepthRenderer, you are responsible
+     * for setting the refresh rate on the renderer yourself!
+     */
+    public get autoCalcDepthBoundsRefreshRate(): number {
+        return this._depthReducer?.depthRenderer?.getDepthMap().refreshRate ?? -1;
+    }
+
+    public set autoCalcDepthBoundsRefreshRate(value: number) {
+        if (this._depthReducer?.depthRenderer) {
+            this._depthReducer.depthRenderer.getDepthMap().refreshRate = value;
+        }
+    }
+
     /**
      * Create the cascade breaks according to the lambda, shadowMaxZ and min/max distance properties, as well as the camera near and far planes.
      * This function is automatically called when updating lambda, shadowMaxZ and min/max distances, however you should call it yourself if

+ 108 - 0
src/Misc/depthReducer.ts

@@ -0,0 +1,108 @@
+import { Nullable } from "../types";
+import { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture";
+import { Camera } from "../Cameras/camera";
+import { Constants } from "../Engines/constants";
+import { DepthRenderer } from "../Rendering/depthRenderer";
+
+import { MinMaxReducer } from "./minMaxReducer";
+
+/**
+ * This class is a small wrapper around the MinMaxReducer class to compute the min/max values of a depth texture
+ */
+export class DepthReducer extends MinMaxReducer {
+
+    private _depthRenderer: Nullable<DepthRenderer>;
+    private _depthRendererId: string;
+
+    /**
+     * Gets the depth renderer used for the computation.
+     * Note that the result is null if you provide your own renderer when calling setDepthRenderer.
+     */
+    public get depthRenderer(): Nullable<DepthRenderer> {
+        return this._depthRenderer;
+    }
+
+    /**
+     * Creates a depth reducer
+     * @param camera The camera used to render the depth texture
+     */
+    constructor(camera: Camera) {
+        super(camera);
+    }
+
+    /**
+     * Sets the depth renderer to use to generate the depth map
+     * @param depthRenderer The depth renderer to use. If not provided, a new one will be created automatically
+     * @param type The texture type of the depth map (default: TEXTURETYPE_HALF_FLOAT)
+     * @param forceFullscreenViewport Forces the post processes used for the reduction to be applied without taking into account viewport (defaults to true)
+     */
+    public setDepthRenderer(depthRenderer: Nullable<DepthRenderer> = null, type: number = Constants.TEXTURETYPE_HALF_FLOAT, forceFullscreenViewport = true): void {
+        const scene = this._camera.getScene();
+
+        if (this._depthRenderer) {
+            delete scene._depthRenderer[this._depthRendererId];
+
+            this._depthRenderer.dispose();
+            this._depthRenderer = null;
+        }
+
+        if (depthRenderer === null) {
+            if (!scene._depthRenderer) {
+                scene._depthRenderer = {};
+            }
+
+            depthRenderer = this._depthRenderer = new DepthRenderer(scene, type, this._camera, false);
+            depthRenderer.enabled = false;
+
+            this._depthRendererId = "minmax" + this._camera.id;
+            scene._depthRenderer[this._depthRendererId] = depthRenderer;
+        }
+
+        super.setSourceTexture(depthRenderer.getDepthMap(), true, type, forceFullscreenViewport);
+    }
+
+    /** @hidden */
+    public setSourceTexture(sourceTexture: RenderTargetTexture, depthRedux: boolean, type: number = Constants.TEXTURETYPE_HALF_FLOAT, forceFullscreenViewport = true): void {
+        super.setSourceTexture(sourceTexture, depthRedux, type, forceFullscreenViewport);
+    }
+
+    /**
+     * Activates the reduction computation.
+     * When activated, the observers registered in onAfterReductionPerformed are
+     * called after the compuation is performed
+     */
+    public activate(): void {
+        if (this._depthRenderer) {
+            this._depthRenderer.enabled = true;
+        }
+
+        super.activate();
+    }
+
+    /**
+     * Deactivates the reduction computation.
+     */
+    public deactivate(): void {
+        super.deactivate();
+
+        if (this._depthRenderer) {
+            this._depthRenderer.enabled = false;
+        }
+    }
+
+    /**
+     * Disposes the depth reducer
+     * @param disposeAll true to dispose all the resources. You should always call this function with true as the parameter (or without any parameter as it is the default one). This flag is meant to be used internally.
+     */
+    public dispose(disposeAll = true): void {
+        super.dispose(disposeAll);
+
+        if (this._depthRenderer && disposeAll) {
+            delete this._depthRenderer.getDepthMap().getScene()?._depthRenderer[this._depthRendererId];
+
+            this._depthRenderer.dispose();
+            this._depthRenderer = null;
+        }
+    }
+
+}

+ 2 - 0
src/Misc/index.ts

@@ -45,3 +45,5 @@ export * from "./canvasGenerator";
 export * from "./fileTools";
 export * from "./stringTools";
 export * from "./dataReader";
+export * from "./minMaxReducer";
+export * from "./depthReducer";

+ 244 - 0
src/Misc/minMaxReducer.ts

@@ -0,0 +1,244 @@
+import { Nullable } from "../types";
+import { RenderTargetTexture } from "../Materials/Textures/renderTargetTexture";
+import { Camera } from "../Cameras/camera";
+import { Constants } from "../Engines/constants";
+import { Observer } from "./observable";
+import { Effect } from "../Materials/effect";
+import { PostProcess } from "../PostProcesses/postProcess";
+import { PostProcessManager } from "../PostProcesses/postProcessManager";
+import { Observable } from "./observable";
+
+import "../Shaders/minmaxRedux.fragment";
+
+/**
+ * This class computes a min/max reduction from a texture: it means it computes the minimum
+ * and maximum values from all values of the texture.
+ * It is performed on the GPU for better performances, thanks to a succession of post processes.
+ * The source values are read from the red channel of the texture.
+ */
+export class MinMaxReducer {
+
+    /**
+     * Observable triggered when the computation has been performed
+     */
+    public onAfterReductionPerformed = new Observable<{ min: number, max: number }>();
+
+    protected _camera: Camera;
+    protected _sourceTexture: Nullable<RenderTargetTexture>;
+    protected _reductionSteps: Nullable<Array<PostProcess>>;
+    protected _postProcessManager: PostProcessManager;
+    protected _onAfterUnbindObserver: Nullable<Observer<RenderTargetTexture>>;
+    protected _forceFullscreenViewport = true;
+
+    /**
+     * Creates a min/max reducer
+     * @param camera The camera to use for the post processes
+     */
+    constructor(camera: Camera) {
+        this._camera = camera;
+        this._postProcessManager = new PostProcessManager(camera.getScene());
+    }
+
+    /**
+     * Gets the texture used to read the values from.
+     */
+    public get sourceTexture(): Nullable<RenderTargetTexture> {
+        return this._sourceTexture;
+    }
+
+    /**
+     * Sets the source texture to read the values from.
+     * One must indicate if the texture is a depth texture or not through the depthRedux parameter
+     * because in such textures '1' value must not be taken into account to compute the maximum
+     * as this value is used to clear the texture.
+     * Note that the computation is not activated by calling this function, you must call activate() for that!
+     * @param sourceTexture The texture to read the values from. The values should be in the red channel.
+     * @param depthRedux Indicates if the texture is a depth texture or not
+     * @param type The type of the textures created for the reduction (defaults to TEXTURETYPE_HALF_FLOAT)
+     * @param forceFullscreenViewport Forces the post processes used for the reduction to be applied without taking into account viewport (defaults to true)
+     */
+    public setSourceTexture(sourceTexture: RenderTargetTexture, depthRedux: boolean, type: number = Constants.TEXTURETYPE_HALF_FLOAT, forceFullscreenViewport = true): void {
+        if (sourceTexture === this._sourceTexture) {
+            return;
+        }
+
+        this.dispose(false);
+
+        this._sourceTexture = sourceTexture;
+        this._reductionSteps = [];
+        this._forceFullscreenViewport = forceFullscreenViewport;
+
+        const scene = this._camera.getScene();
+
+        // create the first step
+        let reductionInitial = new PostProcess(
+            'Initial reduction phase',
+            'minmaxRedux', // shader
+            ['texSize'],
+            ['sourceTexture'], // textures
+            1.0, // options
+            null, // camera
+            Constants.TEXTURE_NEAREST_NEAREST, // sampling
+            scene.getEngine(), // engine
+            false, // reusable
+            "#define INITIAL" + (depthRedux ? "\n#define DEPTH_REDUX" : ""), // defines
+            type,
+            undefined,
+            undefined,
+            undefined,
+            Constants.TEXTUREFORMAT_RG,
+        );
+
+        reductionInitial.autoClear = false;
+        reductionInitial.forceFullscreenViewport = forceFullscreenViewport;
+
+        let w = this._sourceTexture.getRenderWidth(), h = this._sourceTexture.getRenderHeight();
+
+        reductionInitial.onApply = ((w: number, h: number) => {
+            return (effect: Effect) => {
+                effect.setTexture('sourceTexture', this._sourceTexture);
+                effect.setFloatArray2('texSize', new Float32Array([w, h]));
+            };
+        })(w, h);
+
+        this._reductionSteps.push(reductionInitial);
+
+        let index = 1;
+
+        // create the additional steps
+        while (w > 1 || h > 1) {
+            w = Math.max(Math.round(w / 2), 1);
+            h = Math.max(Math.round(h / 2), 1);
+
+            let reduction = new PostProcess(
+                'Reduction phase ' + index,
+                'minmaxRedux', // shader
+                ['texSize'],
+                null,
+                { width: w, height: h }, // options
+                null, // camera
+                Constants.TEXTURE_NEAREST_NEAREST, // sampling
+                scene.getEngine(), // engine
+                false, // reusable
+                "#define " + ((w == 1 && h == 1) ? 'LAST' : (w == 1 || h == 1) ? 'ONEBEFORELAST' : 'MAIN'), // defines
+                type,
+                undefined,
+                undefined,
+                undefined,
+                Constants.TEXTUREFORMAT_RG,
+            );
+
+            reduction.autoClear = false;
+            reduction.forceFullscreenViewport = forceFullscreenViewport;
+
+            reduction.onApply = ((w: number, h: number) => {
+                return (effect: Effect) => {
+                    if (w == 1 || h == 1) {
+                        effect.setIntArray2('texSize', new Int32Array([w, h]));
+                    } else {
+                        effect.setFloatArray2('texSize', new Float32Array([w, h]));
+                    }
+                };
+            })(w, h);
+
+            this._reductionSteps.push(reduction);
+
+            index++;
+
+            if (w == 1 && h == 1) {
+                let func = (w: number, h: number, reduction: PostProcess) => {
+                    let buffer = new Float32Array(4 * w * h),
+                        minmax = { min: 0, max: 0};
+                    return () => {
+                        scene.getEngine()._readTexturePixels(reduction.inputTexture, w, h, -1, 0, buffer);
+                        minmax.min = buffer[0];
+                        minmax.max = buffer[1];
+                        this.onAfterReductionPerformed.notifyObservers(minmax);
+                    };
+                };
+                reduction.onAfterRenderObservable.add(func(w, h, reduction));
+            }
+        }
+    }
+
+    /**
+     * Defines the refresh rate of the computation.
+     * Use 0 to compute just once, 1 to compute on every frame, 2 to compute every two frames and so on...
+     */
+    public get refreshRate(): number {
+        return this._sourceTexture ? this._sourceTexture.refreshRate : -1;
+    }
+
+    public set refreshRate(value: number) {
+        if (this._sourceTexture) {
+            this._sourceTexture.refreshRate = value;
+        }
+    }
+
+    protected _activated = false;
+
+    /**
+     * Gets the activation status of the reducer
+     */
+    public get activated(): boolean {
+        return this._activated;
+    }
+
+    /**
+     * Activates the reduction computation.
+     * When activated, the observers registered in onAfterReductionPerformed are
+     * called after the compuation is performed
+     */
+    public activate(): void {
+        if (this._onAfterUnbindObserver || !this._sourceTexture) {
+            return;
+        }
+
+        this._onAfterUnbindObserver = this._sourceTexture.onAfterUnbindObservable.add(() => {
+            this._reductionSteps![0].activate(this._camera);
+            this._postProcessManager.directRender(this._reductionSteps!, this._reductionSteps![0].inputTexture, this._forceFullscreenViewport);
+            this._camera.getScene().getEngine().unBindFramebuffer(this._reductionSteps![0].inputTexture, false);
+        });
+
+        this._activated = true;
+    }
+
+    /**
+     * Deactivates the reduction computation.
+     */
+    public deactivate(): void {
+        if (!this._onAfterUnbindObserver || !this._sourceTexture) {
+            return;
+        }
+
+        this._sourceTexture.onAfterUnbindObservable.remove(this._onAfterUnbindObserver);
+        this._onAfterUnbindObserver = null;
+        this._activated = false;
+    }
+
+    /**
+     * Disposes the min/max reducer
+     * @param disposeAll true to dispose all the resources. You should always call this function with true as the parameter (or without any parameter as it is the default one). This flag is meant to be used internally.
+     */
+    public dispose(disposeAll = true): void {
+        if (disposeAll) {
+            this.onAfterReductionPerformed.clear();
+        }
+
+        this.deactivate();
+
+        if (this._reductionSteps) {
+            for (let i = 0; i < this._reductionSteps.length; ++i) {
+                this._reductionSteps[i].dispose();
+            }
+            this._reductionSteps = null;
+        }
+
+        if (this._postProcessManager && disposeAll) {
+            this._postProcessManager.dispose();
+        }
+
+        this._sourceTexture = null;
+    }
+
+}

+ 3 - 0
src/Rendering/depthRenderer.ts

@@ -34,6 +34,9 @@ export class DepthRenderer {
     private _cachedDefines: string;
     private _camera: Nullable<Camera>;
 
+    /** Enable or disable the depth renderer. When disabled, the depth texture is not updated */
+    public enabled = true;
+
     /**
      * Specifiess that the depth renderer will only be used within
      * the camera it is created for.

+ 2 - 2
src/Rendering/depthRendererSceneComponent.ts

@@ -114,7 +114,7 @@ export class DepthRendererSceneComponent implements ISceneComponent {
         if (this.scene._depthRenderer) {
             for (var key in this.scene._depthRenderer) {
                 let depthRenderer = this.scene._depthRenderer[key];
-                if (!depthRenderer.useOnlyInActiveCamera) {
+                if (depthRenderer.enabled && !depthRenderer.useOnlyInActiveCamera) {
                     renderTargets.push(depthRenderer.getDepthMap());
                 }
             }
@@ -125,7 +125,7 @@ export class DepthRendererSceneComponent implements ISceneComponent {
         if (this.scene._depthRenderer) {
             for (var key in this.scene._depthRenderer) {
                 let depthRenderer = this.scene._depthRenderer[key];
-                if (depthRenderer.useOnlyInActiveCamera && this.scene.activeCamera!.id === key) {
+                if (depthRenderer.enabled && depthRenderer.useOnlyInActiveCamera && this.scene.activeCamera!.id === key) {
                     renderTargets.push(depthRenderer.getDepthMap());
                 }
             }

+ 70 - 0
src/Shaders/minmaxRedux.fragment.fx

@@ -0,0 +1,70 @@
+attribute vec2 vUV;
+
+uniform sampler2D textureSampler;
+
+#if defined(INITIAL)
+uniform sampler2D sourceTexture;
+uniform vec2 texSize;
+
+void main(void)
+{
+    ivec2 coord = ivec2(vUV * (texSize - 1.0));
+
+    float f1 = texelFetch(sourceTexture, coord, 0).r;
+    float f2 = texelFetch(sourceTexture, coord + ivec2(1, 0), 0).r;
+    float f3 = texelFetch(sourceTexture, coord + ivec2(1, 1), 0).r;
+    float f4 = texelFetch(sourceTexture, coord + ivec2(0, 1), 0).r;
+
+    float minz = min(min(min(f1, f2), f3), f4);
+    #ifdef DEPTH_REDUX
+        float maxz = max(max(max(sign(1.0 - f1) * f1, sign(1.0 - f2) * f2), sign(1.0 - f3) * f3), sign(1.0 - f4) * f4);
+    #else
+        float maxz = max(max(max(f1, f2), f3), f4);
+    #endif
+
+    glFragColor = vec4(minz, maxz, 0., 0.);
+}
+
+#elif defined(MAIN)
+uniform vec2 texSize;
+
+void main(void)
+{
+    ivec2 coord = ivec2(vUV * (texSize - 1.0));
+
+    vec2 f1 = texelFetch(textureSampler, coord, 0).rg;
+    vec2 f2 = texelFetch(textureSampler, coord + ivec2(1, 0), 0).rg;
+    vec2 f3 = texelFetch(textureSampler, coord + ivec2(1, 1), 0).rg;
+    vec2 f4 = texelFetch(textureSampler, coord + ivec2(0, 1), 0).rg;
+
+    float minz = min(min(min(f1.x, f2.x), f3.x), f4.x);
+    float maxz = max(max(max(f1.y, f2.y), f3.y), f4.y);
+
+    glFragColor = vec4(minz, maxz, 0., 0.);
+}
+
+#elif defined(ONEBEFORELAST)
+uniform ivec2 texSize;
+
+void main(void)
+{
+    ivec2 coord = ivec2(vUV * vec2(texSize - 1));
+
+    vec2 f1 = texelFetch(textureSampler, coord % texSize, 0).rg;
+    vec2 f2 = texelFetch(textureSampler, (coord + ivec2(1, 0)) % texSize, 0).rg;
+    vec2 f3 = texelFetch(textureSampler, (coord + ivec2(1, 1)) % texSize, 0).rg;
+    vec2 f4 = texelFetch(textureSampler, (coord + ivec2(0, 1)) % texSize, 0).rg;
+
+    float minz = min(f1.x, f2.x);
+    float maxz = max(f1.y, f2.y);
+
+    glFragColor = vec4(minz, maxz, 0., 0.);
+}
+
+#elif defined(LAST)
+void main(void)
+{
+    discard;
+    glFragColor = vec4(0.);
+}
+#endif

+ 2 - 2
src/sceneComponent.ts

@@ -70,9 +70,9 @@ export class SceneComponentConstants {
 
     public static readonly STEP_AFTERRENDER_AUDIO = 0;
 
-    public static readonly STEP_GATHERRENDERTARGETS_SHADOWGENERATOR = 0;
+    public static readonly STEP_GATHERRENDERTARGETS_DEPTHRENDERER = 0;
     public static readonly STEP_GATHERRENDERTARGETS_GEOMETRYBUFFERRENDERER = 1;
-    public static readonly STEP_GATHERRENDERTARGETS_DEPTHRENDERER = 2;
+    public static readonly STEP_GATHERRENDERTARGETS_SHADOWGENERATOR = 2;
     public static readonly STEP_GATHERRENDERTARGETS_POSTPROCESSRENDERPIPELINEMANAGER = 3;
 
     public static readonly STEP_GATHERACTIVECAMERARENDERTARGETS_DEPTHRENDERER = 0;