Browse Source

ColorAdjustmentFilter (#1665)

* ContrastAdjustmentFilter

* ContrastAdjustmentFilter test

* brightness code fix

* Fixed material reading problems, added gles glsl support, added final pass scales to the color channels

* update the testcase to meet the new changes

* corrected instances namings in the testcase

* Changed the default exp value to 2.2 and documenting other default values

* Fixed default constructor javadocs

* Corrected the brightness docs

* Cleaning up

* Cleaning up

* Java naming convention, code design simplifications, docs improvements

* Java naming conventions

* Minor language fix

* Removed keyword 'brightness' from all files

* don't limit the output scaling to between 0 and 1

* ContrastAdjustmentFilter:  simplify the class description

* ContrastAdjustmentFilter:  clarify what the input range does

* frags:  use max() instead of abs() to avoid unintended color inversion

* ContrastAdjustmentFilter:  remove the null check from getMaterial()

* ContrastAdjustmentFilter:  add setters to set each parameter by itself

* ContrastAdjustmentFilter:  add a toString() method

* TestContrastAdjustmentFilter:  add a user interface for thorough testing

* ContrastAdjustmentFilter:  set the filter name in both constructors

* ContrastAdjustmentFilter:  remove the extra "material" field

* ContrastAdjustmentFilter:  don't use overridable method in constructor

* ContrastAdjustmentFilter: consistent naming of fields, methods, and args

* ContrastAdjustmentFilter:  privatize 8 fields for better encapsulation

* TCAF:  include current year in the copyright notice

* TCAF:  display the filter status using a BitmapText

* TCAF:  scale change rates so they don't depend on the frames per second

* TCAF:  make the globe smaller, orient with the North Pole on top

* TCAF:  unshaded scene doesn't need any lights

* TCAF:  press Enter to reset the filter to defaults

* TCAF:  configure multisampling in the FilterPostProcessor

* TCAF:  improve the class javadoc

* ContrastAdjustmentFilter:  improve the javadoc

* ContrastAdjustmentFilter:  delete some unnecessary checks

* TCAF:  avoid crash when numSamples=0

* ContrastAdjustmentFilter:  more javadoc

* TCAF: rename to TestContrastAdjustment

* rename ColorContrast->ContrastAdjustment consistent with other filters

Co-authored-by: Stephen Gold <[email protected]>
Scrappers Team 3 years ago
parent
commit
645b1bb3fb

+ 433 - 0
jme3-effects/src/main/java/com/jme3/post/filters/ContrastAdjustmentFilter.java

@@ -0,0 +1,433 @@
+/*
+ * Copyright (c) 2009-2021 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.post.Filter;
+import com.jme3.renderer.RenderManager;
+import com.jme3.renderer.ViewPort;
+import java.io.IOException;
+
+/**
+ * A filter to adjust the colors of a rendered scene by normalizing each color channel to a specified range,
+ * applying a power law, and scaling the output. The alpha channel is unaffected.
+ *
+ * @author pavl_g.
+ */
+public class ContrastAdjustmentFilter extends Filter {
+    /**
+     * Power-law exponent for the red channel.
+     */
+    private float redExponent = 1f;
+    /**
+     * Power-law exponent for the green channel.
+     */
+    private float greenExponent = 1f;
+    /**
+     * Power-law exponent for the blue channel.
+     */
+    private float blueExponent = 1f;
+    /**
+     * Lower limit of the input range for all color channels:
+     * the highest level that the filter normalizes to 0.
+     */
+    private float lowerLimit = 0f;
+    /**
+     * Upper limit of the input range for all color channels:
+     * the level that the filter normalizes to 1 (before output scaling).
+     */
+    private float upperLimit = 1f;
+    /**
+     * Output scale factor for the red channel.
+     */
+    private float redScale = 1f;
+    /**
+     * Output scale factor for the green channel.
+     */
+    private float greenScale = 1f;
+    /**
+     * Output scale factor for the blue channel.
+     */
+    private float blueScale = 1f;
+
+    /**
+     * Instantiates a contrast-adjustment filter with the default parameters:
+     *
+     * <p>input range from 0 to 1
+     * <p>power-law exponent=1 for all color channels
+     * <p>output scale factor=1 for all color channels
+     *
+     * Such a filter has no effect.
+     */
+    public ContrastAdjustmentFilter() {
+        super("Contrast Adjustment");
+    }
+
+    /**
+     * Instantiates a contrast-adjustment filter with the default input range and output scaling.
+     *
+     * @param exponent the desired power-law exponent for all color channels
+     */
+    public ContrastAdjustmentFilter(float exponent) {
+        this();
+        this.redExponent = exponent;
+        this.greenExponent = exponent;
+        this.blueExponent = exponent;
+    }
+
+    /**
+     * Sets the power-law exponent for each color channel.
+     * The default value for each exponent is 1, which produces a linear filter.
+     *
+     * @param redExponent the desired red-channel exponent
+     * @param greenExponent the desired green-channel exponent
+     * @param blueExponent the desired blue-channel exponent
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setExponents(float redExponent, float greenExponent, float blueExponent) {
+        setRedExponent(redExponent);
+        setGreenExponent(greenExponent);
+        setBlueExponent(blueExponent);
+
+        return this;
+    }
+
+    /**
+     * Sets the power-law exponent for the red channel.
+     *
+     * @param exponent the desired exponent (default=1 for linear)
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setRedExponent(float exponent) {
+        this.redExponent = exponent;
+        if (material != null) {
+            material.setFloat("redChannelExponent", redExponent);
+        }
+        return this;
+    }
+
+    /**
+     * Sets the power-law exponent for the green channel.
+     *
+     * @param exponent the desired exponent (default=1 for linear)
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setGreenExponent(float exponent) {
+        this.greenExponent = exponent;
+        if (material != null) {
+            material.setFloat("greenChannelExponent", greenExponent);
+        }
+        return this;
+    }
+
+    /**
+     * Sets the power-law exponent for the blue channel.
+     *
+     * @param exponent the desired exponent (default=1 for linear)
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setBlueExponent(float exponent) {
+        this.blueExponent = exponent;
+        if (material != null) {
+            material.setFloat("blueChannelExponent", blueExponent);
+        }
+        return this;
+    }
+
+    /**
+     * Retrieves the red-channel exponent.
+     *
+     * @return the power-law exponent (default=1 for linear)
+     */
+    public float getRedExponent() {
+        return redExponent;
+    }
+
+    /**
+     * Retrieves the green-channel exponent.
+     *
+     * @return the power-law exponent (default=1 for linear)
+     */
+    public float getGreenExponent() {
+        return greenExponent;
+    }
+
+    /**
+     * Retrieves the blue-channel exponent.
+     *
+     * @return the power-law exponent (default=1 for linear)
+     */
+    public float getBlueExponent() {
+        return blueExponent;
+    }
+
+    /**
+     * Sets the input range for each color channel.
+     *
+     * <p>Before applying the power law, the input levels get normalized:
+     * lowerLimit and below normalize to 0 and upperLimit normalizes to 1.
+     *
+     * @param lowerLimit the desired lower limit (default=0)
+     * @param upperLimit the desired upper limit (default=1)
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setInputRange(float lowerLimit, float upperLimit) {
+        setLowerLimit(lowerLimit);
+        setUpperLimit(upperLimit);
+        return this;
+    }
+
+    /**
+     * Sets the upper limit of the input range.
+     *
+     * @param level the input level that should be normalized to 1 (default=1)
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setUpperLimit(float level) {
+        this.upperLimit = level;
+        if (material != null) {
+            material.setFloat("upperLimit", upperLimit);
+        }
+        return this;
+    }
+
+    /**
+     * Sets the lower limit of the input range.
+     *
+     * @param level the highest input level that should be normalized to 0 (default=0)
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setLowerLimit(float level) {
+        this.lowerLimit = level;
+        if (material != null) {
+            material.setFloat("lowerLimit", lowerLimit);
+        }
+        return this;
+    }
+
+    /**
+     * Returns the lower limit of the input range.
+     *
+     * @return the input level (default=0)
+     */
+    public float getLowerLimit() {
+        return lowerLimit;
+    }
+
+    /**
+     * Returns the upper limit of the input range.
+     *
+     * @return the input level (default=1)
+     */
+    public float getUpperLimit() {
+        return upperLimit;
+    }
+
+    /**
+     * Adjusts the output scaling for each color channel.
+     * The default for each scale factor is 1, which has no effect.
+     *
+     * @param redScale the red-channel scale factor
+     * @param greenScale the green-channel scale factor
+     * @param blueScale the blue-channel scale factor
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setScales(float redScale, float greenScale, float blueScale) {
+        setRedScale(redScale);
+        setGreenScale(greenScale);
+        setBlueScale(blueScale);
+
+        return this;
+    }
+
+    /**
+     * Sets the output scale factor for the red channel.
+     *
+     * @param factor the desired scale factor (default=1)
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setRedScale(float factor) {
+        this.redScale = factor;
+        if (material != null) {
+            material.setFloat("redChannelScale", redScale);
+        }
+        return this;
+    }
+
+    /**
+     * Sets the output scale factor for the green channel.
+     *
+     * @param factor the desired scale factor (default=1)
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setGreenScale(float factor) {
+        this.greenScale = factor;
+        if (material != null) {
+            material.setFloat("greenChannelScale", greenScale);
+        }
+        return this;
+    }
+
+    /**
+     * Sets the output scale factor for the blue channel.
+     *
+     * @param factor the desired scale factor (default=1)
+     * @return this filter instance, for chaining
+     */
+    public ContrastAdjustmentFilter setBlueScale(float factor) {
+        this.blueScale = factor;
+        if (material != null) {
+            material.setFloat("blueChannelScale", blueScale);
+        }
+        return this;
+    }
+
+    /**
+     * Retrieves the output scale factor for the red channel.
+     *
+     * @return the scale factor (default=1 for no effect)
+     */
+    public float getRedScale() {
+        return redScale;
+    }
+
+    /**
+     * Retrieves the output scale factor for the green channel.
+     *
+     * @return the scale factor (default=1 for no effect)
+     */
+    public float getGreenScale() {
+        return greenScale;
+    }
+
+    /**
+     * Retrieves the output scale factor for the blue channel.
+     *
+     * @return the scale factor (default=1 for no effect)
+     */
+    public float getBlueScale() {
+        return blueScale;
+    }
+
+    /**
+     * Initializes the Filter when it is added to a FilterPostProcessor.
+     *
+     * @param assetManager for loading assets (not null)
+     * @param renderManager unused
+     * @param viewPort unused
+     * @param width unused
+     * @param height unused
+     */
+    @Override
+    protected void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) {
+        material = new Material(manager, "Common/MatDefs/Post/ContrastAdjustment.j3md");
+
+        //different channels exp for different transfer functions
+        setExponents(redExponent, greenExponent, blueExponent);
+
+        //input range
+        setInputRange(lowerLimit, upperLimit);
+
+        //final pass scales
+        setScales(redScale, greenScale, blueScale);
+    }
+
+    /**
+     * Returns the Material used in this Filter. This method is invoked on every frame.
+     *
+     * @return the pre-existing instance, or null if the Filter hasn't been initialized
+     */
+    @Override
+    protected Material getMaterial() {
+        return material;
+    }
+
+    /**
+     * De-serializes this filter, for example when loading from a J3O file.
+     *
+     * @param im the importer to use (not null)
+     * @throws IOException from the importer
+     */
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        final InputCapsule inputCapsule = im.getCapsule(this);
+        redExponent = inputCapsule.readFloat("redExponent", 1f);
+        greenExponent = inputCapsule.readFloat("greenExponent", 1f);
+        blueExponent = inputCapsule.readFloat("blueExponent", 1f);
+        lowerLimit = inputCapsule.readFloat("lowerLimit", 0f);
+        upperLimit = inputCapsule.readFloat("upperLimit", 1f);
+        redScale = inputCapsule.readFloat("redScale", 1f);
+        greenScale = inputCapsule.readFloat("greenScale", 1f);
+        blueScale = inputCapsule.readFloat("blueScale", 1f);
+    }
+
+    /**
+     * Serializes this filter, for example when saving to a J3O file.
+     *
+     * @param ex the exporter to use (not null)
+     * @throws IOException from the exporter
+     */
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        final OutputCapsule outputCapsule = ex.getCapsule(this);
+        outputCapsule.write(redExponent, "redExponent", 1f);
+        outputCapsule.write(greenExponent, "greenExponent", 1f);
+        outputCapsule.write(blueExponent, "blueExponent", 1f);
+        outputCapsule.write(lowerLimit, "lowerLimit", 0f);
+        outputCapsule.write(upperLimit, "upperLimit", 1f);
+        outputCapsule.write(redScale, "redScale", 1f);
+        outputCapsule.write(greenScale, "greenScale", 1f);
+        outputCapsule.write(blueScale, "blueScale", 1f);
+    }
+
+    /**
+     * Represent this Filter as a String.
+     *
+     * @return a descriptive string of text (not null)
+     */
+    @Override
+    public String toString() {
+        String result = String.format(
+                "input(%.3f, %.3f) exp(%.3f, %.3f, %.3f) scale(%.3f, %.3f, %.3f)",
+                lowerLimit, upperLimit,
+                redExponent, greenExponent, blueExponent,
+                redScale, greenScale, blueScale);
+        return result;
+    }
+}

+ 89 - 0
jme3-effects/src/main/resources/Common/MatDefs/Post/ContrastAdjustment.frag

@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2009-2021 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.
+ */
+
+/*
+ * Used by ContrastAdjustment.j3md to adjust the color channels.
+ *
+ * First, the input range is normalized to upper and lower limits.
+ * Then a power law is applied, using the exponent for each channel.
+ * Finally, the output value is scaled linearly, using the scaling factor for each channel.
+ *
+ * Supports GLSL100 GLSL110 GLSL120 GLSL130.
+ */
+
+//constant inputs from java source
+uniform float m_redChannelExponent;
+uniform float m_greenChannelExponent;
+uniform float m_blueChannelExponent;
+
+//final scale values
+uniform float m_redChannelScale;
+uniform float m_greenChannelScale;
+uniform float m_blueChannelScale;
+
+//input range
+uniform float m_lowerLimit;
+uniform float m_upperLimit;
+
+//container for the input from post.vert
+uniform sampler2D m_Texture;
+
+//varying input from post.vert vertex shader
+varying vec2 texCoord;
+
+void main() {
+    //get the color from a 2d sampler.
+    vec4 color = texture2D(m_Texture, texCoord);
+
+    //apply the color transfer function.
+
+    //1) adjust the channels input range
+    color.rgb = (color.rgb - vec3(m_lowerLimit)) / (vec3(m_upperLimit) - vec3(m_lowerLimit));
+
+    // avoid negative levels
+    color.r = max(color.r, 0.0);
+    color.g = max(color.g, 0.0);
+    color.b = max(color.b, 0.0);
+
+    //2) apply transfer functions on different channels.
+    color.r = pow(color.r, m_redChannelExponent);
+    color.g = pow(color.g, m_greenChannelExponent);
+    color.b = pow(color.b, m_blueChannelExponent);
+
+    //3) scale the output levels
+    color.r = color.r * m_redChannelScale;
+    color.b = color.b * m_blueChannelScale;
+    color.g = color.g * m_greenChannelScale;
+
+    //4) process the textures colors.
+    gl_FragColor = color;
+}

+ 28 - 0
jme3-effects/src/main/resources/Common/MatDefs/Post/ContrastAdjustment.j3md

@@ -0,0 +1,28 @@
+//Used by com.jme3.post.filters.ColorAdjustmentFilter.java
+//supports both OGL and OGLES glsl
+
+MaterialDef ColorAdjustmentFilter {
+
+    MaterialParameters {
+        Int NumSamples
+        Texture2D Texture
+        Float redChannelExponent
+        Float greenChannelExponent
+        Float blueChannelExponent
+        Float lowerLimit
+        Float upperLimit
+        Float redChannelScale
+        Float greenChannelScale
+        Float blueChannelScale
+    }
+
+    Technique {
+        VertexShader GLSL150 GLSL300 GLSL310 GLSL320:   Common/MatDefs/Post/Post15.vert
+        FragmentShader GLSL150 GLSL300 GLSL310 GLSL320: Common/MatDefs/Post/ContrastAdjustment15.frag
+    }
+
+    Technique {
+        VertexShader  GLSL100 GLSL110 GLSL120 GLSL130:   Common/MatDefs/Post/Post.vert
+        FragmentShader GLSL100 GLSL110 GLSL120 GLSL130: Common/MatDefs/Post/ConstrastAdjustment.frag
+    }
+}

+ 95 - 0
jme3-effects/src/main/resources/Common/MatDefs/Post/ContrastAdjustment15.frag

@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2009-2021 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.
+ */
+
+/*
+ * Used by ContrastAdjustment.j3md to adjust the color channels.
+ *
+ * First, the input range is normalized to upper and lower limits.
+ * Then a power law is applied, using the exponent for each channel.
+ * Finally, the output value is scaled linearly, using the scaling factor for each channel.
+ *
+ * Supports glsl150 and glsl150+ including android GLES glsl300, glsl310 and glsl320.
+ */
+
+#import "Common/ShaderLib/GLSLCompat.glsllib"
+#import "Common/ShaderLib/MultiSample.glsllib"
+
+//constant inputs from java source
+uniform float m_redChannelExponent;
+uniform float m_greenChannelExponent;
+uniform float m_blueChannelExponent;
+
+//final scale values
+uniform float m_redChannelScale;
+uniform float m_greenChannelScale;
+uniform float m_blueChannelScale;
+
+//input range
+uniform float m_lowerLimit;
+uniform float m_upperLimit;
+
+//container for the input from post15.vert
+uniform COLORTEXTURE m_Texture;
+
+//varying input from post15.vert vertex shader
+in vec2 texCoord;
+
+//the output color
+out vec4 fragColor;
+
+void main() {
+    //get the color from a 2d sampler.
+    vec4 color = texture2D(m_Texture, texCoord);
+
+    //apply the color transfer function.
+
+    //1) adjust the channels input range
+    color.rgb = (color.rgb - vec3(m_lowerLimit)) / (vec3(m_upperLimit) - vec3(m_lowerLimit));
+
+    // avoid negative levels
+    color.r = max(color.r, 0.0);
+    color.g = max(color.g, 0.0);
+    color.b = max(color.b, 0.0);
+
+    //2) apply transfer functions on different channels.
+    color.r = pow(color.r, m_redChannelExponent);
+    color.g = pow(color.g, m_greenChannelExponent);
+    color.b = pow(color.b, m_blueChannelExponent);
+
+    //3) scale the output levels
+    color.r = color.r * m_redChannelScale;
+    color.b = color.b * m_blueChannelScale;
+    color.g = color.g * m_greenChannelScale;
+
+    //4) process the textures colors.
+    fragColor = color;
+}

+ 220 - 0
jme3-examples/src/main/java/jme3test/post/TestContrastAdjustment.java

@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2009-2021 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.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.material.Material;
+import com.jme3.math.FastMath;
+import com.jme3.post.FilterPostProcessor;
+import com.jme3.post.filters.ContrastAdjustmentFilter;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.shape.Sphere;
+import com.jme3.texture.Texture;
+
+/**
+ * A {@link ContrastAdjustmentFilter} with user-controlled exponents, scales, and input range.
+ *
+ * @author pavl_g.
+ */
+public class TestContrastAdjustment extends SimpleApplication {
+
+    /**
+     * Display filter status.
+     */
+    private BitmapText statusText;
+    /**
+     * The filter being tested.
+     */
+    private ContrastAdjustmentFilter contrastAdjustmentFilter;
+
+    public static void main(String[] args) {
+        new TestContrastAdjustment().start();
+    }
+
+    @Override
+    public void simpleInitApp() {
+        /*
+         * Attach an unshaded globe to the scene.
+         */
+        final Sphere globe = new Sphere(40, 40, 3.5f);
+        final Geometry earth = new Geometry("Earth", globe);
+        earth.rotate(-FastMath.HALF_PI, 0f, 0f);
+        final Material material = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        final Texture texture = assetManager.loadTexture("Textures/Sky/Earth/Earth.jpg");
+        material.setTexture("ColorMap", texture);
+        earth.setMaterial(material);
+        rootNode.attachChild(earth);
+
+        final FilterPostProcessor postProcessor = new FilterPostProcessor(assetManager);
+        int numSamples = settings.getSamples();
+        if (numSamples > 0) {
+            postProcessor.setNumSamples(numSamples);
+        }
+        viewPort.addProcessor(postProcessor);
+        /*
+         * Add the filter to be tested.
+         */
+        contrastAdjustmentFilter = new ContrastAdjustmentFilter();
+        //adjusting some parameters
+        contrastAdjustmentFilter.setExponents(1.8f, 1.8f, 2.1f)
+                                .setInputRange(0, 0.367f)
+                                .setScales(0.25f, 0.25f, 1f);
+        postProcessor.addFilter(contrastAdjustmentFilter);
+
+        setUpUserInterface();
+    }
+
+    /**
+     * Update the status text.
+     *
+     * @param tpf unused
+     */
+    @Override
+    public void simpleUpdate(float tpf) {
+        String status = contrastAdjustmentFilter.toString();
+        statusText.setText(status);
+    }
+
+    private void setUpUserInterface() {
+        /*
+         * Attach a BitmapText to display the status of the ContrastAdjustmentFilter.
+         */
+        statusText = new BitmapText(guiFont);
+        guiNode.attachChild(statusText);
+        statusText.setLocalTranslation(0f, cam.getHeight(), 0f);
+        /*
+         * Create listeners for user keypresses.
+         */
+        ActionListener action = new ActionListener() {
+            @Override
+            public void onAction(String name, boolean keyPressed, float tpf) {
+                if (name.equals("reset") && keyPressed) {
+                    contrastAdjustmentFilter.setExponents(1f, 1f, 1f)
+                            .setInputRange(0f, 1f)
+                            .setScales(1f, 1f, 1f);
+                }
+            }
+        };
+        AnalogListener analog = new AnalogListener() {
+            @Override
+            public void onAnalog(String name, float value, float tpf) {
+                float increment = name.endsWith("+") ? 0.3f * tpf : -0.3f * tpf;
+
+                if (name.startsWith("lower")) {
+                    float newValue = contrastAdjustmentFilter.getLowerLimit() + increment;
+                    contrastAdjustmentFilter.setLowerLimit(newValue);
+
+                } else if (name.startsWith("upper")) {
+                    float newValue = contrastAdjustmentFilter.getUpperLimit() + increment;
+                    contrastAdjustmentFilter.setUpperLimit(newValue);
+
+                } else if (name.startsWith("re")) {
+                    float newValue = contrastAdjustmentFilter.getRedExponent() + increment;
+                    contrastAdjustmentFilter.setRedExponent(newValue);
+
+                } else if (name.startsWith("ge")) {
+                    float newValue = contrastAdjustmentFilter.getGreenExponent() + increment;
+                    contrastAdjustmentFilter.setGreenExponent(newValue);
+
+                } else if (name.startsWith("be")) {
+                    float newValue = contrastAdjustmentFilter.getBlueExponent() + increment;
+                    contrastAdjustmentFilter.setBlueExponent(newValue);
+
+                } else if (name.startsWith("rs")) {
+                    float newValue = contrastAdjustmentFilter.getRedScale() + increment;
+                    contrastAdjustmentFilter.setRedScale(newValue);
+
+                } else if (name.startsWith("gs")) {
+                    float newValue = contrastAdjustmentFilter.getGreenScale() + increment;
+                    contrastAdjustmentFilter.setGreenScale(newValue);
+
+                } else if (name.startsWith("bs")) {
+                    float newValue = contrastAdjustmentFilter.getBlueScale() + increment;
+                    contrastAdjustmentFilter.setBlueScale(newValue);
+                }
+            }
+        };
+        /*
+         * Add mappings and listeners for user keypresses.
+         */
+        System.out.println("Press Enter to reset the filter to defaults.");
+        inputManager.addMapping("reset", new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addListener(action, "reset");
+
+        System.out.println("lower limit:     press R to increase, F to decrease");
+        inputManager.addMapping("lower+", new KeyTrigger(KeyInput.KEY_R));
+        inputManager.addMapping("lower-", new KeyTrigger(KeyInput.KEY_F));
+        inputManager.addListener(analog, "lower+", "lower-");
+
+        System.out.println("upper limit:     press T to increase, G to decrease");
+        inputManager.addMapping("upper+", new KeyTrigger(KeyInput.KEY_T));
+        inputManager.addMapping("upper-", new KeyTrigger(KeyInput.KEY_G));
+        inputManager.addListener(analog, "upper+", "upper-");
+
+        System.out.println("red exponent:    press Y to increase, H to decrease");
+        inputManager.addMapping("re+", new KeyTrigger(KeyInput.KEY_Y));
+        inputManager.addMapping("re-", new KeyTrigger(KeyInput.KEY_H));
+        inputManager.addListener(analog, "re+", "re-");
+
+        System.out.println("green exponent:  press U to increase, J to decrease");
+        inputManager.addMapping("ge+", new KeyTrigger(KeyInput.KEY_U));
+        inputManager.addMapping("ge-", new KeyTrigger(KeyInput.KEY_J));
+        inputManager.addListener(analog, "ge+", "ge-");
+
+        System.out.println("blue exponent:   press I to increase, K to decrease");
+        inputManager.addMapping("be+", new KeyTrigger(KeyInput.KEY_I));
+        inputManager.addMapping("be-", new KeyTrigger(KeyInput.KEY_K));
+        inputManager.addListener(analog, "be+", "be-");
+
+        System.out.println("red scale:       press O to increase, L to decrease");
+        inputManager.addMapping("rs+", new KeyTrigger(KeyInput.KEY_O));
+        inputManager.addMapping("rs-", new KeyTrigger(KeyInput.KEY_L));
+        inputManager.addListener(analog, "rs+", "rs-");
+
+        System.out.println("green scale:     press P to increase, ; to decrease");
+        inputManager.addMapping("gs+", new KeyTrigger(KeyInput.KEY_P));
+        inputManager.addMapping("gs-", new KeyTrigger(KeyInput.KEY_SEMICOLON));
+        inputManager.addListener(analog, "gs+", "gs-");
+
+        System.out.println("blue scale:      press [ to increase, ' to decrease");
+        inputManager.addMapping("bs+", new KeyTrigger(KeyInput.KEY_LBRACKET));
+        inputManager.addMapping("bs-", new KeyTrigger(KeyInput.KEY_APOSTROPHE));
+        inputManager.addListener(analog, "bs+", "bs-");
+
+        System.out.println();
+    }
+}

BIN
jme3-testdata/src/main/resources/Textures/Sky/Earth/Earth.jpg