blob: 9fa95cb4b7037bb0d68b7db5af5a1043c6597c3c [file] [log] [blame]
/*
* 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;
}
}