/*
 * Copyright (c) 2009-2012 jMonkeyEngine
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * * Redistributions of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * * Redistributions in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in the
 *   documentation and/or other materials provided with the distribution.
 *
 * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
 *   may be used to endorse or promote products derived from this software
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.jme3.shadow;

import com.jme3.asset.AssetManager;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Matrix4f;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.post.SceneProcessor;
import com.jme3.renderer.Camera;
import com.jme3.renderer.Caps;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.GeometryList;
import com.jme3.renderer.queue.OpaqueComparator;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.debug.WireFrustum;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Image.Format;
import com.jme3.texture.Texture.MagFilter;
import com.jme3.texture.Texture.MinFilter;
import com.jme3.texture.Texture.ShadowCompareMode;
import com.jme3.texture.Texture2D;
import com.jme3.ui.Picture;
import java.util.ArrayList;
import java.util.List;

/**
 * PssmShadow renderer use Parrallel Split Shadow Mapping technique (pssm)<br>
 * It splits the view frustum in several parts and compute a shadow map for each
 * one.<br> splits are distributed so that the closer they are from the camera,
 * the smaller they are to maximize the resolution used of the shadow map.<br>
 * This result in a better quality shadow than standard shadow mapping.<br> for
 * more informations on this read this <a
 * href="http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html">http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html</a><br>
 * <p/>
 * @author Rémy Bouquet aka Nehon
 * @deprecated use {@link DirectionalLightShadowRenderer}
 */
@Deprecated
public class PssmShadowRenderer implements SceneProcessor {

    /**
     * <code>FilterMode</code> specifies how shadows are filtered
     * @deprecated use {@link EdgeFilteringMode}
     */
    @Deprecated
    public enum FilterMode{

        /**
         * Shadows are not filtered. Nearest sample is used, causing in blocky
         * shadows.
         */
        Nearest,
        /**
         * Bilinear filtering is used. Has the potential of being hardware
         * accelerated on some GPUs
         */
        Bilinear,
        /**
         * Dither-based sampling is used, very cheap but can look bad at low
         * resolutions.
         */
        Dither,
        /**
         * 4x4 percentage-closer filtering is used. Shadows will be smoother at
         * the cost of performance
         */
        PCF4,
        /**
         * 8x8 percentage-closer filtering is used. Shadows will be smoother at
         * the cost of performance
         */
        PCFPOISSON,
        /**
         * 8x8 percentage-closer filtering is used. Shadows will be smoother at
         * the cost of performance
         */
        PCF8
    }

    /**
     * Specifies the shadow comparison mode
     * @deprecated use {@link CompareMode}
     */
    @Deprecated
    public enum CompareMode {

        /**
         * Shadow depth comparisons are done by using shader code
         */
        Software,
        /**
         * Shadow depth comparisons are done by using the GPU's dedicated
         * shadowing pipeline.
         */
        Hardware;
    }
    protected int nbSplits = 3;
    protected float shadowMapSize;
    protected float lambda = 0.65f;
    protected float shadowIntensity = 0.7f;
    protected float zFarOverride = 0;
    protected RenderManager renderManager;
    protected ViewPort viewPort;
    protected FrameBuffer[] shadowFB;
    protected Texture2D[] shadowMaps;
    protected Texture2D dummyTex;
    protected Camera shadowCam;
    protected Material preshadowMat;
    protected Material postshadowMat;
    protected GeometryList splitOccluders = new GeometryList(new OpaqueComparator());
    protected Matrix4f[] lightViewProjectionsMatrices;
    protected ColorRGBA splits;
    protected float[] splitsArray;
    protected boolean noOccluders = false;
    protected Vector3f direction = new Vector3f();
    protected AssetManager assetManager;
    protected boolean debug = false;
    protected float edgesThickness = 1.0f;
    protected FilterMode filterMode;
    protected CompareMode compareMode;
    protected Picture[] dispPic;
    protected Vector3f[] points = new Vector3f[8];
    protected boolean flushQueues = true;
    // define if the fallback material should be used.
    protected boolean needsfallBackMaterial = false;
    //Name of the post material technique
    protected String postTechniqueName = "PostShadow";
    //flags to know when to change params in the materials
    protected boolean applyHWShadows = true;
    protected boolean applyFilterMode = true;
    protected boolean applyPCFEdge = true;
    protected boolean applyShadowIntensity = true;
    //a list of material of the post shadow queue geometries.
    protected List<Material> matCache = new ArrayList<Material>();
    //Holding the info for fading shadows in the far distance 
    protected Vector2f fadeInfo;
    protected float fadeLength;
    protected boolean applyFadeInfo = false;

    protected GeometryList lightReceivers = new GeometryList(new OpaqueComparator());
    
    /**
     * Create a PSSM Shadow Renderer More info on the technique at <a
     * href="http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html">http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html</a>
     *
     * @param manager the application asset manager
     * @param size the size of the rendered shadowmaps (512,1024,2048, etc...)
     * @param nbSplits the number of shadow maps rendered (the more shadow maps
     * the more quality, the less fps).
     * @param nbSplits the number of shadow maps rendered (the more shadow maps
     * the more quality, the less fps).
     */
    public PssmShadowRenderer(AssetManager manager, int size, int nbSplits) {
        this(manager, size, nbSplits, new Material(manager, "Common/MatDefs/Shadow/PostShadow.j3md"));
    }

    /**
     * Create a PSSM Shadow Renderer More info on the technique at <a
     * href="http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html">http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html</a>
     *
     * @param manager the application asset manager
     * @param size the size of the rendered shadowmaps (512,1024,2048, etc...)
     * @param nbSplits the number of shadow maps rendered (the more shadow maps
     * the more quality, the less fps).
     * @param postShadowMat the material used for post shadows if you need to
     * override it
     */
    protected PssmShadowRenderer(AssetManager manager, int size, int nbSplits, Material postShadowMat) {

        this.postshadowMat = postShadowMat;
        assetManager = manager;
        nbSplits = Math.max(Math.min(nbSplits, 4), 1);
        this.nbSplits = nbSplits;
        shadowMapSize = size;

        shadowFB = new FrameBuffer[nbSplits];
        shadowMaps = new Texture2D[nbSplits];
        dispPic = new Picture[nbSplits];
        lightViewProjectionsMatrices = new Matrix4f[nbSplits];
        splits = new ColorRGBA();
        splitsArray = new float[nbSplits + 1];

        //DO NOT COMMENT THIS (it prevent the OSX incomplete read buffer crash)
        dummyTex = new Texture2D(size, size, Format.RGBA8);

        preshadowMat = new Material(manager, "Common/MatDefs/Shadow/PreShadow.j3md");
        postshadowMat.setFloat("ShadowMapSize", size);

        for (int i = 0; i < nbSplits; i++) {
            lightViewProjectionsMatrices[i] = new Matrix4f();
            shadowFB[i] = new FrameBuffer(size, size, 1);
            shadowMaps[i] = new Texture2D(size, size, Format.Depth);

            shadowFB[i].setDepthTexture(shadowMaps[i]);

            //DO NOT COMMENT THIS (it prevent the OSX incomplete read buffer crash)
            shadowFB[i].setColorTexture(dummyTex);

            postshadowMat.setTexture("ShadowMap" + i, shadowMaps[i]);

            //quads for debuging purpose
            dispPic[i] = new Picture("Picture" + i);
            dispPic[i].setTexture(manager, shadowMaps[i], false);
        }

        setCompareMode(CompareMode.Hardware);
        setFilterMode(FilterMode.Bilinear);
        setShadowIntensity(0.7f);

        shadowCam = new Camera(size, size);
        shadowCam.setParallelProjection(true);

        for (int i = 0; i < points.length; i++) {
            points[i] = new Vector3f();
        }

    }

    /**
     * Sets the filtering mode for shadow edges see {@link FilterMode} for more
     * info
     *
     * @param filterMode
     */
    final public void setFilterMode(FilterMode filterMode) {
        if (filterMode == null) {
            throw new NullPointerException();
        }

        if (this.filterMode == filterMode) {
            return;
        }

        this.filterMode = filterMode;
        postshadowMat.setInt("FilterMode", filterMode.ordinal());
        postshadowMat.setFloat("PCFEdge", edgesThickness);
        if (compareMode == CompareMode.Hardware) {
            for (Texture2D shadowMap : shadowMaps) {
                if (filterMode == FilterMode.Bilinear) {
                    shadowMap.setMagFilter(MagFilter.Bilinear);
                    shadowMap.setMinFilter(MinFilter.BilinearNoMipMaps);
                } else {
                    shadowMap.setMagFilter(MagFilter.Nearest);
                    shadowMap.setMinFilter(MinFilter.NearestNoMipMaps);
                }
            }
        }
        applyFilterMode = true;
    }

    /**
     * sets the shadow compare mode see {@link CompareMode} for more info
     *
     * @param compareMode
     */
    final public void setCompareMode(CompareMode compareMode) {
        if (compareMode == null) {
            throw new NullPointerException();
        }

        if (this.compareMode == compareMode) {
            return;
        }

        this.compareMode = compareMode;
        for (Texture2D shadowMap : shadowMaps) {
            if (compareMode == CompareMode.Hardware) {
                shadowMap.setShadowCompareMode(ShadowCompareMode.LessOrEqual);
                if (filterMode == FilterMode.Bilinear) {
                    shadowMap.setMagFilter(MagFilter.Bilinear);
                    shadowMap.setMinFilter(MinFilter.BilinearNoMipMaps);
                } else {
                    shadowMap.setMagFilter(MagFilter.Nearest);
                    shadowMap.setMinFilter(MinFilter.NearestNoMipMaps);
                }
            } else {
                shadowMap.setShadowCompareMode(ShadowCompareMode.Off);
                shadowMap.setMagFilter(MagFilter.Nearest);
                shadowMap.setMinFilter(MinFilter.NearestNoMipMaps);
            }
        }
        postshadowMat.setBoolean("HardwareShadows", compareMode == CompareMode.Hardware);
        applyHWShadows = true;
    }

    //debug function that create a displayable frustrum
    private Geometry createFrustum(Vector3f[] pts, int i) {
        WireFrustum frustum = new WireFrustum(pts);
        Geometry frustumMdl = new Geometry("f", frustum);
        frustumMdl.setCullHint(Spatial.CullHint.Never);
        frustumMdl.setShadowMode(ShadowMode.Off);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.getAdditionalRenderState().setWireframe(true);
        frustumMdl.setMaterial(mat);
        switch (i) {
            case 0:
                frustumMdl.getMaterial().setColor("Color", ColorRGBA.Pink);
                break;
            case 1:
                frustumMdl.getMaterial().setColor("Color", ColorRGBA.Red);
                break;
            case 2:
                frustumMdl.getMaterial().setColor("Color", ColorRGBA.Green);
                break;
            case 3:
                frustumMdl.getMaterial().setColor("Color", ColorRGBA.Blue);
                break;
            default:
                frustumMdl.getMaterial().setColor("Color", ColorRGBA.White);
                break;
        }

        frustumMdl.updateGeometricState();
        return frustumMdl;
    }

    public void initialize(RenderManager rm, ViewPort vp) {
        renderManager = rm;
        viewPort = vp;
        //checking for caps to chosse the appropriate post material technique
        if (renderManager.getRenderer().getCaps().contains(Caps.GLSL150)) {
            postTechniqueName = "PostShadow15";
        } else {
            postTechniqueName = "PostShadow";
        }
    }

    public boolean isInitialized() {
        return viewPort != null;
    }

    /**
     * returns the light direction used by the processor
     *
     * @return
     */
    public Vector3f getDirection() {
        return direction;
    }

    /**
     * Sets the light direction to use to compute shadows
     *
     * @param direction
     */
    public void setDirection(Vector3f direction) {
        this.direction.set(direction).normalizeLocal();
    }

    @SuppressWarnings("fallthrough")
    public void postQueue(RenderQueue rq) {
        for (Spatial scene : viewPort.getScenes()) {
            ShadowUtil.getGeometriesInCamFrustum(scene, viewPort.getCamera(), ShadowMode.Receive, lightReceivers);
        }

        Camera viewCam = viewPort.getCamera();

        float zFar = zFarOverride;
        if (zFar == 0) {
            zFar = viewCam.getFrustumFar();
        }

        //We prevent computing the frustum points and splits with zeroed or negative near clip value
        float frustumNear = Math.max(viewCam.getFrustumNear(), 0.001f);
        ShadowUtil.updateFrustumPoints(viewCam, frustumNear, zFar, 1.0f, points);

        //shadowCam.setDirection(direction);
        shadowCam.getRotation().lookAt(direction, shadowCam.getUp());
        shadowCam.update();
        shadowCam.updateViewProjection();

        PssmShadowUtil.updateFrustumSplits(splitsArray, frustumNear, zFar, lambda);


        switch (splitsArray.length) {
            case 5:
                splits.a = splitsArray[4];
            case 4:
                splits.b = splitsArray[3];
            case 3:
                splits.g = splitsArray[2];
            case 2:
            case 1:
                splits.r = splitsArray[1];
                break;
        }

        Renderer r = renderManager.getRenderer();
        renderManager.setForcedMaterial(preshadowMat);
        renderManager.setForcedTechnique("PreShadow");

        for (int i = 0; i < nbSplits; i++) {

            // update frustum points based on current camera and split
            ShadowUtil.updateFrustumPoints(viewCam, splitsArray[i], splitsArray[i + 1], 1.0f, points);

            //Updating shadow cam with curent split frustra
            ShadowUtil.updateShadowCamera(viewPort, lightReceivers, shadowCam, points, splitOccluders, shadowMapSize);

            //saving light view projection matrix for this split            
            lightViewProjectionsMatrices[i].set(shadowCam.getViewProjectionMatrix());
            renderManager.setCamera(shadowCam, false);

            if (debugfrustums) {
//                    frustrumFromBound(b.casterBB,ColorRGBA.Blue );
//                    frustrumFromBound(b.receiverBB,ColorRGBA.Green );
//                    frustrumFromBound(b.splitBB,ColorRGBA.Yellow );
                ((Node) viewPort.getScenes().get(0)).attachChild(createFrustum(points, i));
                ShadowUtil.updateFrustumPoints2(shadowCam, points);
                ((Node) viewPort.getScenes().get(0)).attachChild(createFrustum(points, i));

            }

            r.setFrameBuffer(shadowFB[i]);
            r.clearBuffers(true, true, true);

            // render shadow casters to shadow map
            viewPort.getQueue().renderShadowQueue(splitOccluders, renderManager, shadowCam, true);
        }
        debugfrustums = false;

        //restore setting for future rendering
        r.setFrameBuffer(viewPort.getOutputFrameBuffer());
        renderManager.setForcedMaterial(null);
        renderManager.setForcedTechnique(null);
        renderManager.setCamera(viewCam, false);

    }
    boolean debugfrustums = false;

    public void displayFrustum() {
        debugfrustums = true;
    }

    //debug only : displays depth shadow maps
    protected void displayShadowMap(Renderer r) {
        Camera cam = viewPort.getCamera();
        renderManager.setCamera(cam, true);
        int h = cam.getHeight();
        for (int i = 0; i < dispPic.length; i++) {
            dispPic[i].setPosition((128 * i) + (150 + 64 * (i + 1)), h / 20f);
            dispPic[i].setWidth(128);
            dispPic[i].setHeight(128);
            dispPic[i].updateGeometricState();
            renderManager.renderGeometry(dispPic[i]);
        }
        renderManager.setCamera(cam, false);
    }

    /**
     * For dubuging purpose Allow to "snapshot" the current frustrum to the
     * scene
     */
    public void displayDebug() {
        debug = true;
    }

    public void postFrame(FrameBuffer out) {

        if (debug) {
            displayShadowMap(renderManager.getRenderer());
        }
        if (!noOccluders) {
            //setting params to recieving geometry list
            setMatParams();

            Camera cam = viewPort.getCamera();
            //some materials in the scene does not have a post shadow technique so we're using the fall back material
            if (needsfallBackMaterial) {
                renderManager.setForcedMaterial(postshadowMat);
            }

            //forcing the post shadow technique and render state
            renderManager.setForcedTechnique(postTechniqueName);

            //rendering the post shadow pass
            viewPort.getQueue().renderShadowQueue(lightReceivers, renderManager, cam, true);

            //resetting renderManager settings
            renderManager.setForcedTechnique(null);
            renderManager.setForcedMaterial(null);
            renderManager.setCamera(cam, false);

        }

    }

    private void setMatParams() {

        GeometryList l = lightReceivers;

        //iteration throught all the geometries of the list to gather the materials

        matCache.clear();
        for (int i = 0; i < l.size(); i++) {
            Material mat = l.get(i).getMaterial();
            //checking if the material has the post technique and adding it to the material cache
            if (mat.getMaterialDef().getTechniqueDef(postTechniqueName) != null) {
                if (!matCache.contains(mat)) {
                    matCache.add(mat);
                }
            } else {
                needsfallBackMaterial = true;
            }
        }

        //iterating through the mat cache and setting the parameters
        for (Material mat : matCache) {
            mat.setColor("Splits", splits);
            mat.setFloat("ShadowMapSize", shadowMapSize);

            for (int j = 0; j < nbSplits; j++) {
                mat.setMatrix4("LightViewProjectionMatrix" + j, lightViewProjectionsMatrices[j]);
            }
            for (int j = 0; j < nbSplits; j++) {
                mat.setTexture("ShadowMap" + j, shadowMaps[j]);
            }
            mat.setBoolean("HardwareShadows", compareMode == CompareMode.Hardware);
            mat.setInt("FilterMode", filterMode.ordinal());
            mat.setFloat("PCFEdge", edgesThickness);
            mat.setFloat("ShadowIntensity", shadowIntensity);

            if (fadeInfo != null) {
                mat.setVector2("FadeInfo", fadeInfo);
            }

        }

        applyHWShadows = false;
        applyFilterMode = false;
        applyPCFEdge = false;
        applyShadowIntensity = false;
        applyFadeInfo = false;

        //At least one material of the receiving geoms does not support the post shadow techniques
        //so we fall back to the forced material solution (transparent shadows won't be supported for these objects)
        if (needsfallBackMaterial) {
            setPostShadowParams();
        }

    }

    protected void setPostShadowParams() {
        postshadowMat.setColor("Splits", splits);
        for (int j = 0; j < nbSplits; j++) {
            postshadowMat.setMatrix4("LightViewProjectionMatrix" + j, lightViewProjectionsMatrices[j]);
            postshadowMat.setTexture("ShadowMap" + j, shadowMaps[j]);
        }        
    }

    public void preFrame(float tpf) {
    }

    public void cleanup() {
    }

    public void reshape(ViewPort vp, int w, int h) {
    }

    /**
     * returns the labda parameter see #setLambda(float lambda)
     *
     * @return lambda
     */
    public float getLambda() {
        return lambda;
    }

    /*
     * Adjust the repartition of the different shadow maps in the shadow extend
     * usualy goes from 0.0 to 1.0
     * a low value give a more linear repartition resulting in a constant quality in the shadow over the extends, but near shadows could look very jagged
     * a high value give a more logarithmic repartition resulting in a high quality for near shadows, but the quality quickly decrease over the extend.
     * the default value is set to 0.65f (theoric optimal value).
     * @param lambda the lambda value.
     */
    public void setLambda(float lambda) {
        this.lambda = lambda;
    }

    /**
     * How far the shadows are rendered in the view
     *
     * @see #setShadowZExtend(float zFar)
     * @return shadowZExtend
     */
    public float getShadowZExtend() {
        return zFarOverride;
    }

    /**
     * Set the distance from the eye where the shadows will be rendered default
     * value is dynamicaly computed to the shadow casters/receivers union bound
     * zFar, capped to view frustum far value.
     *
     * @param zFar the zFar values that override the computed one
     */
    public void setShadowZExtend(float zFar) {
        if (fadeInfo != null) {
            fadeInfo.set(zFar - fadeLength, 1f / fadeLength);
        }
        this.zFarOverride = zFar;

    }

    /**
     * returns the shdaow intensity
     *
     * @see #setShadowIntensity(float shadowIntensity)
     * @return shadowIntensity
     */
    public float getShadowIntensity() {
        return shadowIntensity;
    }

    /**
     * Set the shadowIntensity, the value should be between 0 and 1, a 0 value
     * gives a bright and invisilble shadow, a 1 value gives a pitch black
     * shadow, default is 0.7
     *
     * @param shadowIntensity the darkness of the shadow
     */
    final public void setShadowIntensity(float shadowIntensity) {
        this.shadowIntensity = shadowIntensity;
        postshadowMat.setFloat("ShadowIntensity", shadowIntensity);
        applyShadowIntensity = true;
    }

    /**
     * returns the edges thickness
     *
     * @see #setEdgesThickness(int edgesThickness)
     * @return edgesThickness
     */
    public int getEdgesThickness() {
        return (int) (edgesThickness * 10);
    }

    /**
     * Sets the shadow edges thickness. default is 1, setting it to lower values
     * can help to reduce the jagged effect of the shadow edges
     *
     * @param edgesThickness
     */
    public void setEdgesThickness(int edgesThickness) {
        this.edgesThickness = Math.max(1, Math.min(edgesThickness, 10));
        this.edgesThickness *= 0.1f;
        postshadowMat.setFloat("PCFEdge", edgesThickness);
        applyPCFEdge = true;
    }

    /**
     * returns true if the PssmRenderer flushed the shadow queues
     *
     * @return flushQueues
     */
    public boolean isFlushQueues() {
        return flushQueues;
    }

    /**
     * Set this to false if you want to use several PssmRederers to have
     * multiple shadows cast by multiple light sources. Make sure the last
     * PssmRenderer in the stack DO flush the queues, but not the others
     *
     * @param flushQueues
     */
    public void setFlushQueues(boolean flushQueues) {
        this.flushQueues = flushQueues;
    }

    /**
     * Define the length over which the shadow will fade out when using a
     * shadowZextend This is useful to make dynamic shadows fade into baked
     * shadows in the distance.
     *
     * @param length the fade length in world units
     */
    public void setShadowZFadeLength(float length) {
        if (length == 0) {
            fadeInfo = null;
            fadeLength = 0;
            postshadowMat.clearParam("FadeInfo");
        } else {
            if (zFarOverride == 0) {
                fadeInfo = new Vector2f(0, 0);
            } else {
                fadeInfo = new Vector2f(zFarOverride - length, 1.0f / length);
            }
            fadeLength = length;
            postshadowMat.setVector2("FadeInfo", fadeInfo);
        }
    }

    /**
     * get the length over which the shadow will fade out when using a
     * shadowZextend
     *
     * @return the fade length in world units
     */
    public float getShadowZFadeLength() {
        if (fadeInfo != null) {
            return zFarOverride - fadeInfo.x;
        }
        return 0f;
    }
}
