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 :