diff options
Diffstat (limited to 'engine/src/core/com/jme3/shadow/PssmShadowRenderer.java')
-rw-r--r-- | engine/src/core/com/jme3/shadow/PssmShadowRenderer.java | 554 |
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; + } +} |