aboutsummaryrefslogtreecommitdiff
path: root/engine/src/core/com/jme3/shadow/PssmShadowRenderer.java
diff options
context:
space:
mode:
Diffstat (limited to 'engine/src/core/com/jme3/shadow/PssmShadowRenderer.java')
-rw-r--r--engine/src/core/com/jme3/shadow/PssmShadowRenderer.java554
1 files changed, 554 insertions, 0 deletions
diff --git a/engine/src/core/com/jme3/shadow/PssmShadowRenderer.java b/engine/src/core/com/jme3/shadow/PssmShadowRenderer.java
new file mode 100644
index 0000000..9fa95cb
--- /dev/null
+++ b/engine/src/core/com/jme3/shadow/PssmShadowRenderer.java
@@ -0,0 +1,554 @@
+/*
+ * Copyright (c) 2009-2010 jMonkeyEngine All rights reserved.
+ * <p/>
+ * 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.
+ * <p/>
+ * * 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.
+ * <p/>
+ * * 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.
+ * <p/>
+ * 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.Vector3f;
+import com.jme3.post.SceneProcessor;
+import com.jme3.renderer.Camera;
+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.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;
+
+/**
+ * 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
+ */
+public class PssmShadowRenderer implements SceneProcessor {
+
+ /**
+ * <code>FilterMode</code> specifies how shadows are filtered
+ */
+ 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
+ */
+ PCF8
+ }
+
+ /**
+ * Specifies the shadow comparison mode
+ */
+ 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;
+ }
+ private int nbSplits = 3;
+ private float lambda = 0.65f;
+ private float shadowIntensity = 0.7f;
+ private float zFarOverride = 0;
+ private RenderManager renderManager;
+ private ViewPort viewPort;
+ private FrameBuffer[] shadowFB;
+ private Texture2D[] shadowMaps;
+ private Texture2D dummyTex;
+ private Camera shadowCam;
+ private Material preshadowMat;
+ private Material postshadowMat;
+ private GeometryList splitOccluders = new GeometryList(new OpaqueComparator());
+ private Matrix4f[] lightViewProjectionsMatrices;
+ private ColorRGBA splits;
+ private float[] splitsArray;
+ private boolean noOccluders = false;
+ private Vector3f direction = new Vector3f();
+ private AssetManager assetManager;
+ private boolean debug = false;
+ private float edgesThickness = 1.0f;
+ private FilterMode filterMode;
+ private CompareMode compareMode;
+ private Picture[] dispPic;
+ private Vector3f[] points = new Vector3f[8];
+ private boolean flushQueues = true;
+
+ /**
+ * 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/PostShadowPSSM.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 *
+ */
+ //TODO remove the postShadowMat when we have shader injection....or remove this todo if we are in 2020.
+ public PssmShadowRenderer(AssetManager manager, int size, int nbSplits, Material postShadowMat) {
+ assetManager = manager;
+ nbSplits = Math.max(Math.min(nbSplits, 4), 1);
+ this.nbSplits = nbSplits;
+
+ 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");
+ this.postshadowMat = postShadowMat;
+
+ 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
+ */
+ 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);
+ }
+ }
+ }
+ }
+
+ /**
+ * sets the shadow compare mode see {@link CompareMode} for more info
+ * @param compareMode
+ */
+ 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);
+ }
+
+ //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;
+ }
+
+ 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) {
+ GeometryList occluders = rq.getShadowQueueContent(ShadowMode.Cast);
+ if (occluders.size() == 0) {
+ return;
+ }
+
+ GeometryList receivers = rq.getShadowQueueContent(ShadowMode.Receive);
+ if (receivers.size() == 0) {
+ return;
+ }
+
+ 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(occluders, receivers, shadowCam, points, splitOccluders);
+
+ //saving light view projection matrix for this split
+ lightViewProjectionsMatrices[i] = shadowCam.getViewProjectionMatrix().clone();
+ renderManager.setCamera(shadowCam, false);
+
+ r.setFrameBuffer(shadowFB[i]);
+ r.clearBuffers(false, true, false);
+
+ // render shadow casters to shadow map
+ viewPort.getQueue().renderShadowQueue(splitOccluders, renderManager, shadowCam, true);
+ }
+ if (flushQueues) {
+ occluders.clear();
+ }
+ //restore setting for future rendering
+ r.setFrameBuffer(viewPort.getOutputFrameBuffer());
+ renderManager.setForcedMaterial(null);
+ renderManager.setForcedTechnique(null);
+ renderManager.setCamera(viewCam, false);
+
+ }
+
+ //debug only : displays depth shadow maps
+ private 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(64 * (i + 1) + 128 * i, 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) {
+ Camera cam = viewPort.getCamera();
+ if (!noOccluders) {
+ postshadowMat.setColor("Splits", splits);
+ for (int i = 0; i < nbSplits; i++) {
+ postshadowMat.setMatrix4("LightViewProjectionMatrix" + i, lightViewProjectionsMatrices[i]);
+ }
+ renderManager.setForcedMaterial(postshadowMat);
+
+ viewPort.getQueue().renderShadowQueue(ShadowMode.Receive, renderManager, cam, flushQueues);
+
+ renderManager.setForcedMaterial(null);
+ renderManager.setCamera(cam, false);
+
+ }
+ if (debug) {
+ displayShadowMap(renderManager.getRenderer());
+ }
+ }
+
+ public void preFrame(float tpf) {
+ }
+
+ public void cleanup() {
+ }
+
+ public void reshape(ViewPort vp, int w, int h) {
+ }
+
+ /**
+ * returns the labda parameter<br>
+ * see {@link 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 {@link 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) {
+ this.zFarOverride = zFar;
+ }
+
+ /**
+ * returns the shdaow intensity<br>
+ * see {@link 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
+ */
+ public void setShadowIntensity(float shadowIntensity) {
+ this.shadowIntensity = shadowIntensity;
+ postshadowMat.setFloat("ShadowIntensity", shadowIntensity);
+ }
+
+ /**
+ * returns the edges thickness <br>
+ * see {@link 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);
+ }
+
+ /**
+ * 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;
+ }
+}