diff --git a/jme3-effects/src/main/java/com/jme3/post/filters/SoftBloomFilter.java b/jme3-effects/src/main/java/com/jme3/post/filters/SoftBloomFilter.java new file mode 100644 index 0000000000..3ccbf03ff9 --- /dev/null +++ b/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. + *
+ * Compared to {@link BloomFilter}, this filter produces much higher quality + * results that feel much more natural. + *
+ * 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 only be used + * if HDR is also being utilized, otherwise BloomFilter should be preferred. + *
+ * This filter uses the PBR bloom algorithm presented in + * this article. + * + * @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. + *
+ * Higher values produce more glow with higher resolution, at the cost + * of more passes. Lower values produce less glow with lower resolution. + *
+ * 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). + *
+ * Setting this after the filter has been initialized forces reinitialization. + *
+ * 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. + *
+ * Low values favor the scene texture more, while high values make + * glow more noticeable. This value is clamped between 0 and 1. + *
+ * 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. + *
+ * 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. + *
+ * 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. + *
+ * 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); + } + +} diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Downsample.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/Downsample.frag new file mode 100644 index 0000000000..2803a95d87 --- /dev/null +++ b/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); + +} diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Downsample.j3md b/jme3-effects/src/main/resources/Common/MatDefs/Post/Downsample.j3md new file mode 100644 index 0000000000..595a918eb3 --- /dev/null +++ b/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 + } + } +} diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/SoftBloomFinal.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/SoftBloomFinal.frag new file mode 100644 index 0000000000..20fb792cc3 --- /dev/null +++ b/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); + +} + diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/SoftBloomFinal.j3md b/jme3-effects/src/main/resources/Common/MatDefs/Post/SoftBloomFinal.j3md new file mode 100644 index 0000000000..d0597f9f9b --- /dev/null +++ b/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 + } + } +} diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Upsample.frag b/jme3-effects/src/main/resources/Common/MatDefs/Post/Upsample.frag new file mode 100644 index 0000000000..61bf280061 --- /dev/null +++ b/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); + +} diff --git a/jme3-effects/src/main/resources/Common/MatDefs/Post/Upsample.j3md b/jme3-effects/src/main/resources/Common/MatDefs/Post/Upsample.j3md new file mode 100644 index 0000000000..2ce2cc9976 --- /dev/null +++ b/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 + } + } +} diff --git a/jme3-examples/src/main/java/jme3test/post/TestBloom.java b/jme3-examples/src/main/java/jme3test/post/TestBloom.java index faef6ac8e7..5608890801 100644 --- a/jme3-examples/src/main/java/jme3test/post/TestBloom.java +++ b/jme3-examples/src/main/java/jme3test/post/TestBloom.java @@ -75,8 +75,6 @@ public void simpleInitApp() { 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); diff --git a/jme3-examples/src/main/java/jme3test/post/TestSoftBloom.java b/jme3-examples/src/main/java/jme3test/post/TestSoftBloom.java new file mode 100644 index 0000000000..e277e8f943 --- /dev/null +++ b/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. + *
+ * 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); + } + +}