Home | History | Annotate | Download | only in shadow
      1 /*
      2  * Copyright (c) 2009-2010 jMonkeyEngine All rights reserved.
      3  * <p/>
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions are met:
      6  *
      7  * * Redistributions of source code must retain the above copyright notice,
      8  * this list of conditions and the following disclaimer.
      9  * <p/>
     10  * * Redistributions in binary form must reproduce the above copyright
     11  * notice, this list of conditions and the following disclaimer in the
     12  * documentation and/or other materials provided with the distribution.
     13  * <p/>
     14  * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
     15  * may be used to endorse or promote products derived from this software
     16  * without specific prior written permission.
     17  * <p/>
     18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
     19  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     20  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     21  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
     22  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     23  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
     24  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
     25  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     26  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     27  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
     28  * POSSIBILITY OF SUCH DAMAGE.
     29  */
     30 package com.jme3.shadow;
     31 
     32 import com.jme3.asset.AssetManager;
     33 import com.jme3.material.Material;
     34 import com.jme3.math.ColorRGBA;
     35 import com.jme3.math.Matrix4f;
     36 import com.jme3.math.Vector3f;
     37 import com.jme3.post.SceneProcessor;
     38 import com.jme3.renderer.Camera;
     39 import com.jme3.renderer.RenderManager;
     40 import com.jme3.renderer.Renderer;
     41 import com.jme3.renderer.ViewPort;
     42 import com.jme3.renderer.queue.GeometryList;
     43 import com.jme3.renderer.queue.OpaqueComparator;
     44 import com.jme3.renderer.queue.RenderQueue;
     45 import com.jme3.renderer.queue.RenderQueue.ShadowMode;
     46 import com.jme3.scene.Geometry;
     47 import com.jme3.scene.Spatial;
     48 import com.jme3.scene.debug.WireFrustum;
     49 import com.jme3.texture.FrameBuffer;
     50 import com.jme3.texture.Image.Format;
     51 import com.jme3.texture.Texture.MagFilter;
     52 import com.jme3.texture.Texture.MinFilter;
     53 import com.jme3.texture.Texture.ShadowCompareMode;
     54 import com.jme3.texture.Texture2D;
     55 import com.jme3.ui.Picture;
     56 
     57 /**
     58  * PssmShadow renderer use Parrallel Split Shadow Mapping technique (pssm)<br>
     59  * It splits the view frustum in several parts and compute a shadow map for each
     60  * one.<br> splits are distributed so that the closer they are from the camera,
     61  * the smaller they are to maximize the resolution used of the shadow map.<br>
     62  * This result in a better quality shadow than standard shadow mapping.<br> for
     63  * more informations on this read this
     64  * <a href="http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html">http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html</a><br>
     65  * <p/>
     66  * @author Rmy Bouquet aka Nehon
     67  */
     68 public class PssmShadowRenderer implements SceneProcessor {
     69 
     70     /**
     71      * <code>FilterMode</code> specifies how shadows are filtered
     72      */
     73     public enum FilterMode {
     74 
     75         /**
     76          * Shadows are not filtered. Nearest sample is used, causing in blocky
     77          * shadows.
     78          */
     79         Nearest,
     80         /**
     81          * Bilinear filtering is used. Has the potential of being hardware
     82          * accelerated on some GPUs
     83          */
     84         Bilinear,
     85         /**
     86          * Dither-based sampling is used, very cheap but can look bad
     87          * at low resolutions.
     88          */
     89         Dither,
     90         /**
     91          * 4x4 percentage-closer filtering is used. Shadows will be smoother
     92          * at the cost of performance
     93          */
     94         PCF4,
     95         /**
     96          * 8x8 percentage-closer  filtering is used. Shadows will be smoother
     97          * at the cost of performance
     98          */
     99         PCF8
    100     }
    101 
    102     /**
    103      * Specifies the shadow comparison mode
    104      */
    105     public enum CompareMode {
    106 
    107         /**
    108          * Shadow depth comparisons are done by using shader code
    109          */
    110         Software,
    111         /**
    112          * Shadow depth comparisons are done by using the GPU's dedicated
    113          * shadowing pipeline.
    114          */
    115         Hardware;
    116     }
    117     private int nbSplits = 3;
    118     private float lambda = 0.65f;
    119     private float shadowIntensity = 0.7f;
    120     private float zFarOverride = 0;
    121     private RenderManager renderManager;
    122     private ViewPort viewPort;
    123     private FrameBuffer[] shadowFB;
    124     private Texture2D[] shadowMaps;
    125     private Texture2D dummyTex;
    126     private Camera shadowCam;
    127     private Material preshadowMat;
    128     private Material postshadowMat;
    129     private GeometryList splitOccluders = new GeometryList(new OpaqueComparator());
    130     private Matrix4f[] lightViewProjectionsMatrices;
    131     private ColorRGBA splits;
    132     private float[] splitsArray;
    133     private boolean noOccluders = false;
    134     private Vector3f direction = new Vector3f();
    135     private AssetManager assetManager;
    136     private boolean debug = false;
    137     private float edgesThickness = 1.0f;
    138     private FilterMode filterMode;
    139     private CompareMode compareMode;
    140     private Picture[] dispPic;
    141     private Vector3f[] points = new Vector3f[8];
    142     private boolean flushQueues = true;
    143 
    144     /**
    145      * Create a PSSM Shadow Renderer
    146      * 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>
    147      * @param manager the application asset manager
    148      * @param size the size of the rendered shadowmaps (512,1024,2048, etc...)
    149      * @param nbSplits the number of shadow maps rendered (the more shadow maps the more quality, the less fps).
    150      *  @param nbSplits the number of shadow maps rendered (the more shadow maps the more quality, the less fps).
    151      */
    152     public PssmShadowRenderer(AssetManager manager, int size, int nbSplits) {
    153         this(manager, size, nbSplits, new Material(manager, "Common/MatDefs/Shadow/PostShadowPSSM.j3md"));
    154 
    155     }
    156 
    157     /**
    158      * Create a PSSM Shadow Renderer
    159      * 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>
    160      * @param manager the application asset manager
    161      * @param size the size of the rendered shadowmaps (512,1024,2048, etc...)
    162      * @param nbSplits the number of shadow maps rendered (the more shadow maps the more quality, the less fps).
    163      * @param postShadowMat the material used for post shadows if you need to override it      *
    164      */
    165     //TODO remove the postShadowMat when we have shader injection....or remove this todo if we are in 2020.
    166     public PssmShadowRenderer(AssetManager manager, int size, int nbSplits, Material postShadowMat) {
    167         assetManager = manager;
    168         nbSplits = Math.max(Math.min(nbSplits, 4), 1);
    169         this.nbSplits = nbSplits;
    170 
    171         shadowFB = new FrameBuffer[nbSplits];
    172         shadowMaps = new Texture2D[nbSplits];
    173         dispPic = new Picture[nbSplits];
    174         lightViewProjectionsMatrices = new Matrix4f[nbSplits];
    175         splits = new ColorRGBA();
    176         splitsArray = new float[nbSplits + 1];
    177 
    178         //DO NOT COMMENT THIS (it prevent the OSX incomplete read buffer crash)
    179         dummyTex = new Texture2D(size, size, Format.RGBA8);
    180 
    181         preshadowMat = new Material(manager, "Common/MatDefs/Shadow/PreShadow.j3md");
    182         this.postshadowMat = postShadowMat;
    183 
    184         for (int i = 0; i < nbSplits; i++) {
    185             lightViewProjectionsMatrices[i] = new Matrix4f();
    186             shadowFB[i] = new FrameBuffer(size, size, 1);
    187             shadowMaps[i] = new Texture2D(size, size, Format.Depth);
    188 
    189             shadowFB[i].setDepthTexture(shadowMaps[i]);
    190 
    191             //DO NOT COMMENT THIS (it prevent the OSX incomplete read buffer crash)
    192             shadowFB[i].setColorTexture(dummyTex);
    193 
    194             postshadowMat.setTexture("ShadowMap" + i, shadowMaps[i]);
    195 
    196             //quads for debuging purpose
    197             dispPic[i] = new Picture("Picture" + i);
    198             dispPic[i].setTexture(manager, shadowMaps[i], false);
    199         }
    200 
    201         setCompareMode(CompareMode.Hardware);
    202         setFilterMode(FilterMode.Bilinear);
    203         setShadowIntensity(0.7f);
    204 
    205         shadowCam = new Camera(size, size);
    206         shadowCam.setParallelProjection(true);
    207 
    208         for (int i = 0; i < points.length; i++) {
    209             points[i] = new Vector3f();
    210         }
    211     }
    212 
    213     /**
    214      * Sets the filtering mode for shadow edges see {@link FilterMode} for more info
    215      * @param filterMode
    216      */
    217     public void setFilterMode(FilterMode filterMode) {
    218         if (filterMode == null) {
    219             throw new NullPointerException();
    220         }
    221 
    222         if (this.filterMode == filterMode) {
    223             return;
    224         }
    225 
    226         this.filterMode = filterMode;
    227         postshadowMat.setInt("FilterMode", filterMode.ordinal());
    228         postshadowMat.setFloat("PCFEdge", edgesThickness);
    229         if (compareMode == CompareMode.Hardware) {
    230             for (Texture2D shadowMap : shadowMaps) {
    231                 if (filterMode == FilterMode.Bilinear) {
    232                     shadowMap.setMagFilter(MagFilter.Bilinear);
    233                     shadowMap.setMinFilter(MinFilter.BilinearNoMipMaps);
    234                 } else {
    235                     shadowMap.setMagFilter(MagFilter.Nearest);
    236                     shadowMap.setMinFilter(MinFilter.NearestNoMipMaps);
    237                 }
    238             }
    239         }
    240     }
    241 
    242     /**
    243      * sets the shadow compare mode see {@link CompareMode} for more info
    244      * @param compareMode
    245      */
    246     public void setCompareMode(CompareMode compareMode) {
    247         if (compareMode == null) {
    248             throw new NullPointerException();
    249         }
    250 
    251         if (this.compareMode == compareMode) {
    252             return;
    253         }
    254 
    255         this.compareMode = compareMode;
    256         for (Texture2D shadowMap : shadowMaps) {
    257             if (compareMode == CompareMode.Hardware) {
    258                 shadowMap.setShadowCompareMode(ShadowCompareMode.LessOrEqual);
    259                 if (filterMode == FilterMode.Bilinear) {
    260                     shadowMap.setMagFilter(MagFilter.Bilinear);
    261                     shadowMap.setMinFilter(MinFilter.BilinearNoMipMaps);
    262                 } else {
    263                     shadowMap.setMagFilter(MagFilter.Nearest);
    264                     shadowMap.setMinFilter(MinFilter.NearestNoMipMaps);
    265                 }
    266             } else {
    267                 shadowMap.setShadowCompareMode(ShadowCompareMode.Off);
    268                 shadowMap.setMagFilter(MagFilter.Nearest);
    269                 shadowMap.setMinFilter(MinFilter.NearestNoMipMaps);
    270             }
    271         }
    272         postshadowMat.setBoolean("HardwareShadows", compareMode == CompareMode.Hardware);
    273     }
    274 
    275     //debug function that create a displayable frustrum
    276     private Geometry createFrustum(Vector3f[] pts, int i) {
    277         WireFrustum frustum = new WireFrustum(pts);
    278         Geometry frustumMdl = new Geometry("f", frustum);
    279         frustumMdl.setCullHint(Spatial.CullHint.Never);
    280         frustumMdl.setShadowMode(ShadowMode.Off);
    281         Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
    282         mat.getAdditionalRenderState().setWireframe(true);
    283         frustumMdl.setMaterial(mat);
    284         switch (i) {
    285             case 0:
    286                 frustumMdl.getMaterial().setColor("Color", ColorRGBA.Pink);
    287                 break;
    288             case 1:
    289                 frustumMdl.getMaterial().setColor("Color", ColorRGBA.Red);
    290                 break;
    291             case 2:
    292                 frustumMdl.getMaterial().setColor("Color", ColorRGBA.Green);
    293                 break;
    294             case 3:
    295                 frustumMdl.getMaterial().setColor("Color", ColorRGBA.Blue);
    296                 break;
    297             default:
    298                 frustumMdl.getMaterial().setColor("Color", ColorRGBA.White);
    299                 break;
    300         }
    301 
    302         frustumMdl.updateGeometricState();
    303         return frustumMdl;
    304     }
    305 
    306     public void initialize(RenderManager rm, ViewPort vp) {
    307         renderManager = rm;
    308         viewPort = vp;
    309     }
    310 
    311     public boolean isInitialized() {
    312         return viewPort != null;
    313     }
    314 
    315     /**
    316      * returns the light direction used by the processor
    317      * @return
    318      */
    319     public Vector3f getDirection() {
    320         return direction;
    321     }
    322 
    323     /**
    324      * Sets the light direction to use to compute shadows
    325      * @param direction
    326      */
    327     public void setDirection(Vector3f direction) {
    328         this.direction.set(direction).normalizeLocal();
    329     }
    330 
    331     @SuppressWarnings("fallthrough")
    332     public void postQueue(RenderQueue rq) {
    333         GeometryList occluders = rq.getShadowQueueContent(ShadowMode.Cast);
    334         if (occluders.size() == 0) {
    335             return;
    336         }
    337 
    338         GeometryList receivers = rq.getShadowQueueContent(ShadowMode.Receive);
    339         if (receivers.size() == 0) {
    340             return;
    341         }
    342 
    343         Camera viewCam = viewPort.getCamera();
    344 
    345         float zFar = zFarOverride;
    346         if (zFar == 0) {
    347             zFar = viewCam.getFrustumFar();
    348         }
    349 
    350         //We prevent computing the frustum points and splits with zeroed or negative near clip value
    351         float frustumNear = Math.max(viewCam.getFrustumNear(), 0.001f);
    352         ShadowUtil.updateFrustumPoints(viewCam, frustumNear, zFar, 1.0f, points);
    353 
    354         //shadowCam.setDirection(direction);
    355         shadowCam.getRotation().lookAt(direction, shadowCam.getUp());
    356         shadowCam.update();
    357         shadowCam.updateViewProjection();
    358 
    359         PssmShadowUtil.updateFrustumSplits(splitsArray, frustumNear, zFar, lambda);
    360 
    361 
    362         switch (splitsArray.length) {
    363             case 5:
    364                 splits.a = splitsArray[4];
    365             case 4:
    366                 splits.b = splitsArray[3];
    367             case 3:
    368                 splits.g = splitsArray[2];
    369             case 2:
    370             case 1:
    371                 splits.r = splitsArray[1];
    372                 break;
    373         }
    374 
    375         Renderer r = renderManager.getRenderer();
    376         renderManager.setForcedMaterial(preshadowMat);
    377         renderManager.setForcedTechnique("PreShadow");
    378 
    379         for (int i = 0; i < nbSplits; i++) {
    380 
    381             // update frustum points based on current camera and split
    382             ShadowUtil.updateFrustumPoints(viewCam, splitsArray[i], splitsArray[i + 1], 1.0f, points);
    383 
    384             //Updating shadow cam with curent split frustra
    385             ShadowUtil.updateShadowCamera(occluders, receivers, shadowCam, points, splitOccluders);
    386 
    387             //saving light view projection matrix for this split
    388             lightViewProjectionsMatrices[i] = shadowCam.getViewProjectionMatrix().clone();
    389             renderManager.setCamera(shadowCam, false);
    390 
    391             r.setFrameBuffer(shadowFB[i]);
    392             r.clearBuffers(false, true, false);
    393 
    394             // render shadow casters to shadow map
    395             viewPort.getQueue().renderShadowQueue(splitOccluders, renderManager, shadowCam, true);
    396         }
    397         if (flushQueues) {
    398             occluders.clear();
    399         }
    400         //restore setting for future rendering
    401         r.setFrameBuffer(viewPort.getOutputFrameBuffer());
    402         renderManager.setForcedMaterial(null);
    403         renderManager.setForcedTechnique(null);
    404         renderManager.setCamera(viewCam, false);
    405 
    406     }
    407 
    408     //debug only : displays depth shadow maps
    409     private void displayShadowMap(Renderer r) {
    410         Camera cam = viewPort.getCamera();
    411         renderManager.setCamera(cam, true);
    412         int h = cam.getHeight();
    413         for (int i = 0; i < dispPic.length; i++) {
    414             dispPic[i].setPosition(64 * (i + 1) + 128 * i, h / 20f);
    415             dispPic[i].setWidth(128);
    416             dispPic[i].setHeight(128);
    417             dispPic[i].updateGeometricState();
    418             renderManager.renderGeometry(dispPic[i]);
    419         }
    420         renderManager.setCamera(cam, false);
    421     }
    422 
    423     /**For dubuging purpose
    424      * Allow to "snapshot" the current frustrum to the scene
    425      */
    426     public void displayDebug() {
    427         debug = true;
    428     }
    429 
    430     public void postFrame(FrameBuffer out) {
    431         Camera cam = viewPort.getCamera();
    432         if (!noOccluders) {
    433             postshadowMat.setColor("Splits", splits);
    434             for (int i = 0; i < nbSplits; i++) {
    435                 postshadowMat.setMatrix4("LightViewProjectionMatrix" + i, lightViewProjectionsMatrices[i]);
    436             }
    437             renderManager.setForcedMaterial(postshadowMat);
    438 
    439             viewPort.getQueue().renderShadowQueue(ShadowMode.Receive, renderManager, cam, flushQueues);
    440 
    441             renderManager.setForcedMaterial(null);
    442             renderManager.setCamera(cam, false);
    443 
    444         }
    445         if (debug) {
    446             displayShadowMap(renderManager.getRenderer());
    447         }
    448     }
    449 
    450     public void preFrame(float tpf) {
    451     }
    452 
    453     public void cleanup() {
    454     }
    455 
    456     public void reshape(ViewPort vp, int w, int h) {
    457     }
    458 
    459     /**
    460      * returns the labda parameter<br>
    461      * see {@link setLambda(float lambda)}
    462      * @return lambda
    463      */
    464     public float getLambda() {
    465         return lambda;
    466     }
    467 
    468     /*
    469      * Adjust the repartition of the different shadow maps in the shadow extend
    470      * usualy goes from 0.0 to 1.0
    471      * 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
    472      * a high value give a more logarithmic repartition resulting in a high quality for near shadows, but the quality quickly decrease over the extend.
    473      * the default value is set to 0.65f (theoric optimal value).
    474      * @param lambda the lambda value.
    475      */
    476     public void setLambda(float lambda) {
    477         this.lambda = lambda;
    478     }
    479 
    480     /**
    481      * How far the shadows are rendered in the view
    482      * see {@link setShadowZExtend(float zFar)}
    483      * @return shadowZExtend
    484      */
    485     public float getShadowZExtend() {
    486         return zFarOverride;
    487     }
    488 
    489     /**
    490      * Set the distance from the eye where the shadows will be rendered
    491      * default value is dynamicaly computed to the shadow casters/receivers union bound zFar, capped to view frustum far value.
    492      * @param zFar the zFar values that override the computed one
    493      */
    494     public void setShadowZExtend(float zFar) {
    495         this.zFarOverride = zFar;
    496     }
    497 
    498     /**
    499      * returns the shdaow intensity<br>
    500      * see {@link setShadowIntensity(float shadowIntensity)}
    501      * @return shadowIntensity
    502      */
    503     public float getShadowIntensity() {
    504         return shadowIntensity;
    505     }
    506 
    507     /**
    508      * Set the shadowIntensity, the value should be between 0 and 1,
    509      * a 0 value gives a bright and invisilble shadow,
    510      * a 1 value gives a pitch black shadow,
    511      * default is 0.7
    512      * @param shadowIntensity the darkness of the shadow
    513      */
    514     public void setShadowIntensity(float shadowIntensity) {
    515         this.shadowIntensity = shadowIntensity;
    516         postshadowMat.setFloat("ShadowIntensity", shadowIntensity);
    517     }
    518 
    519     /**
    520      * returns the edges thickness <br>
    521      * see {@link setEdgesThickness(int edgesThickness)}
    522      * @return edgesThickness
    523      */
    524     public int getEdgesThickness() {
    525         return (int) (edgesThickness * 10);
    526     }
    527 
    528     /**
    529      * Sets the shadow edges thickness. default is 1, setting it to lower values can help to reduce the jagged effect of the shadow edges
    530      * @param edgesThickness
    531      */
    532     public void setEdgesThickness(int edgesThickness) {
    533         this.edgesThickness = Math.max(1, Math.min(edgesThickness, 10));
    534         this.edgesThickness *= 0.1f;
    535         postshadowMat.setFloat("PCFEdge", edgesThickness);
    536     }
    537 
    538     /**
    539      * returns true if the PssmRenderer flushed the shadow queues
    540      * @return flushQueues
    541      */
    542     public boolean isFlushQueues() {
    543         return flushQueues;
    544     }
    545 
    546     /**
    547      * Set this to false if you want to use several PssmRederers to have multiple shadows cast by multiple light sources.
    548      * Make sure the last PssmRenderer in the stack DO flush the queues, but not the others
    549      * @param flushQueues
    550      */
    551     public void setFlushQueues(boolean flushQueues) {
    552         this.flushQueues = flushQueues;
    553     }
    554 }
    555