• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 Rémy 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      */
PssmShadowRenderer(AssetManager manager, int size, int nbSplits)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.
PssmShadowRenderer(AssetManager manager, int size, int nbSplits, Material postShadowMat)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      */
setFilterMode(FilterMode filterMode)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      */
setCompareMode(CompareMode compareMode)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
createFrustum(Vector3f[] pts, int i)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 
initialize(RenderManager rm, ViewPort vp)306     public void initialize(RenderManager rm, ViewPort vp) {
307         renderManager = rm;
308         viewPort = vp;
309     }
310 
isInitialized()311     public boolean isInitialized() {
312         return viewPort != null;
313     }
314 
315     /**
316      * returns the light direction used by the processor
317      * @return
318      */
getDirection()319     public Vector3f getDirection() {
320         return direction;
321     }
322 
323     /**
324      * Sets the light direction to use to compute shadows
325      * @param direction
326      */
setDirection(Vector3f direction)327     public void setDirection(Vector3f direction) {
328         this.direction.set(direction).normalizeLocal();
329     }
330 
331     @SuppressWarnings("fallthrough")
postQueue(RenderQueue rq)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
displayShadowMap(Renderer r)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      */
displayDebug()426     public void displayDebug() {
427         debug = true;
428     }
429 
postFrame(FrameBuffer out)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 
preFrame(float tpf)450     public void preFrame(float tpf) {
451     }
452 
cleanup()453     public void cleanup() {
454     }
455 
reshape(ViewPort vp, int w, int h)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      */
getLambda()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      */
setLambda(float lambda)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      */
getShadowZExtend()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      */
setShadowZExtend(float zFar)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      */
getShadowIntensity()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      */
setShadowIntensity(float shadowIntensity)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      */
getEdgesThickness()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      */
setEdgesThickness(int edgesThickness)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      */
isFlushQueues()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      */
setFlushQueues(boolean flushQueues)551     public void setFlushQueues(boolean flushQueues) {
552         this.flushQueues = flushQueues;
553     }
554 }
555