Parallel Split Shadow Mapping Processor


Hi all,
i've been working on a PSSM processor as Momoko_Fan suggested in this post :
http://www.jmonkeyengine.com/forum/index.php?topic=14342.msg102874#msg102874

I've finally come to some results, but i bumped into lots of problems/questions, and i really need some insight.

This is going to be a long post.
For those who are not familiar with the technique here is a full explanation :
http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html

First here are some screens of the renderer so far :

   
The first one is the rendered scene and the second one is a distance view to understand how the splits are computed

I'm using 4 splits here of 1024x1024, the far bound of the frustrum and the Lambda parameter are dynamicaly computed (though there is still something wrong about it).
The technique used is the Partialy accelerated one described in the GPU gem article : Multiple pass for rendering split's lightview depth textures, one shader based pass for rendering the shadow maps

here is the code, mainly inspired from the Basic Shadow renderer but be aware that this is a draft



import com.jme3.material.Material;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.renderer.queue.RenderQueue.ShadowMode;
import com.jme3.renderer.queue.GeometryList;
import com.jme3.asset.AssetManager;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Matrix4f;
import com.jme3.post.SceneProcessor;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
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.Texture2D;
import com.jme3.ui.Picture;

public class PSSMShadowRenderer implements SceneProcessor {

    private int nbSplits = 4;
    private RenderManager renderManager;
    private ViewPort viewPort;
    private FrameBuffer[] shadowFB;
    private Texture2D[] shadowMaps;
    private Camera shadowCam;
    
    private Material preshadowMat;
    private Material postshadowMat;
    private Picture[] dispPic;
    private Matrix4f[] lightViewProjectionsMatrices;

    private float[] splits;
    private boolean noOccluders = false;
    private Vector3f[] points = new Vector3f[8];
    private Vector3f direction = new Vector3f();
    private AssetManager assetManager;

    boolean debug=false;

    public PSSMShadowRenderer(AssetManager manager, int size, int nbSplits) {
        this.nbSplits = 5;
        assetManager = manager;
        shadowFB = new FrameBuffer[nbSplits - 1];
        shadowMaps = new Texture2D[nbSplits - 1];
        dispPic = new Picture[nbSplits - 1];
        lightViewProjectionsMatrices = new Matrix4f[nbSplits - 1];
        splits = new float[nbSplits];

        for (int i = 0; i < shadowFB.length; i++) {

            shadowFB[i] = new FrameBuffer(size, size, 0);
            shadowMaps[i] = new Texture2D(size, size, Format.Depth);
            shadowFB[i].setDepthTexture(shadowMaps[i]);

            //quads for debuging purpose
            dispPic[i] = new Picture("Picture" + i);
            dispPic[i].setTexture(manager, shadowMaps[i], false);
        }
        postshadowMat = new Material(manager, "MatDefs/Shadow/PostShadow.j3md");
        preshadowMat = new Material(manager, "MatDefs/Shadow/PreShadow.j3md");


        shadowCam = new Camera(size, size);
        shadowCam.setParallelProjection(true);

        for (int i = 0; i < points.length; i++) {
            points[i] = new Vector3f();
        }
    }

    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);
        frustumMdl.setMaterial(new Material(assetManager, "Common/MatDefs/Misc/WireColor.j3md"));
        switch(i){
            case 0:frustumMdl.getMaterial().setColor("m_Color", ColorRGBA.Pink);break;
            case 1:frustumMdl.getMaterial().setColor("m_Color", ColorRGBA.Red);break;
            case 2:frustumMdl.getMaterial().setColor("m_Color", ColorRGBA.Green);break;
            case 3:frustumMdl.getMaterial().setColor("m_Color", ColorRGBA.Blue);break;
            default:frustumMdl.getMaterial().setColor("m_Color", ColorRGBA.White);break;
        }
        
        return frustumMdl;
    }

    public void initialize(RenderManager rm, ViewPort vp) {
        renderManager = rm;
        viewPort = vp;
        //viewPort.getCamera().setFrustumPerspective(75f, (float)viewPort.getCamera().getWidth() / viewPort.getCamera().getHeight(), 1f, 1000f);


        System.out.println("top : "+viewPort.getCamera().getFrustumTop());
        System.out.println("right : "+viewPort.getCamera().getFrustumRight());
        System.out.println("bottom : "+viewPort.getCamera().getFrustumBottom());
        System.out.println("left : "+viewPort.getCamera().getFrustumLeft());

        

//        for (int i = 0; i < dispPic.length; i++) {
//            viewPort.attachScene(dispPic[i]);
//        }

    }

    public boolean isInitialized() {
        return viewPort != null;
    }

    public Vector3f getDirection() {
        return direction;
    }

    public void setDirection(Vector3f direction) {
        this.direction.set(direction).normalizeLocal();
    }


    public void postQueue(RenderQueue rq) {
        GeometryList occluders = rq.getShadowQueueContent(ShadowMode.Cast);
        noOccluders=occluders.size() == 0;
        if (noOccluders) return;

        GeometryList recievers = rq.getShadowQueueContent(ShadowMode.Recieve);
        Camera viewCam = viewPort.getCamera();
        
        float zFar =MyShadowUtil.computeZFar(occluders,recievers,viewCam);
        MyShadowUtil.updateFrustumPoints(viewCam, viewCam.getFrustumNear(), zFar,1.0f, points);
      
     //  System.out.println("zFar : "+zFar);
        
        Vector3f frustaCenter = new Vector3f();
        for (Vector3f point : points) {
            frustaCenter.addLocal(point);
        }
        frustaCenter.multLocal(1f / 8f);

        shadowCam.setDirection(direction);
        shadowCam.update();
        shadowCam.setLocation(frustaCenter);
        shadowCam.update();
        shadowCam.updateViewProjection();

        float l=Math.min(zFar/1000,0.85f);

        MyShadowUtil.updateFrustumSplits(splits, viewCam.getFrustumNear(), zFar,l);
    
        Renderer r = renderManager.getRenderer();
      
        for (int i = 0; i < nbSplits - 1; i++) {
            // update frustum points based on current camera and split
           MyShadowUtil.updateFrustumPoints(viewCam,splits[i],splits[i+ 1],1.0f,points);

           //Updating shadow cam with curent slip frustra
           MyShadowUtil.updateShadowCameraSceneIndependent(occluders, recievers, shadowCam, points);

           //displaying the current splitted frustrum and the associated croped light frustrums in wireframe.
            if(debug){
               viewPort.attachScene(createFrustum(points,i));
                Vector3f[] pts=new Vector3f[8];
                for (int j = 0; j < pts.length; j++) {
                  pts[j] = new Vector3f();
                }
                MyShadowUtil.updateFrustumPoints2(shadowCam, shadowCam.getFrustumNear(), shadowCam.getFrustumFar(), 1.0f, pts);
                viewPort.attachScene(createFrustum(pts,i));
                if(i=:3) debug=false;
            }
            
            //saving light view projection matrix for this split
            lightViewProjectionsMatrices[i] = shadowCam.getViewProjectionMatrix().clone();

            renderManager.setCamera(shadowCam, false);
            renderManager.setForcedMaterial(preshadowMat);
            r.setFrameBuffer(shadowFB[i]);
            r.clearBuffers(false, true, false);

            // render shadow casters to shadow map
            //viewPort.getQueue().renderShadowQueue(ShadowMode.Cast, renderManager, shadowCam);
            //Simple render to avoid the list.clear of renderShadowQueue
            renderManager.renderGeometryList(occluders);
        }
        //do the list.clear
        viewPort.getQueue().getShadowQueueContent(ShadowMode.Cast).clear();

        //restore setting for future rendering
        r.setFrameBuffer(viewPort.getOutputFrameBuffer());
        renderManager.setForcedMaterial(null);
        renderManager.setCamera(viewCam, false);


    }

    public 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);

    }

    public void displayDebug(){
        debug=true;
    }


    public void postFrame(FrameBuffer out) {
        Camera cam = viewPort.getCamera();
        if (!noOccluders) {
            postshadowMat.setMatrix4("m_LightViewProjectionMatrix0", lightViewProjectionsMatrices[0]);
            postshadowMat.setMatrix4("m_LightViewProjectionMatrix1", lightViewProjectionsMatrices[1]);
            postshadowMat.setMatrix4("m_LightViewProjectionMatrix2", lightViewProjectionsMatrices[2]);
            postshadowMat.setMatrix4("m_LightViewProjectionMatrix3", lightViewProjectionsMatrices[3]);
            postshadowMat.setTexture("m_ShadowMap0", shadowMaps[0]);
            postshadowMat.setTexture("m_ShadowMap1",shadowMaps[1]);
            postshadowMat.setTexture("m_ShadowMap2", shadowMaps[2]);
            postshadowMat.setTexture("m_ShadowMap3", shadowMaps[3]);

            postshadowMat.setVector3("far_d", new Vector3f(splits[1], splits[2], splits[3]));

            renderManager.setForcedMaterial(postshadowMat);
            viewPort.getQueue().renderShadowQueue(ShadowMode.Recieve, renderManager, cam);
    
            renderManager.setForcedMaterial(null);
        }

        displayShadowMap(renderManager.getRenderer());
     }

    public void preFrame(float tpf) {
    }

    public void cleanup() {
    }

    public void reshape(ViewPort vp, int w, int h) {

    }


}

I had to implement a MyShadowUtil which is the same as ShadowUtil with some bug fixes



import com.jme3.bounding.BoundingBox;
import com.jme3.bounding.BoundingVolume;
import com.jme3.bullet.nodes.PhysicsNode;
import com.jme3.math.FastMath;
import com.jme3.math.Matrix4f;
import com.jme3.math.Transform;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.queue.GeometryList;

import com.jme3.scene.Geometry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static java.lang.Math.*;

/**
 * Includes various useful shadow mapping functions.
 *
 * See:
 * http://appsrv.cse.cuhk.edu.hk/~fzhang/pssm_vrcia/
 * http://http.developer.nvidia.com/GPUGems3/gpugems3_ch10.html
 * for more info.
 */
public class MyShadowUtil {

    public static void main(String[] args){
        float[] splits = new float[4];
        updateFrustumSplits(splits, 1, 1000, 0.5f);
        System.out.println(Arrays.toString(splits));
    }

    /**
     * Updates the frustum splits stores in <code>splits</code> using PSSM.
     */
    public static void updateFrustumSplits(float[] splits, float near, float far, float lambda){
        for(int i = 0; i < splits.length; i++){
            float IDM = i / (float)splits.length;
            float log = near * FastMath.pow((far / near), IDM);
            float uniform = near + (far - near) * IDM;
            splits[i] = log * lambda + uniform * (1.0f - lambda);
        }

        // This is used to improve the correctness of the calculations. Our main near- and farplane
        // of the camera always stay the same, no matter what happens.
        splits[0] = near;
        splits[splits.length-1] = far;
    }

    public static void updateFrustumPoints2(Camera viewCam,
                                     float nearOverride,
                                     float farOverride,
                                     float scale,
                                     Vector3f[] points){
        int w = viewCam.getWidth();
        int h = viewCam.getHeight();
        float n = viewCam.getFrustumNear();
        float f = viewCam.getFrustumFar();

        points[0].set(viewCam.getWorldCoordinates(new Vector2f(0, 0), n));
        points[1].set(viewCam.getWorldCoordinates(new Vector2f(0, h), n));
        points[2].set(viewCam.getWorldCoordinates(new Vector2f(w, h), n));
        points[3].set(viewCam.getWorldCoordinates(new Vector2f(w, 0), n));

        points[4].set(viewCam.getWorldCoordinates(new Vector2f(0, 0), f));
        points[5].set(viewCam.getWorldCoordinates(new Vector2f(0, h), f));
        points[6].set(viewCam.getWorldCoordinates(new Vector2f(w, h), f));
        points[7].set(viewCam.getWorldCoordinates(new Vector2f(w, 0), f));
    }

    /**
     * Updates the points array to contain the frustum corners of the given
     * camera. The nearOverride and farOverride variables can be used
     * to override the camera's near/far values with own values.
     *
     * TODO: Reduce creation of new vectors
     *
     * @param viewCam
     * @param nearOverride
     * @param farOverride
     */
    public static void updateFrustumPoints(Camera viewCam,
                                     float nearOverride,
                                     float farOverride,
                                     float scale,
                                     Vector3f[] points){
        Vector3f pos = viewCam.getLocation();
        Vector3f dir = viewCam.getDirection();
        Vector3f up = viewCam.getUp();
        float depthHeightRatio=viewCam.getFrustumTop()/viewCam.getFrustumNear();
        float near = nearOverride;
        float far = farOverride;
        float ftop = viewCam.getFrustumTop();
        float fright = viewCam.getFrustumRight();
        float ratio = fright / ftop;

        float near_height;
        float near_width;
        float far_height;
        float far_width;

        if (viewCam.isParallelProjection()){
            near_height = ftop;
            near_width = near_height * ratio;
            far_height = ftop;
            far_width = far_height * ratio;
        }else{
            near_height = depthHeightRatio*near;
            near_width = near_height * ratio;
            far_height = depthHeightRatio * far;
            far_width = far_height * ratio;
        }

        Vector3f right = dir.cross(up).normalizeLocal();

        Vector3f temp = new Vector3f();
        temp.set(dir).multLocal(far).addLocal(pos);
        Vector3f farCenter = temp.clone();
        temp.set(dir).multLocal(near).addLocal(pos);
        Vector3f nearCenter = temp.clone();

        Vector3f nearUp = temp.set(up).multLocal(near_height).clone();
        Vector3f farUp = temp.set(up).multLocal(far_height).clone();
        Vector3f nearRight = temp.set(right).multLocal(near_width).clone();
        Vector3f farRight = temp.set(right).multLocal(far_width).clone();

        points[0].set(nearCenter).subtractLocal(nearUp).subtractLocal(nearRight);
        points[1].set(nearCenter).addLocal(nearUp).subtractLocal(nearRight);
        points[2].set(nearCenter).addLocal(nearUp).addLocal(nearRight);
        points[3].set(nearCenter).subtractLocal(nearUp).addLocal(nearRight);

        points[4].set(farCenter).subtractLocal(farUp).subtractLocal(farRight);
        points[5].set(farCenter).addLocal(farUp).subtractLocal(farRight);
        points[6].set(farCenter).addLocal(farUp).addLocal(farRight);
        points[7].set(farCenter).subtractLocal(farUp).addLocal(farRight);

        if (scale != 1.0f){
            // find center of frustum
            Vector3f center = new Vector3f();
            for (Vector3f pt : points){
                center.addLocal(pt);
            }
            center.divideLocal(8f);

            Vector3f cDir = new Vector3f();
            for (Vector3f pt : points){
                cDir.set(pt).subtractLocal(center);
                cDir.multLocal(scale - 1.0f);
                pt.addLocal(cDir);
            }
        }      
    }

    public static BoundingBox computeUnionBound(GeometryList list, Transform transform){
        BoundingBox bbox = new BoundingBox();
        for (int i = 0; i < list.size(); i++){
            BoundingVolume vol = list.get(i).getWorldBound();
            BoundingVolume newVol = vol.transform(transform);
            //Nehon : prevent NaN and infinity values to screw the final bounding box
            if(newVol.getCenter().x!=Float.NaN && newVol.getCenter().x!=Float.POSITIVE_INFINITY && newVol.getCenter().x!=Float.NEGATIVE_INFINITY){
                bbox.mergeLocal(newVol);
            }
        }
        return bbox;
    }

    public static BoundingBox computeUnionBound(GeometryList list, Matrix4f mat){
        BoundingBox bbox = new BoundingBox();
        BoundingVolume store = null;
        for (int i = 0; i < list.size(); i++){
            BoundingVolume vol = list.get(i).getWorldBound();
            store = vol.clone().transform(mat, null);
            //Nehon : prevent NaN and infinity values to screw the final bounding box
            if(store.getCenter().x!=Float.NaN && store.getCenter().x!=Float.POSITIVE_INFINITY && store.getCenter().x!=Float.NEGATIVE_INFINITY){
                bbox.mergeLocal(store);
            }
        }        
        return bbox;
    }

    public static BoundingBox computeUnionBound(List<BoundingVolume> bv){
        BoundingBox bbox = new BoundingBox();
        for (int i = 0; i < bv.size(); i++){
            BoundingVolume vol = bv.get(i);
            bbox.mergeLocal(vol);
        }
        return bbox;
    }

    public static BoundingBox computeBoundForPoints(Vector3f[] pts, Transform transform){
        Vector3f min = new Vector3f(Vector3f.POSITIVE_INFINITY);
        Vector3f max = new Vector3f(Vector3f.NEGATIVE_INFINITY);
        Vector3f temp = new Vector3f();
        for (int i = 0; i < pts.length; i++){
            transform.transformVector(pts[i], temp);
            
            min.minLocal(temp);
            max.maxLocal(temp);
        }
        Vector3f center = min.add(max).multLocal(0.5f);
        Vector3f extent = max.subtract(min).multLocal(0.5f);
        return new BoundingBox(center, extent.x, extent.y, extent.z);
    }

    public static BoundingBox computeBoundForPoints(Vector3f[] pts, Matrix4f mat){
        Vector3f min = new Vector3f(Vector3f.POSITIVE_INFINITY);
        Vector3f max = new Vector3f(Vector3f.NEGATIVE_INFINITY);
        Vector3f temp = new Vector3f();

        for (int i = 0; i < pts.length; i++){
            float w = mat.multProj(pts[i], temp);
      
            temp.x /= w;
            temp.y /= w;

            min.minLocal(temp);
            max.maxLocal(temp);

        }

        Vector3f center = min.add(max).multLocal(0.5f);
        Vector3f extent = max.subtract(min).multLocal(0.5f);
        return new BoundingBox(center, extent.x, extent.y, extent.z);
    }

    /**
     * Updates the shadow camera to properly contain the given
     * points (which contain the eye camera frustum corners)
     *
     * @param occluders
     * @param lightCam
     * @param points
     */
    //Nehon : No Occluder bounding computed just croping to the frusta boundings
    public static void updateShadowCameraSceneIndependent(GeometryList occluders,
                                          GeometryList recievers,
                                          Camera shadowCam,
                                          Vector3f[] points){

        boolean ortho = shadowCam.isParallelProjection();

        shadowCam.setProjectionMatrix(null);

        if (ortho){
            shadowCam.setFrustum(-1, 1, -1, 1, 1, -1);
        }else{
            shadowCam.setFrustumPerspective(45, 1, 1, 150);
        }

        Matrix4f viewProjMatrix = shadowCam.getViewProjectionMatrix();


        BoundingBox splitBB    = computeBoundForPoints(points, viewProjMatrix);

        Vector3f splitMin = splitBB.getMin(null);
        Vector3f splitMax = splitBB.getMax(null);

  
    //    splitMin.z = 0;

//        if (!ortho)
//            shadowCam.setFrustumPerspective(45, 1, 1, splitMax.z);

        Matrix4f projMatrix = shadowCam.getProjectionMatrix();

        // Create the crop matrix.
        float scaleX, scaleY, scaleZ;
        float offsetX, offsetY, offsetZ;


         scaleX = 2.0f / (splitMax.x - splitMin.x);
        scaleY = 2.0f / (splitMax.y - splitMin.y);
          offsetX = -0.5f * (splitMax.x + splitMin.x) * scaleX;
        offsetY = -0.5f * (splitMax.y + splitMin.y) * scaleY;
        scaleZ = 1.0f / (splitMax.z - splitMin.z);
          offsetZ = -splitMin.z * scaleZ;



        Matrix4f cropMatrix = new Matrix4f(scaleX,  0f,      0f,      offsetX,
                                           0f,      scaleY,  0f,      offsetY,
                                           0f,      0f,      scaleZ,  offsetZ,
                                           0f,      0f,      0f,      1f);


        Matrix4f result = new Matrix4f();
        result.set(cropMatrix);
        result.multLocal(projMatrix);

        shadowCam.setProjectionMatrix(result);

    }


    /**
     * Updates the shadow camera to properly contain the given
     * points (which contain the eye camera frustum corners) and the
     * shadow occluder objects.
     *
     * @param occluders
     * @param lightCam
     * @param points
     */
    public static void updateShadowCamera(GeometryList occluders,
                                          GeometryList recievers,
                                          Camera shadowCam,
                                          Vector3f[] points){

        boolean ortho = shadowCam.isParallelProjection();

        shadowCam.setProjectionMatrix(null);

        if (ortho){
            shadowCam.setFrustum(-1, 1, -1, 1, 1, -1);
        }else{
            shadowCam.setFrustumPerspective(45, 1, 1, 150);
        }

        Matrix4f viewProjMatrix = shadowCam.getViewProjectionMatrix();


        BoundingBox splitBB    = computeBoundForPoints(points, viewProjMatrix);

        ArrayList<BoundingVolume> visRecvList = new ArrayList<BoundingVolume>();
        for (int i = 0; i < recievers.size(); i++){
            // convert bounding box to light's viewproj space
            Geometry reciever = recievers.get(i);
            BoundingVolume bv = reciever.getWorldBound();
            BoundingVolume recvBox = bv.transform(viewProjMatrix, null);

            if (splitBB.intersects(recvBox)){
                visRecvList.add(recvBox);
            }
        }

        ArrayList<BoundingVolume> visOccList = new ArrayList<BoundingVolume>();
        for (int i = 0; i < occluders.size(); i++){
            // convert bounding box to light's viewproj space
            Geometry occluder = occluders.get(i);
            BoundingVolume bv = occluder.getWorldBound();
            BoundingVolume occBox = bv.transform(viewProjMatrix, null);

            if (splitBB.intersects(occBox)){
                visOccList.add(occBox);
            }

        }
     // System.out.println("occluders : "+visOccList.size());
        BoundingBox casterBB   = computeUnionBound(visOccList);
        BoundingBox recieverBB = computeUnionBound(visRecvList);

        Vector3f casterMin = casterBB.getMin(null);
        Vector3f casterMax = casterBB.getMax(null);

        Vector3f recieverMin = recieverBB.getMin(null);
        Vector3f recieverMax = recieverBB.getMax(null);

        Vector3f splitMin = splitBB.getMin(null);
        Vector3f splitMax = splitBB.getMax(null);


           splitMin.z = 0;

//        if (!ortho)
//            shadowCam.setFrustumPerspective(45, 1, 1, splitMax.z);

        Matrix4f projMatrix = shadowCam.getProjectionMatrix();

        Vector3f cropMin = new Vector3f();
        Vector3f cropMax = new Vector3f();

        // IMPORTANT: Special handling for Z values
        cropMin.x = max(max(casterMin.x, recieverMin.x), splitMin.x);
        cropMax.x = min(min(casterMax.x, recieverMax.x), splitMax.x);

        cropMin.y = max(max(casterMin.y, recieverMin.y), splitMin.y);
        cropMax.y = min(min(casterMax.y, recieverMax.y), splitMax.y);

        cropMin.z = min(casterMin.z, splitMin.z);
        cropMax.z = min(recieverMax.z, splitMax.z);



        // Create the crop matrix.
        float scaleX, scaleY, scaleZ;
        float offsetX, offsetY, offsetZ;

        scaleX = (2.0f) / (cropMax.x - cropMin.x);
        scaleY = (2.0f) / (cropMax.y - cropMin.y);

        offsetX = -0.5f * (cropMax.x + cropMin.x) * scaleX;
        offsetY = -0.5f * (cropMax.y + cropMin.y) * scaleY;

        scaleZ = 1.0f / (cropMax.z - cropMin.z);
        offsetZ = -cropMin.z * scaleZ;


        Matrix4f cropMatrix = new Matrix4f(scaleX,  0f,      0f,      offsetX,
                                           0f,      scaleY,  0f,      offsetY,
                                           0f,      0f,      scaleZ,  offsetZ,
                                           0f,      0f,      0f,      1f);


        Matrix4f result = new Matrix4f();
        result.set(cropMatrix);
        result.multLocal(projMatrix);

        shadowCam.setProjectionMatrix(result);

    }


    /*
     * Nehon : Compute the Zfar in the model vieuw to adjust the Zfar distance for the splits calculation
     */
    public static float computeZFar(GeometryList occ,GeometryList recv, Camera cam) {
        Matrix4f mat= cam.getViewMatrix();
        BoundingBox bbOcc=MyShadowUtil.computeUnionBound(occ,mat);
        BoundingBox bbRecv=MyShadowUtil.computeUnionBound(recv,mat);
      
//        System.out.println("zfar : "+(bbOcc.getZExtent()-bbOcc.getCenter().z)+" "+(bbRecv.getZExtent()-bbRecv.getCenter().z));
        return min(max(bbOcc.getZExtent()-bbOcc.getCenter().z,bbRecv.getZExtent()-bbRecv.getCenter().z),cam.getFrustumFar());

    }


}

Bugs i've found were :

  • in updateFrustumPoints, frustrum calculation was assuming the near plane of the frustrum was at 1.0f
  • the computeUnionBound returned some NaN values or Infinite values for the bounding that was screwing things up (i don't really know why…maybe some problems with the Geometry objects of the scene)
  • I didn't manage to make the updateShadowCamera work correctly because it always failed to crop the matrix correctly around the occluders/recievers boundings (except for the first frustra). So i created a updateShadowCameraSceneIndependent as explained in the GPU gem article.





    For the shader part here is the code (preshadow shaders are the same as for the BasicRenderer)

    postShadow.vert


uniform mat4 m_LightViewProjectionMatrix0;
uniform mat4 m_LightViewProjectionMatrix1;
uniform mat4 m_LightViewProjectionMatrix2;
uniform mat4 m_LightViewProjectionMatrix3;

uniform mat4 g_WorldViewProjectionMatrix;

uniform mat4 g_WorldMatrix;

varying vec4 projCoord0;
varying vec4 projCoord1;
varying vec4 projCoord2;
varying vec4 projCoord3;

varying float shadowPosition;

attribute vec3 inPosition;

const mat4 biasMat = mat4(0.5, 0.0, 0.0, 0.0,
                          0.0, 0.5, 0.0, 0.0,
                          0.0, 0.0, 0.5, 0.0,
                          0.5, 0.5, 0.5, 1.0);

void main(){
    gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
    shadowPosition=gl_Position.z;
    // get the vertex in world space
    vec4 worldPos = g_WorldMatrix * vec4(inPosition, 1.0);

    // convert vertex to light viewProj space
    projCoord0 = biasMat * (m_LightViewProjectionMatrix0 * worldPos);
    projCoord1 = biasMat * (m_LightViewProjectionMatrix1 * worldPos);
    projCoord2 = biasMat * (m_LightViewProjectionMatrix2 * worldPos);
    projCoord3 = biasMat * (m_LightViewProjectionMatrix3 * worldPos);
   // vec4 coord = m_LightViewProjectionMatrix * worldPos;
  //  projCoord = biasMat * coord;

  
    //projCoord.z /= gl_DepthRange.far;
    //projCoord = (m_LightViewProjectionMatrix * worldPos);
    //projCoord /= projCoord.w;
    //projCoord.xy = projCoord.xy * vec2(0.5, -0.5) + vec2(0.5);

    // bias from [-1, 1] to [0, 1] for sampling shadow map
    //projCoord = (projCoord.xyzw * vec4(0.5)) + vec4(0.5);
}



postShadow.frag


#import "MatDefs/Shadow/Shadow.glsllib"

uniform SHADOWMAP m_ShadowMap0;
uniform SHADOWMAP m_ShadowMap1;
uniform SHADOWMAP m_ShadowMap2;
uniform SHADOWMAP m_ShadowMap3;

uniform vec3 far_d;

varying vec4 projCoord0;
varying vec4 projCoord1;
varying vec4 projCoord2;
varying vec4 projCoord3;

varying float shadowPosition;

float getShadow(in SHADOWMAP tex, in vec4 pProjCoord){
    vec4 coord = pProjCoord;
    coord.xyz /= coord.w;
    return  Shadow_GetShadow(tex, coord);
}

void main() {
 
    float shad=1.0;
    vec4 color=vec4(0.8,0.8,0.8,1.0);

    // find the appropriate depth map to look up in
    // based on the depth of this fragment
    if(shadowPosition < far_d.x){
       shad = getShadow(m_ShadowMap0, projCoord0);
       color=vec4(1.0,1.0,1.0,1.0);
    }else if(shadowPosition < far_d.y){
       shad = getShadow(m_ShadowMap1, projCoord1);
        color.x=1.0;
    }else if(shadowPosition < far_d.z){
       shad = getShadow(m_ShadowMap2, projCoord2);
        color.y=1.0;
    }else{
       shad = getShadow(m_ShadowMap3, projCoord3);
        color.z=1.0;
    }
    shad=shad*0.7+0.3;

 //  if (shad > 0.7)
  //   shad = 1.0;
  //  if (shad <= 0.7)
  //    shad = 0.3;
   //shad = 1.0 - shad;

   gl_FragColor = vec4(shad,shad,shad,1.0);
  
}



and the j3md


MaterialDef Post Shadow {

    MaterialParameters {
        Texture2D m_ShadowMap0
        Texture2D m_ShadowMap1
        Texture2D m_ShadowMap2
        Texture2D m_ShadowMap3

        Vector3 far_d

        Matrix4 m_LightViewProjectionMatrix0
        Matrix4 m_LightViewProjectionMatrix1
        Matrix4 m_LightViewProjectionMatrix2
        Matrix4 m_LightViewProjectionMatrix3
    }

    Technique {
        VertexShader GLSL100:   MatDefs/Shadow/PostShadow.vert
        FragmentShader GLSL100: MatDefs/Shadow/PostShadow.frag

        WorldParameters {
            WorldViewProjectionMatrix
            WorldMatrix
        }

        Defines {
            NO_SHADOW2DPROJ
        }

        RenderState {
            Blend Modulate
        }
    }

}



I changed a bit the Shadows.glslib to use the PCF_2x2 method instead of the Dither_2x2 because it was producing better results IMHO. (by the way isn't that a 4x4 PCF?)

So here are the questions :
- As you can see, in the shaders i didn't managed to pass arrays for the light view projection matrices and the shadow maps because it seems to be a opengl3.0 capability only. Resulting in the pretty ugly uniform declaration.
Is there a way doing this more dynamicaly/properly and allow dynamic change of the number of splits?
Maybe there is a way of making different implementation based on the system current opengl version?

- The max distance of the frustrum and the lambda parameter are difficult to compute. Maybe i should let them accessible to perform tweeking depending on the scene, what do you think?

- The shadows borders looks ugly...especially from maximum zoomed in views, i tried a 4x4 PCF with weighted edge tap smoothing, but it was even uglier... Is there an easy way to fix this? (bluring maybe)

- Is there a way of preventing the multi pass depth textures rendering. GPU gem talks about a technique involving a geometry shader, but i don't even know if OpenGl 3 supports them?


Next, i'm gona try to optimize the code (fix the crop matrix calculation based on occluders boundings), and make some performance benches
Fix some gliches (shadows some times looks insane depending on the position of the camera)
And i hope enhence the renderer based on your answers/ideas  :D

Maybe i'm going to make a jnlp of the demo, if some of you want's to play with it (though that's not really a game...)

thanks for reading this...  ;)

Very, very impressive! Maybe you want to join the team? :smiley:



I was actually also working on a PSSM implementation, it seems you bumped into many of the same problems I did. So what I am going to do is submit both your and mine implementation to SVN and then we can somehow combine them.


The max distance of the frustrum and the lambda parameter are difficult to compute. Maybe i should let them accessible to perform tweeking depending on the scene, what do you think?

I think they should be configurable. You might want to cap the max distance of the frustum so that no shadows are computed beyond a certain distance. The lambda parameter is almost always tweak-able in PSSM implementations, it will allow the user to decide how the shadow maps are being distributed over the frustum. If I recall correctly the "optimal" lambda value was 0.65 so you can use that as default.

The shadows borders looks ugly...especially from maximum zoomed in views, i tried a 4x4 PCF with weighted edge tap smoothing, but it was even uglier... Is there an easy way to fix this? (bluring maybe)

There are some "soft-shadow" solutions, like CSM (Convolution Shadow Map), VSM (Variance Shadow Map), and Jittered shadows.
Although I think even the default dither one looks fine, unless you use really low resolution shadow map.

Is there a way of preventing the multi pass depth textures rendering. GPU gem talks about a technique involving a geometry shader, but i don't even know if OpenGl 3 supports them?

I don't think this is a big issue, rendering 3 FBOs per frame is nothing compared to 100 different shaders per frame, for example.
Although, I do think its possible to do this with MRT and instancing (e.g OpenGL2 only). You look up the light view projection matrix depending on the instance ID, and then transform the position by that. In the pixel shader, you check the instance ID again and write into that particular render target. The one disadvantage of this method is that you cant use depth maps, you have to use the luminance image format and write the depth as grayscale.
Momoko_Fan said:

Very, very impressive! Maybe you want to join the team? :D

Er... i'm flattered but i don't think that i can be as dedicated as you are (my wife's gona kill me if I spend more time in front of my computer :p)
But, i will contribute as much as i can!!!

Momoko_Fan said:

I think they should be configurable. You might want to cap the max distance of the frustum so that no shadows are computed beyond a certain distance. The lambda parameter is almost always tweak-able in PSSM implementations, it will allow the user to decide how the shadow maps are being distributed over the frustum. If I recall correctly the "optimal" lambda value was 0.65 so you can use that as default.

I tried to evaluate the zfar in the viewcam viewmatrix to the last bounding box in the frustrum, but that gives strange results. The shadow quality increase as you approach the ledge of the scene...and decrease suddenly when you turn your back...
The zfar should not vary and definetly be manually tweaked depending on the scene.

Momoko_Fan said:

There are some "soft-shadow" solutions, like CSM (Convolution Shadow Map), VSM (Variance Shadow Map), and Jittered shadows.
Although I think even the default dither one looks fine, unless you use really low resolution shadow map.

I've seen some pretty good result with silhouette maps, but i think i lack some 3D/math background to understand how it really works. Besides i read so much things about shadow mapping this week that things are blending a bit with each others in my mind  :P

Momoko_Fan said:

I don't think this is a big issue, rendering 3 FBOs per frame is nothing compared to 100 different shaders per frame, for example.
Although, I do think its possible to do this with MRT and instancing (e.g OpenGL2 only). You look up the light view projection matrix depending on the instance ID, and then transform the position by that. In the pixel shader, you check the instance ID again and write into that particular render target. The one disadvantage of this method is that you cant use depth maps, you have to use the luminance image format and write the depth as grayscale.

That worth a try, 1 pass is always better than 3 or 4...

On a side note, i was wondering :
when i render the depth textures i use


             renderManager.renderGeometryList(occluders);


Would it be better to render only the occluders of the current split instead of rendering those of the whole scene, or are they culled by the scene graph like with any other render?

Well i will post my progress in this post

Good night

Nehon

Hi all, here is a new version of the PSSM shadow renderer (attached files)


  • Added support for choosing number of shadow maps in the constructor ( 1 to 8 ).
  • Added support for choosing edge filtering technique in the constructor (PCF or DITHER, PCF is default) (via shader defines).
  • Added support for changing the lambda parameter.
  • Added "smart" calculation of the zFar ( zFar= caster/occluders bounding zExtends in the camera frustum capped to the camera frustum far, gives good results especially at close range)
  • Added support for overriding the zFar (if smart calculation failed to be smart ).
  • Added scene dependent cropping for better shadow quality.
  • Added Some javadoc to understand how the hell that works  :stuck_out_tongue:



    There are still some glitches :
  • when turning back to the light source, and camera frustum is aligned with the light frustum, i’ve got some weird banding artifacts



    Banding artifacts and the aligned frustums


  • when the terrain is set to only receive shadows…things go crazy



    Entire areas are in shadows when they shouldn’t, and shadows are “bleeding” on the ground



    I think there is still a problem with cropping



    Next :
  • Fix the glitches
  • Add better edge filtering (looked into jitter but…that's a bit tricky…)
  • Compute the pre shadowmaps in only one pass, for better performances

      In the screenshot scene when all objects are rendered (~80 objects ~18k triangles) i'm at 600-630 FPS without the shadow processor

      With the processor activated with one 1024 map i drop to 270-280, 4 maps 180-190, 8 maps 130-140.



    And here come my questions :
  • I don't get the difference between MRT and multi-sampled FBO.
  • I tried to create a multi-sampled FBO and i noticed the current FrameBuffer class has support for multiple samples, but only use one.  Do you plan on adding this support?

    Things were going way deep into the renderers implementations so i stopped as i didn't want to break everything.


Oh, very cool!

When the terrain is set to only receive shadows and it bleeds, maybe the issue is in the depth comparison, a higher poly offset might help I am guessing.


I don't get the difference between MRT and multi-sampled FBO.

They are completely different! MRT stands for multiple render targets, meaning, you can have a single shader, in a single pass, draw to multiple textures.
Multi-sampled FBO essentially means using a higher resolution texture/RT and then scaling it down, to remove so called "jaggies", this is also known as anti-aliasing.

Ok so i totally misunderstood what were multi-sampled FBO  :stuck_out_tongue:



What i was talking about is that in the FrameBuffer class there was an ArrayList of RenderBuffer named colorBufs (it seems to have disappeared in latest nightly builds, now it's just a single RenderBuffer). Only one of them was used and the texture was attached to GL_COLOR_ATTACHMENT0 in the renderer implementation (at least the LWJGL one).

To use MRT we need to attach multiple textures to an FBO and attach them properly to the gl renderer.



What are the plans for this feature?

Would be great to have it, also i would understand that it is not a priority.

Here is a new version of my PSSMShadowProcessor

I fixed the banding/bleeding glitches and made the processor last nightly compliant



I also added a shadow intensity variable that can attenuate or strengthen shadows, depending on the ambient lighting you want for your scene.

Let’s say a strong shiny weather should give strong shadows, but an overcast weather should give lighter shadows.

The values goes from 0 to 1 and default is 0.7. 0 would give a completely transparent shadow and 1 a pitch black shadow. The value can be changed at runtime so it can also be animated. (ie when a cloud pass before the sun)

here is a sreenshot



top value is 0.3, bottom is 0.9.

Notice that jagged shadow edges are a lot less noticeable when the value is low, it could be a way of tweaking shadow aspect.





If someone want to test it and gives feedback, i’d be glad

Hey look really good now :slight_smile: , I will try to implement/test this in the stuff I already have, and give you feedback, and screens then :slight_smile:

Hopefully this one will be in alpha2, which will be out soon.

Thanks a lot!

Thought I'd play around with it. I try and create it like the BasicShadowRenderer


      PSSMrenderer = new PSSMShadowRenderer(assetManager,512,8);
      PSSMrenderer.setDirection(new Vector3f(-1,-1,-1).normalizeLocal());
      viewPort.addProcessor(PSSMrenderer);



and get the following error:

SEVERE: Uncaught exception thrown in Thread[LWJGL Renderer Thread,5,main]
java.lang.IllegalStateException: Incomplete read buffer.
   at com.jme3.renderer.lwjgl.LwjglRenderer.checkFrameBufferError(LwjglRenderer.java:999)
   at com.jme3.renderer.lwjgl.LwjglRenderer.setFrameBuffer(LwjglRenderer.java:1181)
   at shadowtest.PSSMShadowRenderer.postQueue(PSSMShadowRenderer.java:212)
   at com.jme3.renderer.RenderManager.renderViewPort(RenderManager.java:546)
   at com.jme3.renderer.RenderManager.render(RenderManager.java:564)
   at com.jme3.app.SimpleApplication.update(SimpleApplication.java:189)
   at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:112)
   at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:162)
   at java.lang.Thread.run(Thread.java:637)



I'm sure there's some setup step I'm missing but don't know what.

Thanks,
William

ha strange,

what version of JME3 do you use?

what is your graphic card configuration, and opengl version?



Could you give me a test case so i can reproduce the issue?



Otherwise you got it right this should work.

however 8 maps of 512 will give crappy result, 4 maps of 1024 should be better.



You don't have to normalize the direction though but i don't think that is the problem.

This is the latest svn version of jme3. Card is an ATI 4850. Tried opengl 2, 3, and 3.1



This is a mac though - will switch to pc and test.



The code is just pulled from the TestShadow code, replacing the basic shadow renderer with yours.


        cam.setLocation(new Vector3f(0.7804813f, 1.7502685f, -2.1556435f));
        cam.setRotation(new Quaternion(0.1961598f, -0.7213164f, 0.2266092f, 0.6243975f));
        cam.setFrustumFar(10);

        Material mat = assetManager.loadMaterial("Common/Materials/WhiteColor.j3m");
        rootNode.setShadowMode(ShadowMode.Off);
        Box floor = new Box(Vector3f.ZERO, 3, 0.1f, 3);
        Geometry floorGeom = new Geometry("Floor", floor);
        floorGeom.setMaterial(mat);
        floorGeom.setLocalTranslation(0,-0.2f,0);
        floorGeom.updateModelBound();
        floorGeom.setShadowMode(ShadowMode.Recieve);
        rootNode.attachChild(floorGeom);

        Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.obj");
        teapot.setLocalScale(2f);
        teapot.setMaterial(mat);
        teapot.setShadowMode(ShadowMode.CastAndRecieve);
        rootNode.attachChild(teapot);
      
   PSSMrenderer = new PSSMShadowRenderer(assetManager,1024,4);
   PSSMrenderer.setDirection(new Vector3f(-1,-1,-1));
   viewPort.addProcessor(PSSMrenderer);





EDIT: Switched to the bootcamped pc on this mac and it works. So looks like it's a mac thing.

This one is gonna be tough to test, because i have no Mac.



Maybe someone else could test it with his mac, maybe the problem is with your Mac and not all Mac…

So far have tested on 10.5 and 10.6 macs. Same error. Also tried java 5 and 6 as well as forcing 32bit mode. No help.

Ok so it seems that's a general Mac issue with the processor.



I'm going to look for a solution to test and reproduce it.



Thank you Kirdel for taking the time to test it.

Well, if you need someone to test it at win7 32 with a ati 3750HD I could help you

Have you tried declaring a const array, like:


const vec2 array = { ... };

void main(){
    // ...
}

Yep Kirill i tried (this was for the SSAO issue though :p) the problem is that Apple GLSL 1.2 implementation does not support array initialization constructor. it's a known bug… but never fixed

By the way i work around it by passing the array as a uniform



Empire Phoenix yes, i would be glad if you can help



In the latest SVN you have a testPssmShadows, could you test it and output the log here if it fails?

There is a testSSAO and testSSAO2 too, if you are in the mood of testing



thanks