Soft particle effect for a cloud layer

Hi,
for a game I want to display animated cloud layers that the player passes when he/she climbs up a mountain.
For that I create a large quad with the cloud texture (see below) and animate it. In order to soften the transition when the cloud layer hits the terrain, I use the soft particle effect to blend out the alpha when the clouds get close to the terrain. Since the TranslucentBucketFilter is tailored only for the particle emitter, I had to copy it so that I can access the depth texture in the cloud shader.

That is the result: The transition does not follow the shape of the terrain. Instead, a sharp edge is visible. This sharp edge also changes when I move the camera.


I tested it with both jME3.1-stable and the latest master from 03/09/2017 14:12:55, the result was the same.

What causes these sharp edges? How can I fix it to get a nice, smooth transition?

That is the code to reproduce the effect:

cloudtest/CloudTest.java :

package cloudtest;

import com.jme3.app.SimpleApplication;
import com.jme3.asset.AssetManager;
import com.jme3.light.DirectionalLight;
import com.jme3.material.Material;
import com.jme3.material.RenderState;
import com.jme3.math.*;
import com.jme3.post.Filter;
import com.jme3.post.FilterPostProcessor;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Quad;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.terrain.geomipmap.TerrainLodControl;
import com.jme3.terrain.geomipmap.TerrainQuad;
import com.jme3.terrain.geomipmap.lodcalc.DistanceLodCalculator;
import com.jme3.terrain.heightmap.AbstractHeightMap;
import com.jme3.terrain.heightmap.ImageBasedHeightMap;
import com.jme3.texture.FrameBuffer;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture.WrapMode;
import com.jme3.texture.Texture2D;

/**
 * This tests a custom soft-particle implementation.
 * This time not with particles, but with a larger quad acting as a cloud layer.
 * @author Sebastian Weiss
 */
public class CloudTest extends SimpleApplication {

    //terrain stuff
    private TerrainQuad terrain;
    private Material matRock;
    private Material matWire;
    private float grassScale = 64;
    private float dirtScale = 16;
    private float rockScale = 128;
    
    //filters
    private FilterPostProcessor fpp;
    private DirectionalLightShadowFilter shadowFilter;
    private TranslucentBucketFilter translucentFilter;
    
    //cloud layer
    private Geometry cloudGeom;
    private float cloudHeight;
    private Vector2f cloudScale;
    private Vector2f cloudScroll;
    private Material cloudMat;

    public static void main(String[] args) {
        CloudTest app = new CloudTest();
        app.start();
    }
    
    @Override
    public void simpleInitApp() {
        //Terrain, copied from TerrainTest
        matRock = new Material(assetManager, "Common/MatDefs/Terrain/Terrain.j3md");
        matRock.setBoolean("useTriPlanarMapping", false);
        matRock.setTexture("Alpha", assetManager.loadTexture("Textures/Terrain/splat/alphamap.png"));
        Texture heightMapImage = assetManager.loadTexture("Textures/Terrain/splat/mountains512.png");
        Texture grass = assetManager.loadTexture("Textures/Terrain/splat/grass.jpg");
        grass.setWrap(WrapMode.Repeat);
        matRock.setTexture("Tex1", grass);
        matRock.setFloat("Tex1Scale", grassScale);
        Texture dirt = assetManager.loadTexture("Textures/Terrain/splat/dirt.jpg");
        dirt.setWrap(WrapMode.Repeat);
        matRock.setTexture("Tex2", dirt);
        matRock.setFloat("Tex2Scale", dirtScale);
        Texture rock = assetManager.loadTexture("Textures/Terrain/splat/road.jpg");
        rock.setWrap(WrapMode.Repeat);
        matRock.setTexture("Tex3", rock);
        matRock.setFloat("Tex3Scale", rockScale);
        matWire = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        matWire.getAdditionalRenderState().setWireframe(true);
        matWire.setColor("Color", ColorRGBA.Green);
        AbstractHeightMap heightmap = null;
        try {
            heightmap = new ImageBasedHeightMap(heightMapImage.getImage(), 1f);
            heightmap.load();
        } catch (Exception e) {
            e.printStackTrace();
        }
        terrain = new TerrainQuad("terrain", 65, 513, heightmap.getHeightMap());
        TerrainLodControl control = new TerrainLodControl(terrain, getCamera());
        control.setLodCalculator( new DistanceLodCalculator(65, 2.7f) ); // patch size, and a multiplier
        terrain.addControl(control);
        terrain.setMaterial(matRock);
        terrain.setLocalTranslation(0, -100, 0);
        terrain.setLocalScale(2f, 0.5f, 2f);
        terrain.setShadowMode(RenderQueue.ShadowMode.Receive);
        rootNode.attachChild(terrain);

        //create light
        DirectionalLight light = new DirectionalLight();
        light.setDirection((new Vector3f(-0.5f, -1f, -0.5f)).normalize());
        rootNode.addLight(light);
        
        //create shadow
        fpp = new FilterPostProcessor(assetManager);
        shadowFilter = new DirectionalLightShadowFilter(assetManager, 512, 4);
        shadowFilter.setLight(light);
        fpp.addFilter(shadowFilter);
        viewPort.addProcessor(fpp);
        
        //some random model
        Spatial oto = assetManager.loadModel("Models/Oto/Oto.mesh.xml");
        oto.setLocalTranslation(new Vector3f(-126.15294f, -21.429466f, -146.82469f));
        oto.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);
        rootNode.attachChild(oto);

        //camera (praise the 'c'-command :D )
        cam.setLocation(new Vector3f(-80.31565f, -2.7272663f, -123.51088f));
        cam.setRotation(new Quaternion(-0.1200374f, 0.8401792f, -0.20728625f, -0.48654124f));
        flyCam.setMoveSpeed(50);
        
        //now the main stuff: the cloud layer
        translucentFilter = new TranslucentBucketFilter();
        fpp.addFilter(translucentFilter);
    }

    @Override
    public void simpleUpdate(float tpf) {
        if (fpp.isInitialized() && cloudGeom == null) {
            //delay initialization until here, so that we can get the depth texture
            //from the translucent bucket filter.
            initCloudLayer();
            System.out.println("cloud layer initialized");
        }
    }
    
    
    private void initCloudLayer() {
        //properties
        cloudScale = new Vector2f(10f, 10f);
        cloudScroll = new Vector2f(0, 0);
        cloudHeight = -21;
        //in my game, cloud scroll is animated
        
        //create material
        cloudMat = new Material(assetManager, "cloudtest/SoftLayer.j3md");
        Texture tex = assetManager.loadTexture("cloudtest/Clouds1.png");
        tex.setWrap(Texture.WrapMode.Repeat);
        cloudMat.setTexture("Texture", tex);
        cloudMat.setFloat("Alpha", 0.4f);
        cloudMat.setFloat("Softness", 3);
        cloudMat.setVector2("Scale", cloudScale);
        cloudMat.setVector2("Scroll", cloudScroll);
        cloudMat.getAdditionalRenderState().setFaceCullMode(RenderState.FaceCullMode.Off);
        
        //create geometry
        cloudGeom = new Geometry("cloud", new Quad(1, 1));
        cloudGeom.rotate(FastMath.HALF_PI, 0, 0);
        cloudGeom.setLocalTranslation(-226.15294f, cloudHeight, -246.82469f);
        cloudGeom.setLocalScale(200);
        //in my game, translation and scale are adjusted every frame so that the cloud
        // layer fills the whole screen
        cloudGeom.setMaterial(cloudMat);
        cloudGeom.setQueueBucket(RenderQueue.Bucket.Translucent);
        cloudGeom.setCullHint(Spatial.CullHint.Never);
        cloudGeom.setShadowMode(RenderQueue.ShadowMode.Off);
        rootNode.attachChild(cloudGeom);
        
        //soft effect
        Texture depthTexture = translucentFilter.getDepthTexture();
        cloudMat.setTexture("DepthTexture", depthTexture);
        int samples = translucentFilter.getDepthSamples();
        cloudMat.setInt("NumSamplesDepth", samples);
        if (samples > 1) {
            cloudMat.selectTechnique("SoftLayer15", renderManager);
        } else {
            cloudMat.selectTechnique("SoftLayer", renderManager);
        }
    }
    
    /**
     * Custom translucent bucket filter.
     * It is a copy of jme's translucent bucket filter, that exposes the
     * depth texture
     */
    private final class TranslucentBucketFilter extends Filter {
        
        private RenderManager renderManager;
        private Texture depthTexture;
        private ViewPort viewPort;

        @Override
        protected void initFilter(AssetManager manager, RenderManager rm, ViewPort vp, int w, int h) {
            this.renderManager = rm;
            this.viewPort = vp;
            material = new Material(manager, "Common/MatDefs/Post/Overlay.j3md");
            material.setColor("Color", ColorRGBA.White);
            Texture2D tex = processor.getFilterTexture();
            material.setTexture("Texture", tex);
            if (tex.getImage().getMultiSamples() > 1) {
                material.setInt("NumSamples", tex.getImage().getMultiSamples());
            } else {
                material.clearParam("NumSamples");
            }
            renderManager.setHandleTranslucentBucket(false);
        }

        @Override
        protected void setDepthTexture(Texture depthTexture) {
            this.depthTexture = depthTexture;
            //TODO: maybe call registered translucent effects here
            //      if the depth texture is available only now
        }

        public Texture getDepthTexture() {
            return depthTexture;
        }
        
        public int getDepthSamples() {
            return processor.getNumSamples();
        }
        
        /**
         * Override this method and return false if your Filter does not need
         * the scene texture
         *
         * @return
         */
        @Override
        protected boolean isRequiresSceneTexture() {
            return false;
        }

        @Override
        protected boolean isRequiresDepthTexture() {
            return true;
        }

        @Override
        protected void postFrame(RenderManager renderManager, ViewPort viewPort, FrameBuffer prevFilterBuffer, FrameBuffer sceneBuffer) {
            renderManager.setCamera(viewPort.getCamera(), false);
            if (prevFilterBuffer != sceneBuffer) {
                renderManager.getRenderer().copyFrameBuffer(prevFilterBuffer, sceneBuffer, false);
            }
            renderManager.getRenderer().setFrameBuffer(sceneBuffer);
            viewPort.getQueue().renderQueue(RenderQueue.Bucket.Translucent, renderManager, viewPort.getCamera());
        }

        @Override
        protected void cleanUpFilter(Renderer r) {
            if (renderManager != null) {
                renderManager.setHandleTranslucentBucket(true);
            }
        }

        @Override
        protected Material getMaterial() {
            return material;
        }

        @Override
        public void setEnabled(boolean enabled) {
            super.setEnabled(enabled);
            if (renderManager != null) {
                renderManager.setHandleTranslucentBucket(!enabled);
            }
        }
    }
}

cloudtest/SoftLayer.j3md :

MaterialDef Soft Layer {
    MaterialParameters {
        Texture2D Texture

        Float Alpha
        Vector2 Scale
        Vector2 Scroll

        //soft particles
        Texture2D DepthTexture
        Float Softness
        Int NumSamplesDepth
    }

    Technique SoftLayer {
        VertexShader   GLSL100 : cloudtest/SoftLayer.vert
        FragmentShader GLSL100 : cloudtest/SoftLayer.frag

        WorldParameters {
            WorldViewProjectionMatrix
            WorldViewMatrix
            WorldMatrix
            CameraPosition
        }

        RenderState {
            Blend Alpha
            DepthWrite Off
        }

        Defines {
        }
    }

    Technique SoftLayer15 {
        VertexShader   GLSL100 : cloudtest/SoftLayer.vert
        FragmentShader GLSL150 : cloudtest/SoftLayer15.frag

        WorldParameters {
            WorldViewProjectionMatrix
            WorldViewMatrix
            WorldMatrix
            CameraPosition
        }

        RenderState {
            Blend Alpha
            DepthWrite Off
        }

        Defines {
            RESOLVE_DEPTH_MS : NumSamplesDepth
        }
    }
}

cloudtest/SoftLayer.vert :

uniform mat4 g_WorldViewProjectionMatrix;

attribute vec3 inPosition;
attribute vec4 inTexCoord;

uniform vec2 m_Scale;
uniform vec2 m_Scroll;

// z and w values in projection space
varying vec2 projPos;
varying vec2 vPos; // Position of the pixel in clip space

varying vec2 texCoord;

/*
* vertex shader template
*/
void main() {
    //basic projection
    vec4 pos = vec4(inPosition, 1.0);
    gl_Position = g_WorldViewProjectionMatrix * pos;
    projPos = gl_Position.zw;
    // Transforms the vPosition data to the range [0,1]
    vPos = (gl_Position.xy / gl_Position.w + 1.0) / 2.0;

    //texture coordinates
    texCoord = (inTexCoord.xy * m_Scale) + m_Scroll;
}

cloudtest/SoftLayer.frag :

uniform sampler2D m_DepthTexture;
uniform float m_Softness; // Power used in the contrast function
varying vec2 vPos; // Position of the pixel
varying vec2 projPos;// z and w valus in projection space

uniform sampler2D m_Texture;
uniform float m_Alpha;
varying vec2 texCoord;

float Contrast(float d){
    float val = clamp( 2.0*( (d > 0.5) ? 1.0-d : d ), 0.0, 1.0);
    float a = 0.5 * pow(val, m_Softness);
    return (d > 0.5) ? 1.0 - a : a;
}

float stdDiff(float d){   
    return clamp((d)*m_Softness,0.0,1.0);
}

/*
* fragment shader template
*/
void main() {
    //compute color
    vec4 color = texture2D(m_Texture, texCoord);
    color.a *= m_Alpha;
    if (color.a <= 0.01)
        discard;

    float depthv = texture2D(m_DepthTexture, vPos).x*2.0-1.0; // Scene depth
    depthv*=projPos.y;   
    float particleDepth = projPos.x;
    
    float zdiff =depthv-particleDepth;
    if(zdiff<=0.0){
        discard;
    }
    // Computes alpha based on the particles distance to the rest of the scene
    color.a = color.a * stdDiff(zdiff);// Contrast(zdiff);
    gl_FragColor = color;
}

cloudtest/SoftLayer15.frag :

#import "Common/ShaderLib/MultiSample.glsllib"

uniform DEPTHTEXTURE m_DepthTexture;
uniform float m_Softness; // Power used in the contrast function
varying vec2 vPos; // Position of the pixel
varying vec2 projPos;// z and w valus in projection space

uniform sampler2D m_Texture;
uniform float m_Alpha;
varying vec2 texCoord;

float Contrast(float d){
    float val = clamp( 2.0*( (d > 0.5) ? 1.0-d : d ), 0.0, 1.0);
    float a = 0.5 * pow(val, m_Softness);
    return (d > 0.5) ? 1.0 - a : a;
}

float stdDiff(float d){   
    return clamp((d)*m_Softness,0.0,1.0);
}

/*
* fragment shader template
*/
void main() {
    //compute color
    vec4 color = texture2D(m_Texture, texCoord);
    color.a *= m_Alpha;
    if (color.a <= 0.01)
        discard;

    float depthv = getDepth(m_DepthTexture, vPos).x*2.0-1.0; // Scene depth
    depthv*=projPos.y;   
    float particleDepth = projPos.x;
    
    float zdiff =depthv-particleDepth;
    if(zdiff<=0.0){
        discard;
    }
    // Computes alpha based on the particles distance to the rest of the scene
    color.a = color.a * stdDiff(zdiff);// Contrast(zdiff);
    gl_FragColor = color;
}

and finally the cloud texture cloudtest/Clouds1.png :

2 Likes

It seems as if the computation of the pixel position on screen (the ‘vPos’ varying variable) is screwed up if one vertex leaves the screen. (Strangely, projPos, so the depth, works fine)
A simple fix would be to retrieve the fragment position manually:
Instead of

float depthv = texture2D(m_DepthTexture, vPos).x*2.0-1.0;

the line

float depthv = texelFetch(m_DepthTexture, ivec2(gl_FragCoord.xy), 0).x*2.0-1.0;

fixes the issue, see screenshot below.
This, however, requires a GLSL version of at least 130.
Is there a way around? How can the fragment position on screen be computed correctly without the build-in gl_FragCoord when the vertices of the quad are outside of the screen?

3 Likes