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 */
30package com.jme3.shadow;
31
32import com.jme3.asset.AssetManager;
33import com.jme3.material.Material;
34import com.jme3.math.ColorRGBA;
35import com.jme3.math.Matrix4f;
36import com.jme3.math.Vector3f;
37import com.jme3.post.SceneProcessor;
38import com.jme3.renderer.Camera;
39import com.jme3.renderer.RenderManager;
40import com.jme3.renderer.Renderer;
41import com.jme3.renderer.ViewPort;
42import com.jme3.renderer.queue.GeometryList;
43import com.jme3.renderer.queue.OpaqueComparator;
44import com.jme3.renderer.queue.RenderQueue;
45import com.jme3.renderer.queue.RenderQueue.ShadowMode;
46import com.jme3.scene.Geometry;
47import com.jme3.scene.Spatial;
48import com.jme3.scene.debug.WireFrustum;
49import com.jme3.texture.FrameBuffer;
50import com.jme3.texture.Image.Format;
51import com.jme3.texture.Texture.MagFilter;
52import com.jme3.texture.Texture.MinFilter;
53import com.jme3.texture.Texture.ShadowCompareMode;
54import com.jme3.texture.Texture2D;
55import 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 Rémy Bouquet aka Nehon
67 */
68public 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