Просмотр исходного кода

Added SoftBloomFilter (#2229)

* added pbr bloom filter

* add javadoc and license

* documented and tweaked test

* added exception

* various formatting fixes

* fixed javadoc typo

* fixed bug on applying glow factor

* fix javadoc typo

* fixed formatting issues

* switched texture min/mag filters

* rename filter

* rename filter

* improved test and capped number of passes

* reformat test

* serialize bilinear filtering

* delete unrelated files

* increase size limit to 2

* renamed shaders
codex 1 год назад
Родитель
Сommit
76d8a43297

+ 337 - 0
jme3-effects/src/main/java/com/jme3/post/filters/SoftBloomFilter.java

@@ -0,0 +1,337 @@
+/*
+ * Copyright (c) 2024 jMonkeyEngine
+ * All rights reserved.
+ *
+ * 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.
+ *
+ * * 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.
+ *
+ * * 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.
+ *
+ * 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.post.filters;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector2f;
+import com.jme3.post.Filter;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.Renderer;
+import com.jme3.renderer.ViewPort;
+import com.jme3.texture.Image;
+import com.jme3.texture.Texture;
+import java.io.IOException;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+import java.util.LinkedList;
+
+/**
+ * Adds a glow effect to the scene.
+ * <p>
+ * Compared to {@link BloomFilter}, this filter produces much higher quality
+ * results that feel much more natural.
+ * <p>
+ * This implementation, unlike BloomFilter, has no brightness threshold,
+ * meaning all aspects of the scene glow, although only very bright areas will
+ * noticeably produce glow. For this reason, this filter should <em>only</em> be used
+ * if HDR is also being utilized, otherwise BloomFilter should be preferred.
+ * <p>
+ * This filter uses the PBR bloom algorithm presented in
+ * <a href="https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom">this article</a>.
+ * 
+ * @author codex
+ */
+public class SoftBloomFilter extends Filter {
+    
+    private static final Logger logger = Logger.getLogger(SoftBloomFilter.class.getName());
+    
+    private AssetManager assetManager;
+    private RenderManager renderManager;
+    private ViewPort viewPort;
+    private int width;
+    private int height;
+    private Pass[] downsamplingPasses;
+    private Pass[] upsamplingPasses;
+    private final Image.Format format = Image.Format.RGBA16F;
+    private boolean initialized = false;
+    private int numSamplingPasses = 5;
+    private float glowFactor = 0.05f;
+    private boolean bilinearFiltering = true;
+    
+    /**
+     * Creates filter with default settings.
+     */
+    public SoftBloomFilter() {
+        super("SoftBloomFilter");
+    }
+    
+    @Override
+    protected void initFilter(AssetManager am, RenderManager rm, ViewPort vp, int w, int h) {
+        
+        assetManager = am;
+        renderManager = rm;
+        viewPort = vp;
+        postRenderPasses = new LinkedList<>();
+        Renderer renderer = renderManager.getRenderer();
+        this.width = w;
+        this.height = h;
+        
+        capPassesToSize(w, h);
+        
+        downsamplingPasses = new Pass[numSamplingPasses];
+        upsamplingPasses = new Pass[numSamplingPasses];
+        
+        // downsampling passes
+        Material downsampleMat = new Material(assetManager, "Common/MatDefs/Post/Downsample.j3md");
+        Vector2f initTexelSize = new Vector2f(1f/w, 1f/h);
+        w = w >> 1; h = h >> 1;
+        Pass initialPass = new Pass() {
+            @Override
+            public boolean requiresSceneAsTexture() {
+                return true;
+            }
+            @Override
+            public void beforeRender() {
+                downsampleMat.setVector2("TexelSize", initTexelSize);
+            }
+        };
+        initialPass.init(renderer, w, h, format, Image.Format.Depth, 1, downsampleMat);
+        postRenderPasses.add(initialPass);
+        downsamplingPasses[0] = initialPass;
+        for (int i = 1; i < downsamplingPasses.length; i++) {
+            Vector2f texelSize = new Vector2f(1f/w, 1f/h);
+            w = w >> 1; h = h >> 1;
+            Pass prev = downsamplingPasses[i-1];
+            Pass pass = new Pass() {
+                @Override
+                public void beforeRender() {
+                    downsampleMat.setTexture("Texture", prev.getRenderedTexture());
+                    downsampleMat.setVector2("TexelSize", texelSize);
+                }
+            };
+            pass.init(renderer, w, h, format, Image.Format.Depth, 1, downsampleMat);
+            if (bilinearFiltering) {
+                pass.getRenderedTexture().setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
+            }
+            postRenderPasses.add(pass);
+            downsamplingPasses[i] = pass;
+        }
+        
+        // upsampling passes
+        Material upsampleMat = new Material(assetManager, "Common/MatDefs/Post/Upsample.j3md");
+        for (int i = 0; i < upsamplingPasses.length; i++) {
+            Vector2f texelSize = new Vector2f(1f/w, 1f/h);
+            w = w << 1; h = h << 1;
+            Pass prev;
+            if (i == 0) {
+                prev = downsamplingPasses[downsamplingPasses.length-1];
+            } else {
+                prev = upsamplingPasses[i-1];
+            }
+            Pass pass = new Pass() {
+                @Override
+                public void beforeRender() {
+                    upsampleMat.setTexture("Texture", prev.getRenderedTexture());
+                    upsampleMat.setVector2("TexelSize", texelSize);
+                }
+            };
+            pass.init(renderer, w, h, format, Image.Format.Depth, 1, upsampleMat);
+            if (bilinearFiltering) {
+                pass.getRenderedTexture().setMagFilter(Texture.MagFilter.Bilinear);
+            }
+            postRenderPasses.add(pass);
+            upsamplingPasses[i] = pass;
+        }
+        
+        material = new Material(assetManager, "Common/MatDefs/Post/SoftBloomFinal.j3md");
+        material.setTexture("GlowMap", upsamplingPasses[upsamplingPasses.length-1].getRenderedTexture());
+        material.setFloat("GlowFactor", glowFactor);
+        
+        initialized = true;
+        
+    }
+    
+    @Override
+    protected Material getMaterial() {
+        return material;
+    }
+    
+    /**
+     * Sets the number of sampling passes in each step.
+     * <p>
+     * Higher values produce more glow with higher resolution, at the cost
+     * of more passes. Lower values produce less glow with lower resolution.
+     * <p>
+     * The total number of passes is {@code 2n+1}: n passes for downsampling
+     * (13 texture reads per pass per fragment), n passes for upsampling and blur
+     * (9 texture reads per pass per fragment), and 1 pass for blending (2 texture reads
+     * per fragment). Though, it should be noted that for each downsampling pass the
+     * number of fragments decreases by 75%, and for each upsampling pass, the number
+     * of fragments quadruples (which restores the number of fragments to the original
+     * resolution).
+     * <p>
+     * Setting this after the filter has been initialized forces reinitialization.
+     * <p>
+     * default=5
+     * 
+     * @param numSamplingPasses The number of passes per donwsampling/upsampling step. Must be greater than zero.
+     * @throws IllegalArgumentException if argument is less than or equal to zero
+     */
+    public void setNumSamplingPasses(int numSamplingPasses) {
+        if (numSamplingPasses <= 0) {
+            throw new IllegalArgumentException("Number of sampling passes must be greater than zero (found: " + numSamplingPasses + ").");
+        }
+        if (this.numSamplingPasses != numSamplingPasses) {
+            this.numSamplingPasses = numSamplingPasses;
+            if (initialized) {
+                initFilter(assetManager, renderManager, viewPort, width, height);
+            }
+        }
+    }
+    
+    /**
+     * Sets the factor at which the glow result texture is merged with
+     * the scene texture.
+     * <p>
+     * Low values favor the scene texture more, while high values make
+     * glow more noticeable. This value is clamped between 0 and 1.
+     * <p>
+     * default=0.05f
+     * 
+     * @param factor 
+     */
+    public void setGlowFactor(float factor) {
+        this.glowFactor = FastMath.clamp(factor, 0, 1);
+        if (material != null) {
+            material.setFloat("GlowFactor", glowFactor);
+        }
+    }
+    
+    /**
+     * Sets pass textures to use bilinear filtering.
+     * <p>
+     * If true, downsampling textures are set to {@code min=BilinearNoMipMaps} and
+     * upsampling textures are set to {@code mag=Bilinear}, which produces better
+     * quality glow. If false, textures use their default filters.
+     * <p>
+     * default=true
+     * 
+     * @param bilinearFiltering true to use bilinear filtering
+     */
+    public void setBilinearFiltering(boolean bilinearFiltering) {
+        if (this.bilinearFiltering != bilinearFiltering) {
+            this.bilinearFiltering = bilinearFiltering;
+            if (initialized) {
+                for (Pass p : downsamplingPasses) {
+                    if (this.bilinearFiltering) {
+                        p.getRenderedTexture().setMinFilter(Texture.MinFilter.BilinearNoMipMaps);
+                    } else {
+                        p.getRenderedTexture().setMinFilter(Texture.MinFilter.NearestNoMipMaps);
+                    }
+                }
+                for (Pass p : upsamplingPasses) {
+                    if (this.bilinearFiltering) {
+                        p.getRenderedTexture().setMagFilter(Texture.MagFilter.Bilinear);
+                    } else {
+                        p.getRenderedTexture().setMagFilter(Texture.MagFilter.Nearest);
+                    }
+                }
+            }
+        }
+    }
+    
+    /**
+     * Gets the number of downsampling/upsampling passes per step.
+     * 
+     * @return number of downsampling/upsampling passes
+     * @see #setNumSamplingPasses(int)
+     */
+    public int getNumSamplingPasses() {
+        return numSamplingPasses;
+    }
+    
+    /**
+     * Gets the glow factor.
+     * 
+     * @return glow factor
+     * @see #setGlowFactor(float)
+     */
+    public float getGlowFactor() {
+        return glowFactor;
+    }
+    
+    /**
+     * Returns true if pass textures use bilinear filtering.
+     * 
+     * @return 
+     * @see #setBilinearFiltering(boolean)
+     */
+    public boolean isBilinearFiltering() {
+        return bilinearFiltering;
+    }
+    
+    /**
+     * Caps the number of sampling passes so that texture size does
+     * not go below 1 on any axis.
+     * <p>
+     * A message will be logged if the number of sampling passes is changed.
+     * 
+     * @param w texture width
+     * @param h texture height
+     */
+    private void capPassesToSize(int w, int h) {
+        int limit = Math.min(w, h);
+        for (int i = 0; i < numSamplingPasses; i++) {
+            limit = limit >> 1;
+            if (limit <= 2) {
+                numSamplingPasses = i;
+                logger.log(Level.INFO, "Number of sampling passes capped at {0} due to texture size.", i);
+                break;
+            }
+        }
+    }
+    
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(numSamplingPasses, "numSamplingPasses", 5);
+        oc.write(glowFactor, "glowFactor", 0.05f);
+        oc.write(bilinearFiltering, "bilinearFiltering", true);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        numSamplingPasses = ic.readInt("numSamplingPasses", 5);
+        glowFactor = ic.readFloat("glowFactor", 0.05f);
+        bilinearFiltering = ic.readBoolean("bilinearFiltering", true);
+    }
+    
+}

+ 60 - 0
jme3-effects/src/main/resources/Common/MatDefs/Post/Downsample.frag

@@ -0,0 +1,60 @@
+
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+#import "Common/ShaderLib/MultiSample.glsllib"
+
+uniform COLORTEXTURE m_Texture;
+uniform vec2 m_TexelSize;
+varying vec2 texCoord;
+
+void main() {
+
+    // downsampling code: https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom
+
+    float x = m_TexelSize.x;
+    float y = m_TexelSize.y;
+
+    // Take 13 samples around current texel
+    // a - b - c
+    // - j - k -
+    // d - e - f
+    // - l - m -
+    // g - h - i
+    // === ('e' is the current texel) ===
+    vec3 a = getColor(m_Texture, vec2(texCoord.x - 2*x, texCoord.y + 2*y)).rgb;
+    vec3 b = getColor(m_Texture, vec2(texCoord.x,       texCoord.y + 2*y)).rgb;
+    vec3 c = getColor(m_Texture, vec2(texCoord.x + 2*x, texCoord.y + 2*y)).rgb;
+
+    vec3 d = getColor(m_Texture, vec2(texCoord.x - 2*x, texCoord.y)).rgb;
+    vec3 e = getColor(m_Texture, vec2(texCoord.x,       texCoord.y)).rgb;
+    vec3 f = getColor(m_Texture, vec2(texCoord.x + 2*x, texCoord.y)).rgb;
+
+    vec3 g = getColor(m_Texture, vec2(texCoord.x - 2*x, texCoord.y - 2*y)).rgb;
+    vec3 h = getColor(m_Texture, vec2(texCoord.x,       texCoord.y - 2*y)).rgb;
+    vec3 i = getColor(m_Texture, vec2(texCoord.x + 2*x, texCoord.y - 2*y)).rgb;
+
+    vec3 j = getColor(m_Texture, vec2(texCoord.x - x, texCoord.y + y)).rgb;
+    vec3 k = getColor(m_Texture, vec2(texCoord.x + x, texCoord.y + y)).rgb;
+    vec3 l = getColor(m_Texture, vec2(texCoord.x - x, texCoord.y - y)).rgb;
+    vec3 m = getColor(m_Texture, vec2(texCoord.x + x, texCoord.y - y)).rgb;
+
+    // Apply weighted distribution:
+    // 0.5 + 0.125 + 0.125 + 0.125 + 0.125 = 1
+    // a,b,d,e * 0.125
+    // b,c,e,f * 0.125
+    // d,e,g,h * 0.125
+    // e,f,h,i * 0.125
+    // j,k,l,m * 0.5
+    // This shows 5 square areas that are being sampled. But some of them overlap,
+    // so to have an energy preserving downsample we need to make some adjustments.
+    // The weights are the distributed, so that the sum of j,k,l,m (e.g.)
+    // contribute 0.5 to the final color output. The code below is written
+    // to effectively yield this sum. We get:
+    // 0.125*5 + 0.03125*4 + 0.0625*4 = 1
+    vec3 downsample = e*0.125;
+    downsample += (a+c+g+i)*0.03125;
+    downsample += (b+d+f+h)*0.0625;
+    downsample += (j+k+l+m)*0.125;
+    
+    gl_FragColor = vec4(downsample, 1.0);
+    
+}

+ 22 - 0
jme3-effects/src/main/resources/Common/MatDefs/Post/Downsample.j3md

@@ -0,0 +1,22 @@
+MaterialDef Downsample {
+
+    MaterialParameters {
+        Texture2D Texture
+        Vector2 TexelSize
+        Int BoundDrawBuffer
+        Int NumSamples
+    }
+
+    Technique {
+        VertexShader   GLSL300 GLSL150 GLSL100: Common/MatDefs/Post/Post.vert
+        FragmentShader GLSL300 GLSL150 GLSL100: Common/MatDefs/Post/Downsample.frag
+
+        WorldParameters {
+        }
+
+        Defines {
+            BOUND_DRAW_BUFFER: BoundDrawBuffer
+            RESOLVE_MS : NumSamples
+        }
+    }
+}

+ 15 - 0
jme3-effects/src/main/resources/Common/MatDefs/Post/SoftBloomFinal.frag

@@ -0,0 +1,15 @@
+
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+#import "Common/ShaderLib/MultiSample.glsllib"
+
+uniform COLORTEXTURE m_Texture;
+uniform sampler2D m_GlowMap;
+uniform float m_GlowFactor;
+varying vec2 texCoord;
+
+void main() {
+
+    gl_FragColor = mix(getColor(m_Texture, texCoord), texture2D(m_GlowMap, texCoord), m_GlowFactor);
+
+}
+

+ 23 - 0
jme3-effects/src/main/resources/Common/MatDefs/Post/SoftBloomFinal.j3md

@@ -0,0 +1,23 @@
+MaterialDef PBRBloomFinal {
+
+    MaterialParameters {
+        Texture2D Texture
+        Texture2D GlowMap
+        Float GlowFactor : 0.05
+        Int BoundDrawBuffer
+        Int NumSamples
+    }
+
+    Technique {
+        VertexShader   GLSL300 GLSL150 GLSL100: Common/MatDefs/Post/Post.vert
+        FragmentShader GLSL300 GLSL150 GLSL100: Common/MatDefs/Post/SoftBloomFinal.frag
+
+        WorldParameters {
+        }
+
+        Defines {
+            BOUND_DRAW_BUFFER: BoundDrawBuffer
+            RESOLVE_MS : NumSamples
+        }
+    }
+}

+ 46 - 0
jme3-effects/src/main/resources/Common/MatDefs/Post/Upsample.frag

@@ -0,0 +1,46 @@
+
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+#import "Common/ShaderLib/MultiSample.glsllib"
+
+uniform COLORTEXTURE m_Texture;
+uniform vec2 m_TexelSize;
+varying vec2 texCoord;
+
+void main() {
+    
+    // upsampling code: https://learnopengl.com/Guest-Articles/2022/Phys.-Based-Bloom
+
+    // The filter kernel is applied with a radius, specified in texture
+    // coordinates, so that the radius will vary across mip resolutions.
+    float x = m_TexelSize.x;
+    float y = m_TexelSize.y;
+
+    // Take 9 samples around current texel:
+    // a - b - c
+    // d - e - f
+    // g - h - i
+    // === ('e' is the current texel) ===
+    vec3 a = getColor(m_Texture, vec2(texCoord.x - x, texCoord.y + y)).rgb;
+    vec3 b = getColor(m_Texture, vec2(texCoord.x,     texCoord.y + y)).rgb;
+    vec3 c = getColor(m_Texture, vec2(texCoord.x + x, texCoord.y + y)).rgb;
+
+    vec3 d = getColor(m_Texture, vec2(texCoord.x - x, texCoord.y)).rgb;
+    vec3 e = getColor(m_Texture, vec2(texCoord.x,     texCoord.y)).rgb;
+    vec3 f = getColor(m_Texture, vec2(texCoord.x + x, texCoord.y)).rgb;
+
+    vec3 g = getColor(m_Texture, vec2(texCoord.x - x, texCoord.y - y)).rgb;
+    vec3 h = getColor(m_Texture, vec2(texCoord.x,     texCoord.y - y)).rgb;
+    vec3 i = getColor(m_Texture, vec2(texCoord.x + x, texCoord.y - y)).rgb;
+
+    // Apply weighted distribution, by using a 3x3 tent filter:
+    //        | 1 2 1 |
+    // 1/16 * | 2 4 2 |
+    //        | 1 2 1 |
+    vec3 upsample = e*4.0;
+    upsample += (b+d+f+h)*2.0;
+    upsample += (a+c+g+i);
+    upsample /= 16.0;
+    
+    gl_FragColor = vec4(upsample, 1.0);
+    
+}

+ 23 - 0
jme3-effects/src/main/resources/Common/MatDefs/Post/Upsample.j3md

@@ -0,0 +1,23 @@
+MaterialDef Upsample {
+
+    MaterialParameters {
+        Texture2D Texture
+        Vector2 TexelSize
+        Float FilterRadius : 0.01
+        Int BoundDrawBuffer
+        Int NumSamples
+    }
+
+    Technique {
+        VertexShader   GLSL300 GLSL150 GLSL100: Common/MatDefs/Post/Post.vert
+        FragmentShader GLSL300 GLSL150 GLSL100: Common/MatDefs/Post/Upsample.frag
+
+        WorldParameters {
+        }
+
+        Defines {
+            BOUND_DRAW_BUFFER: BoundDrawBuffer
+            RESOLVE_MS : NumSamples
+        }
+    }
+}

+ 0 - 2
jme3-examples/src/main/java/jme3test/post/TestBloom.java

@@ -75,8 +75,6 @@ public class TestBloom extends SimpleApplication {
         mat.setColor("Diffuse", ColorRGBA.Yellow.mult(0.2f));
         mat.setColor("Specular", ColorRGBA.Yellow.mult(0.8f));
 
-    
-
 
         Material matSoil = new Material(assetManager,"Common/MatDefs/Light/Lighting.j3md");
         matSoil.setFloat("Shininess", 15f);

+ 253 - 0
jme3-examples/src/main/java/jme3test/post/TestSoftBloom.java

@@ -0,0 +1,253 @@
+/*
+ * Copyright (c) 2024 jMonkeyEngine
+ * All rights reserved.
+ *
+ * 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.
+ *
+ * * 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.
+ *
+ * * 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.
+ *
+ * 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 jme3test.post;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.asset.TextureKey;
+import com.jme3.environment.EnvironmentProbeControl;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.AnalogListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.light.DirectionalLight;
+import com.jme3.light.PointLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.SoftBloomFilter;
+import com.jme3.renderer.queue.RenderQueue.ShadowMode;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Spatial;
+import com.jme3.scene.shape.Box;
+import com.jme3.util.SkyFactory;
+import com.jme3.util.SkyFactory.EnvMapType;
+
+/**
+ * Tests {@link SoftBloomFilter} with HDR.
+ * <p>
+ * Note: the camera is pointed directly at the ground, which is completely
+ * black for some reason.
+ * 
+ * @author codex
+ */
+public class TestSoftBloom extends SimpleApplication implements ActionListener, AnalogListener {
+
+    private SoftBloomFilter bloom;
+    private BitmapText passes, factor, bilinear;
+    private BitmapText power, intensity;
+    private Material tankMat;
+    private float emissionPower = 50;
+    private float emissionIntensity = 50;
+    private final int maxPasses = 10;
+    private final float factorRate = 0.1f;
+    
+    public static void main(String[] args){
+        TestSoftBloom app = new TestSoftBloom();
+        app.start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        
+        cam.setLocation(new Vector3f(10, 10, 10));
+        flyCam.setMoveSpeed(20);
+
+        Material mat = new Material(assetManager,"Common/MatDefs/Light/Lighting.j3md");
+        mat.setFloat("Shininess", 15f);
+        mat.setBoolean("UseMaterialColors", true);
+        mat.setColor("Ambient", ColorRGBA.Yellow.mult(0.2f));
+        mat.setColor("Diffuse", ColorRGBA.Yellow.mult(0.2f));
+        mat.setColor("Specular", ColorRGBA.Yellow.mult(0.8f));
+
+        Material matSoil = new Material(assetManager,"Common/MatDefs/Light/Lighting.j3md");
+        matSoil.setFloat("Shininess", 15f);
+        matSoil.setBoolean("UseMaterialColors", true);
+        matSoil.setColor("Ambient", ColorRGBA.Gray);
+        matSoil.setColor("Diffuse", ColorRGBA.Gray);
+        matSoil.setColor("Specular", ColorRGBA.Gray);
+
+        Spatial teapot = assetManager.loadModel("Models/Teapot/Teapot.obj");
+        teapot.setLocalTranslation(0,0,10);
+
+        teapot.setMaterial(mat);
+        teapot.setShadowMode(ShadowMode.CastAndReceive);
+        teapot.setLocalScale(10.0f);
+        rootNode.attachChild(teapot);
+
+        Geometry soil = new Geometry("soil", new Box(800, 10, 700));
+        soil.setLocalTranslation(0, -13, 550);
+        soil.setMaterial(matSoil);
+        soil.setShadowMode(ShadowMode.CastAndReceive);
+        rootNode.attachChild(soil);
+        
+        tankMat = new Material(assetManager, "Common/MatDefs/Light/PBRLighting.j3md");
+        tankMat.setTexture("BaseColorMap", assetManager.loadTexture(new TextureKey("Models/HoverTank/tank_diffuse.jpg", !true)));
+        tankMat.setTexture("SpecularMap", assetManager.loadTexture(new TextureKey("Models/HoverTank/tank_specular.jpg", !true)));
+        tankMat.setTexture("NormalMap", assetManager.loadTexture(new TextureKey("Models/HoverTank/tank_normals.png", !true)));
+        tankMat.setTexture("EmissiveMap", assetManager.loadTexture(new TextureKey("Models/HoverTank/tank_glow_map.jpg", !true)));
+        tankMat.setFloat("EmissivePower", emissionPower);
+        tankMat.setFloat("EmissiveIntensity", 50);
+        tankMat.setFloat("Metallic", .5f);
+        Spatial tank = assetManager.loadModel("Models/HoverTank/Tank2.mesh.xml");
+        tank.setLocalTranslation(-10, 5, -10);
+        tank.setMaterial(tankMat);
+        rootNode.attachChild(tank);
+
+        DirectionalLight light=new DirectionalLight();
+        light.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());
+        light.setColor(ColorRGBA.White);
+        //rootNode.addLight(light);
+        
+        PointLight pl = new PointLight();
+        pl.setPosition(new Vector3f(5, 5, 5));
+        pl.setRadius(1000);
+        pl.setColor(ColorRGBA.White);
+        rootNode.addLight(pl);
+
+        // load sky
+        Spatial sky = SkyFactory.createSky(assetManager, 
+                "Textures/Sky/Bright/FullskiesBlueClear03.dds", 
+                EnvMapType.CubeMap);
+        sky.setCullHint(Spatial.CullHint.Never);
+        rootNode.attachChild(sky);
+        EnvironmentProbeControl.tagGlobal(sky);
+        
+        rootNode.addControl(new EnvironmentProbeControl(assetManager, 256));
+        
+        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
+        bloom = new SoftBloomFilter();
+        fpp.addFilter(bloom);
+        viewPort.addProcessor(fpp);
+        
+        int textY = context.getSettings().getHeight()-5;
+        float xRow1 = 10, xRow2 = 250;
+        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        passes = createText("", xRow1, textY);
+        createText("[ R / F ]", xRow2, textY);
+        factor = createText("", xRow1, textY-25);
+        createText("[ T / G ]", xRow2, textY-25);
+        bilinear = createText("", xRow1, textY-25*2);
+        createText("[ space ]", xRow2, textY-25*2);
+        power = createText("", xRow1, textY-25*3);
+        createText("[ Y / H ]", xRow2, textY-25*3);
+        intensity = createText("", xRow1, textY-25*4);
+        createText("[ U / J ]", xRow2, textY-25*4);
+        updateHud();
+        
+        inputManager.addMapping("incr-passes", new KeyTrigger(KeyInput.KEY_R));
+        inputManager.addMapping("decr-passes", new KeyTrigger(KeyInput.KEY_F));
+        inputManager.addMapping("incr-factor", new KeyTrigger(KeyInput.KEY_T));
+        inputManager.addMapping("decr-factor", new KeyTrigger(KeyInput.KEY_G));
+        inputManager.addMapping("toggle-bilinear", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addMapping("incr-power", new KeyTrigger(KeyInput.KEY_Y));
+        inputManager.addMapping("decr-power", new KeyTrigger(KeyInput.KEY_H));
+        inputManager.addMapping("incr-intensity", new KeyTrigger(KeyInput.KEY_U));
+        inputManager.addMapping("decr-intensity", new KeyTrigger(KeyInput.KEY_J));
+        inputManager.addListener(this, "incr-passes", "decr-passes", "incr-factor", "decr-factor",
+                "toggle-bilinear", "incr-power", "decr-power", "incr-intensity", "decr-intensity");
+        
+    }
+    
+    @Override
+    public void simpleUpdate(float tpf) {
+        updateHud();
+    }
+    
+    @Override
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if (isPressed) {
+            if (name.equals("incr-passes")) {
+                bloom.setNumSamplingPasses(Math.min(bloom.getNumSamplingPasses()+1, maxPasses));
+            } else if (name.equals("decr-passes")) {
+                bloom.setNumSamplingPasses(Math.max(bloom.getNumSamplingPasses()-1, 1));
+            } else if (name.equals("toggle-bilinear")) {
+                bloom.setBilinearFiltering(!bloom.isBilinearFiltering());
+            }
+            updateHud();
+        }
+    }
+    
+    @Override
+    public void onAnalog(String name, float value, float tpf) {
+        if (name.equals("incr-factor")) {
+            bloom.setGlowFactor(bloom.getGlowFactor()+factorRate*tpf);
+        } else if (name.equals("decr-factor")) {
+            bloom.setGlowFactor(bloom.getGlowFactor()-factorRate*tpf);
+        } else if (name.equals("incr-power")) {
+            emissionPower += 10f*tpf;
+            updateTankMaterial();
+        } else if (name.equals("decr-power")) {
+            emissionPower -= 10f*tpf;
+            updateTankMaterial();
+        } else if (name.equals("incr-intensity")) {
+            emissionIntensity += 10f*tpf;
+            updateTankMaterial();
+        } else if (name.equals("decr-intensity")) {
+            emissionIntensity -= 10f*tpf;
+            updateTankMaterial();
+        }
+        updateHud();
+    }
+    
+    private BitmapText createText(String string, float x, float y) {
+        BitmapText text = new BitmapText(guiFont);
+        text.setSize(guiFont.getCharSet().getRenderedSize());
+        text.setLocalTranslation(x, y, 0);
+        text.setText(string);
+        guiNode.attachChild(text);
+        return text;
+    }
+    
+    private void updateHud() {
+        passes.setText("Passes = " + bloom.getNumSamplingPasses());
+        factor.setText("Glow Factor = " + floatToString(bloom.getGlowFactor(), 5));
+        bilinear.setText("Bilinear Filtering = " + bloom.isBilinearFiltering());
+        power.setText("Emission Power = " + floatToString(emissionPower, 5));
+        intensity.setText("Emission Intensity = " + floatToString(emissionIntensity, 5));
+    }
+    
+    private String floatToString(float value, int length) {
+        String string = Float.toString(value);
+        return string.substring(0, Math.min(length, string.length()));
+    }
+    
+    private void updateTankMaterial() {
+        emissionPower = Math.max(emissionPower, 0);
+        emissionIntensity = Math.max(emissionIntensity, 0);
+        tankMat.setFloat("EmissivePower", emissionPower);
+        tankMat.setFloat("EmissiveIntensity", emissionIntensity);
+    }
+    
+}