Browse Source

Merge branch 'jMonkeyEngine:master' into capdevon-TestAudioDirectional

Wyatt Gillette 4 months ago
parent
commit
18063ee989

+ 153 - 53
jme3-core/src/main/java/com/jme3/audio/Environment.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -34,49 +34,80 @@ package com.jme3.audio;
 import com.jme3.math.FastMath;
 import com.jme3.math.FastMath;
 
 
 /**
 /**
- * Audio environment, for reverb effects.
+ * Represents an audio environment, primarily used to define reverb effects.
+ * This class provides parameters that correspond to the properties controllable
+ * through the OpenAL EFX (Environmental Effects Extension) library.
+ * By adjusting these parameters, developers can simulate various acoustic spaces
+ * like rooms, caves, and concert halls, adding depth and realism to the audio experience.
+ *
  * @author Kirill
  * @author Kirill
  */
  */
 public class Environment {
 public class Environment {
 
 
-    private float airAbsorbGainHf   = 0.99426f;
+    /** High-frequency air absorption gain (0.0f to 1.0f). */
+    private float airAbsorbGainHf = 0.99426f;
+    /** Factor controlling room effect rolloff with distance. */
     private float roomRolloffFactor = 0;
     private float roomRolloffFactor = 0;
-
-    private float decayTime         = 1.49f;
-    private float decayHFRatio      = 0.54f;
-
-    private float density           = 1.0f;
-    private float diffusion         = 0.3f;
-
-    private float gain              = 0.316f;
-    private float gainHf            = 0.022f;
-
-    private float lateReverbDelay   = 0.088f;
-    private float lateReverbGain    = 0.768f;
-
-    private float reflectDelay      = 0.162f;
-    private float reflectGain       = 0.052f;
-
-    private boolean decayHfLimit    = true;
-
-    public static final Environment Garage, Dungeon, Cavern, AcousticLab, Closet;
-
-    static {
-        Garage = new Environment(1, 1, 1, 1, .9f, .5f, .751f, .0039f, .661f, .0137f);
-        Dungeon = new Environment(.75f, 1, 1, .75f, 1.6f, 1, 0.95f, 0.0026f, 0.93f, 0.0103f);
-        Cavern = new Environment(.5f, 1, 1, .5f, 2.25f, 1, .908f, .0103f, .93f, .041f);
-        AcousticLab = new Environment(.5f, 1, 1, 1, .28f, 1, .87f, .002f, .81f, .008f);
-        Closet = new Environment(1, 1, 1, 1, .15f, 1, .6f, .0025f, .5f, .0006f);
-    }
-
+    /** Overall decay time of the reverberation (in seconds). */
+    private float decayTime = 1.49f;
+    /** Ratio of high-frequency decay time to overall decay time (0.0f to 1.0f). */
+    private float decayHFRatio = 0.54f;
+    /** Density of the medium affecting reverb smoothness (0.0f to 1.0f). */
+    private float density = 1.0f;
+    /** Diffusion of reflections affecting echo distinctness (0.0f to 1.0f). */
+    private float diffusion = 0.3f;
+    /** Overall gain of the environment effect (linear scale). */
+    private float gain = 0.316f;
+    /** High-frequency gain of the environment effect (linear scale). */
+    private float gainHf = 0.022f;
+    /** Delay time for late reverberation relative to early reflections (in seconds). */
+    private float lateReverbDelay = 0.088f;
+    /** Gain of the late reverberation (linear scale). */
+    private float lateReverbGain = 0.768f;
+    /** Delay time for the initial reflections (in seconds). */
+    private float reflectDelay = 0.162f;
+    /** Gain of the initial reflections (linear scale). */
+    private float reflectGain = 0.052f;
+    /** Flag limiting high-frequency decay by the overall decay time. */
+    private boolean decayHfLimit = true;
+
+    public static final Environment Garage = new Environment(
+            1, 1, 1, 1, .9f, .5f, .751f, .0039f, .661f, .0137f);
+    public static final Environment Dungeon = new Environment(
+            .75f, 1, 1, .75f, 1.6f, 1, 0.95f, 0.0026f, 0.93f, 0.0103f);
+    public static final Environment Cavern = new Environment(
+            .5f, 1, 1, .5f, 2.25f, 1, .908f, .0103f, .93f, .041f);
+    public static final Environment AcousticLab = new Environment(
+            .5f, 1, 1, 1, .28f, 1, .87f, .002f, .81f, .008f);
+    public static final Environment Closet = new Environment(
+            1, 1, 1, 1, .15f, 1, .6f, .0025f, .5f, .0006f);
+
+    /**
+     * Utility method to convert an EAX decibel value to an amplitude factor.
+     * EAX often expresses gain and attenuation in decibels scaled by 1000.
+     * This method performs the reverse of that conversion to obtain a linear
+     * amplitude value suitable for OpenAL.
+     *
+     * @param eaxDb The EAX decibel value (scaled by 1000).
+     * @return The corresponding amplitude factor.
+     */
     private static float eaxDbToAmp(float eaxDb) {
     private static float eaxDbToAmp(float eaxDb) {
         float dB = eaxDb / 2000f;
         float dB = eaxDb / 2000f;
         return FastMath.pow(10f, dB);
         return FastMath.pow(10f, dB);
     }
     }
 
 
+    /**
+     * Constructs a new, default {@code Environment}. The default values are
+     * typically chosen to represent a neutral or common acoustic space.
+     */
     public Environment() {
     public Environment() {
     }
     }
 
 
+    /**
+     * Creates a new {@code Environment} as a copy of the provided {@code Environment}.
+     *
+     * @param source The {@code Environment} to copy the settings from.
+     */
     public Environment(Environment source) {
     public Environment(Environment source) {
         this.airAbsorbGainHf = source.airAbsorbGainHf;
         this.airAbsorbGainHf = source.airAbsorbGainHf;
         this.roomRolloffFactor = source.roomRolloffFactor;
         this.roomRolloffFactor = source.roomRolloffFactor;
@@ -93,9 +124,24 @@ public class Environment {
         this.decayHfLimit = source.decayHfLimit;
         this.decayHfLimit = source.decayHfLimit;
     }
     }
 
 
+    /**
+     * Creates a new {@code Environment} with the specified parameters. These parameters
+     * directly influence the properties of the reverb effect as managed by OpenAL EFX.
+     *
+     * @param density      The density of the medium.
+     * @param diffusion    The diffusion of the reflections.
+     * @param gain         Overall gain applied to the environment effect.
+     * @param gainHf       High-frequency gain applied to the environment effect.
+     * @param decayTime    The overall decay time of the reflected sound.
+     * @param decayHf      Ratio of high-frequency decay time to the overall decay time.
+     * @param reflectGain  Gain applied to the initial reflections.
+     * @param reflectDelay Delay time for the initial reflections.
+     * @param lateGain     Gain applied to the late reverberation.
+     * @param lateDelay    Delay time for the late reverberation.
+     */
     public Environment(float density, float diffusion, float gain, float gainHf,
     public Environment(float density, float diffusion, float gain, float gainHf,
-                       float decayTime, float decayHf, float reflectGain,
-                       float reflectDelay, float lateGain, float lateDelay) {
+                       float decayTime, float decayHf, float reflectGain, float reflectDelay,
+                       float lateGain, float lateDelay) {
         this.decayTime = decayTime;
         this.decayTime = decayTime;
         this.decayHFRatio = decayHf;
         this.decayHFRatio = decayHf;
         this.density = density;
         this.density = density;
@@ -108,6 +154,16 @@ public class Environment {
         this.reflectGain = reflectGain;
         this.reflectGain = reflectGain;
     }
     }
 
 
+    /**
+     * Creates a new {@code Environment} by interpreting an array of 28 float values
+     * as an EAX preset. This constructor attempts to map the EAX preset values to
+     * the corresponding OpenAL EFX parameters. Note that not all EAX parameters
+     * have a direct equivalent in standard OpenAL EFX, so some values might be
+     * approximated or ignored.
+     *
+     * @param e An array of 28 float values representing an EAX preset.
+     * @throws IllegalArgumentException If the provided array does not have a length of 28.
+     */
     public Environment(float[] e) {
     public Environment(float[] e) {
         if (e.length != 28)
         if (e.length != 28)
             throw new IllegalArgumentException("Not an EAX preset");
             throw new IllegalArgumentException("Not an EAX preset");
@@ -254,27 +310,71 @@ public class Environment {
     }
     }
 
 
     @Override
     @Override
-    public boolean equals(Object env2) {
-        if (env2 == null)
+    public boolean equals(Object obj) {
+
+        if (!(obj instanceof Environment))
             return false;
             return false;
-        if (env2 == this)
+
+        if (obj == this)
             return true;
             return true;
-        if (!(env2 instanceof Environment))
-            return false;
 
 
-        Environment e2 = (Environment) env2;
-        return (e2.airAbsorbGainHf == airAbsorbGainHf
-                && e2.decayHFRatio == decayHFRatio
-                && e2.decayHfLimit == decayHfLimit
-                && e2.decayTime == decayTime
-                && e2.density == density
-                && e2.diffusion == diffusion
-                && e2.gain == gain
-                && e2.gainHf == gainHf
-                && e2.lateReverbDelay == lateReverbDelay
-                && e2.lateReverbGain == lateReverbGain
-                && e2.reflectDelay == reflectDelay
-                && e2.reflectGain == reflectGain
-                && e2.roomRolloffFactor == roomRolloffFactor);
-    } 
+        Environment other = (Environment) obj;
+        float epsilon = 1e-6f;
+
+        float[] thisFloats = {
+                this.airAbsorbGainHf,
+                this.decayHFRatio,
+                this.decayTime,
+                this.density,
+                this.diffusion,
+                this.gain,
+                this.gainHf,
+                this.lateReverbDelay,
+                this.lateReverbGain,
+                this.reflectDelay,
+                this.reflectGain,
+                this.roomRolloffFactor
+        };
+
+        float[] otherFloats = {
+                other.airAbsorbGainHf,
+                other.decayHFRatio,
+                other.decayTime,
+                other.density,
+                other.diffusion,
+                other.gain,
+                other.gainHf,
+                other.lateReverbDelay,
+                other.lateReverbGain,
+                other.reflectDelay,
+                other.reflectGain,
+                other.roomRolloffFactor
+        };
+
+        for (int i = 0; i < thisFloats.length; i++) {
+            if (Math.abs(thisFloats[i] - otherFloats[i]) >= epsilon) {
+                return false;
+            }
+        }
+
+        return this.decayHfLimit == other.decayHfLimit;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = (airAbsorbGainHf != +0.0f ? Float.floatToIntBits(airAbsorbGainHf) : 0);
+        result = 31 * result + (roomRolloffFactor != +0.0f ? Float.floatToIntBits(roomRolloffFactor) : 0);
+        result = 31 * result + (decayTime != +0.0f ? Float.floatToIntBits(decayTime) : 0);
+        result = 31 * result + (decayHFRatio != +0.0f ? Float.floatToIntBits(decayHFRatio) : 0);
+        result = 31 * result + (density != +0.0f ? Float.floatToIntBits(density) : 0);
+        result = 31 * result + (diffusion != +0.0f ? Float.floatToIntBits(diffusion) : 0);
+        result = 31 * result + (gain != +0.0f ? Float.floatToIntBits(gain) : 0);
+        result = 31 * result + (gainHf != +0.0f ? Float.floatToIntBits(gainHf) : 0);
+        result = 31 * result + (lateReverbDelay != +0.0f ? Float.floatToIntBits(lateReverbDelay) : 0);
+        result = 31 * result + (lateReverbGain != +0.0f ? Float.floatToIntBits(lateReverbGain) : 0);
+        result = 31 * result + (reflectDelay != +0.0f ? Float.floatToIntBits(reflectDelay) : 0);
+        result = 31 * result + (reflectGain != +0.0f ? Float.floatToIntBits(reflectGain) : 0);
+        result = 31 * result + (decayHfLimit ? 1 : 0);
+        return result;
+    }
 }
 }

+ 1 - 1
jme3-core/src/main/resources/Common/MatDefs/Light/PBRLighting.vert

@@ -76,7 +76,7 @@ void main(){
        texCoord2 = inTexCoord2;
        texCoord2 = inTexCoord2;
     #endif
     #endif
 
 
-    wPosition = (g_WorldMatrix * vec4(inPosition, 1.0)).xyz;
+    wPosition = TransformWorld(modelSpacePos).xyz;
     wNormal  = TransformWorldNormal(modelSpaceNorm);
     wNormal  = TransformWorldNormal(modelSpaceNorm);
     
     
     wTangent = vec4(TransformWorldNormal(modelSpaceTan),inTangent.w);
     wTangent = vec4(TransformWorldNormal(modelSpaceTan),inTangent.w);

+ 60 - 40
jme3-examples/src/main/java/jme3test/audio/TestAmbient.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -39,50 +39,70 @@ import com.jme3.material.Material;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.ColorRGBA;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Geometry;
 import com.jme3.scene.Geometry;
-import com.jme3.scene.shape.Box;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.debug.Grid;
+import com.jme3.scene.shape.Sphere;
 
 
 public class TestAmbient extends SimpleApplication {
 public class TestAmbient extends SimpleApplication {
 
 
-  public static void main(String[] args) {
-    TestAmbient test = new TestAmbient();
-    test.start();
-  }
+    public static void main(String[] args) {
+        TestAmbient test = new TestAmbient();
+        test.start();
+    }
 
 
-  @Override
-  public void simpleInitApp() {
-    float[] eax = new float[]{15, 38.0f, 0.300f, -1000, -3300, 0,
-      1.49f, 0.54f, 1.00f, -2560, 0.162f, 0.00f, 0.00f,
-      0.00f, -229, 0.088f, 0.00f, 0.00f, 0.00f, 0.125f, 1.000f,
-      0.250f, 0.000f, -5.0f, 5000.0f, 250.0f, 0.00f, 0x3f};
-    Environment env = new Environment(eax);
-    audioRenderer.setEnvironment(env);
+    private final float[] eax = {
+            15, 38.0f, 0.300f, -1000, -3300, 0,
+            1.49f, 0.54f, 1.00f, -2560, 0.162f, 0.00f, 0.00f,
+            0.00f, -229, 0.088f, 0.00f, 0.00f, 0.00f, 0.125f, 1.000f,
+            0.250f, 0.000f, -5.0f, 5000.0f, 250.0f, 0.00f, 0x3f
+    };
 
 
-    AudioNode waves = new AudioNode(assetManager,
-            "Sound/Environment/Ocean Waves.ogg", DataType.Buffer);
-    waves.setPositional(true);
-    waves.setLocalTranslation(new Vector3f(0, 0,0));
-    waves.setMaxDistance(100);
-    waves.setRefDistance(5);
+    @Override
+    public void simpleInitApp() {
+        configureCamera();
 
 
-    AudioNode nature = new AudioNode(assetManager,
-            "Sound/Environment/Nature.ogg", DataType.Stream);
-    nature.setPositional(false);
-    nature.setVolume(3);
-    
-    waves.playInstance();
-    nature.play();
-    
-    // just a blue box to mark the spot
-    Box box1 = new Box(.5f, .5f, .5f);
-    Geometry player = new Geometry("Player", box1);
-    Material mat1 = new Material(assetManager,
-            "Common/MatDefs/Misc/Unshaded.j3md");
-    mat1.setColor("Color", ColorRGBA.Blue);
-    player.setMaterial(mat1);
-    rootNode.attachChild(player);
-  }
+        Environment env = new Environment(eax);
+        audioRenderer.setEnvironment(env);
 
 
-  @Override
-  public void simpleUpdate(float tpf) {
-  }
+        AudioNode waves = new AudioNode(assetManager,
+                "Sound/Environment/Ocean Waves.ogg", DataType.Buffer);
+        waves.setPositional(true);
+        waves.setLooping(true);
+        waves.setReverbEnabled(true);
+        rootNode.attachChild(waves);
+
+        AudioNode nature = new AudioNode(assetManager,
+                "Sound/Environment/Nature.ogg", DataType.Stream);
+        nature.setPositional(false);
+        nature.setLooping(true);
+        nature.setVolume(3);
+        rootNode.attachChild(nature);
+
+        waves.play();
+        nature.play();
+
+        // just a blue sphere to mark the spot
+        Geometry marker = makeShape("Marker", new Sphere(16, 16, 1f), ColorRGBA.Blue);
+        waves.attachChild(marker);
+
+        Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 2), ColorRGBA.Gray);
+        grid.center().move(0, 0, 0);
+        rootNode.attachChild(grid);
+    }
+
+    private void configureCamera() {
+        flyCam.setMoveSpeed(25f);
+        flyCam.setDragToRotate(true);
+
+        cam.setLocation(Vector3f.UNIT_XYZ.mult(5f));
+        cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
+    }
+
+    private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) {
+        Geometry geo = new Geometry(name, mesh);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", color);
+        geo.setMaterial(mat);
+        return geo;
+    }
 }
 }

+ 63 - 0
jme3-examples/src/main/java/jme3test/audio/TestAudioDeviceDisconnect.java

@@ -0,0 +1,63 @@
+package jme3test.audio;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.audio.AudioData;
+import com.jme3.audio.AudioNode;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.Trigger;
+
+/**
+ * This test demonstrates that destroying and recreating the OpenAL Context
+ * upon device disconnection is not an optimal solution.
+ *
+ * As shown, AudioNode instances playing in a loop cease to play after a device disconnection
+ * and would require explicit restarting.
+ *
+ * This test serves solely to highlight this issue,
+ * which should be addressed with a more robust solution within
+ * the ALAudioRenderer class in a dedicated future pull request on Git.
+ */
+public class TestAudioDeviceDisconnect extends SimpleApplication implements ActionListener {
+
+    public static void main(String[] args) {
+        TestAudioDeviceDisconnect test = new TestAudioDeviceDisconnect();
+        test.start();
+    }
+
+    private AudioNode audioSource;
+
+    @Override
+    public void simpleInitApp() {
+        audioSource = new AudioNode(assetManager,
+                "Sound/Environment/Ocean Waves.ogg", AudioData.DataType.Buffer);
+        audioSource.setName("Waves");
+        audioSource.setLooping(true);
+        rootNode.attachChild(audioSource);
+
+        audioSource.play();
+
+        registerInputMappings();
+    }
+
+    @Override
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if (!isPressed) return;
+
+        if (name.equals("play")) {
+            // re-play active sounds
+            audioSource.play();
+        }
+    }
+
+    private void registerInputMappings() {
+        addMapping("play", new KeyTrigger(KeyInput.KEY_SPACE));
+    }
+
+    private void addMapping(String mappingName, Trigger... triggers) {
+        inputManager.addMapping(mappingName, triggers);
+        inputManager.addListener(this, mappingName);
+    }
+
+}

+ 150 - 14
jme3-examples/src/main/java/jme3test/audio/TestOgg.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -37,34 +37,170 @@ import com.jme3.audio.AudioData.DataType;
 import com.jme3.audio.AudioNode;
 import com.jme3.audio.AudioNode;
 import com.jme3.audio.AudioSource;
 import com.jme3.audio.AudioSource;
 import com.jme3.audio.LowPassFilter;
 import com.jme3.audio.LowPassFilter;
+import com.jme3.font.BitmapText;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.Trigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.FastMath;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.debug.Grid;
+import com.jme3.scene.shape.Sphere;
 
 
-public class TestOgg extends SimpleApplication {
+/**
+ *
+ * @author capdevon
+ */
+public class TestOgg extends SimpleApplication implements ActionListener {
 
 
+    private final StringBuilder sb = new StringBuilder();
+    private int frameCount = 0;
+    private BitmapText bmp;
     private AudioNode audioSource;
     private AudioNode audioSource;
+    private float volume = 1.0f;
+    private float pitch = 1.0f;
 
 
-    public static void main(String[] args){
+    /**
+     * ### Filters ###
+     * Changing a parameter value in the Filter Object after it has been attached to a Source will not
+     * affect the Source. To update the filter(s) used on a Source, an application must update the
+     * parameters of a Filter object and then re-attach it to the Source.
+     */
+    private final LowPassFilter dryFilter = new LowPassFilter(1f, .1f);
+
+    public static void main(String[] args) {
         TestOgg test = new TestOgg();
         TestOgg test = new TestOgg();
         test.start();
         test.start();
     }
     }
 
 
     @Override
     @Override
-    public void simpleInitApp(){
-        System.out.println("Playing without filter");
+    public void simpleInitApp() {
+        configureCamera();
+
+        bmp = createLabelText(10, 20, "<placeholder>");
+
+        // just a blue sphere to mark the spot
+        Geometry marker = makeShape("Marker", new Sphere(16, 16, 1f), ColorRGBA.Blue);
+        rootNode.attachChild(marker);
+
+        Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 2), ColorRGBA.Gray);
+        grid.center().move(0, 0, 0);
+        rootNode.attachChild(grid);
+
         audioSource = new AudioNode(assetManager, "Sound/Effects/Foot steps.ogg", DataType.Buffer);
         audioSource = new AudioNode(assetManager, "Sound/Effects/Foot steps.ogg", DataType.Buffer);
+        audioSource.setName("Foot steps");
+        audioSource.setLooping(true);
+        audioSource.setVolume(volume);
+        audioSource.setPitch(pitch);
+        audioSource.setMaxDistance(100);
+        audioSource.setRefDistance(5);
         audioSource.play();
         audioSource.play();
+        rootNode.attachChild(audioSource);
+
+        registerInputMappings();
+    }
+
+    private void configureCamera() {
+        flyCam.setMoveSpeed(25f);
+        flyCam.setDragToRotate(true);
+
+        cam.setLocation(Vector3f.UNIT_XYZ.mult(20f));
+        cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        frameCount++;
+        if (frameCount % 10 == 0) {
+            frameCount = 0;
+
+            sb.append("Audio: ").append(audioSource.getName()).append("\n");
+            sb.append(audioSource.getAudioData()).append("\n");
+            sb.append("Looping: ").append(audioSource.isLooping()).append("\n");
+            sb.append("Volume: ").append(String.format("%.2f", audioSource.getVolume())).append("\n");
+            sb.append("Pitch: ").append(String.format("%.2f", audioSource.getPitch())).append("\n");
+            sb.append("Positional: ").append(audioSource.isPositional()).append("\n");
+            sb.append("MaxDistance: ").append(audioSource.getMaxDistance()).append("\n");
+            sb.append("RefDistance: ").append(audioSource.getRefDistance()).append("\n");
+            sb.append("Status: ").append(audioSource.getStatus()).append("\n");
+            sb.append("SourceId: ").append(audioSource.getChannel()).append("\n");
+            sb.append("DryFilter: ").append(audioSource.getDryFilter() != null).append("\n");
+            sb.append("FilterId: ").append(dryFilter.getId()).append("\n");
+
+            bmp.setText(sb.toString());
+            sb.setLength(0);
+        }
     }
     }
 
 
     @Override
     @Override
-    public void simpleUpdate(float tpf){
-        if (audioSource.getStatus() != AudioSource.Status.Playing){
-            audioRenderer.deleteAudioData(audioSource.getAudioData());
-
-            System.out.println("Playing with low pass filter");
-            audioSource = new AudioNode(assetManager, "Sound/Effects/Foot steps.ogg", DataType.Buffer);
-            audioSource.setDryFilter(new LowPassFilter(1f, .1f));
-            audioSource.setVolume(3);
-            audioSource.play();
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if (!isPressed) return;
+
+        if (name.equals("togglePlayPause")) {
+            if (audioSource.getStatus() != AudioSource.Status.Playing) {
+                audioSource.play();
+            } else {
+                audioSource.stop();
+            }
+        } else if (name.equals("togglePositional")) {
+            boolean positional = audioSource.isPositional();
+            audioSource.setPositional(!positional);
+
+        } else if (name.equals("dryFilter")) {
+            boolean hasFilter = audioSource.getDryFilter() != null;
+            audioSource.setDryFilter(hasFilter ? null : dryFilter);
+
+        } else if (name.equals("Volume+")) {
+            volume = FastMath.clamp(volume + 0.1f, 0, 5f);
+            audioSource.setVolume(volume);
+
+        } else if (name.equals("Volume-")) {
+            volume = FastMath.clamp(volume - 0.1f, 0, 5f);
+            audioSource.setVolume(volume);
+
+        } else if (name.equals("Pitch+")) {
+            pitch = FastMath.clamp(pitch + 0.1f, 0.5f, 2f);
+            audioSource.setPitch(pitch);
+
+        } else if (name.equals("Pitch-")) {
+            pitch = FastMath.clamp(pitch - 0.1f, 0.5f, 2f);
+            audioSource.setPitch(pitch);
         }
         }
     }
     }
 
 
+    private void registerInputMappings() {
+        addMapping("togglePlayPause", new KeyTrigger(KeyInput.KEY_P));
+        addMapping("togglePositional", new KeyTrigger(KeyInput.KEY_RETURN));
+        addMapping("dryFilter", new KeyTrigger(KeyInput.KEY_SPACE));
+        addMapping("Volume+", new KeyTrigger(KeyInput.KEY_I));
+        addMapping("Volume-", new KeyTrigger(KeyInput.KEY_K));
+        addMapping("Pitch+", new KeyTrigger(KeyInput.KEY_J));
+        addMapping("Pitch-", new KeyTrigger(KeyInput.KEY_L));
+    }
+
+    private void addMapping(String mappingName, Trigger... triggers) {
+        inputManager.addMapping(mappingName, triggers);
+        inputManager.addListener(this, mappingName);
+    }
+
+    private BitmapText createLabelText(int x, int y, String text) {
+        BitmapText bmp = new BitmapText(guiFont);
+        bmp.setText(text);
+        bmp.setLocalTranslation(x, settings.getHeight() - y, 0);
+        bmp.setColor(ColorRGBA.Red);
+        guiNode.attachChild(bmp);
+        return bmp;
+    }
+
+    private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) {
+        Geometry geo = new Geometry(name, mesh);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", color);
+        geo.setMaterial(mat);
+        return geo;
+    }
 }
 }

+ 136 - 43
jme3-examples/src/main/java/jme3test/audio/TestReverb.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -35,50 +35,143 @@ import com.jme3.app.SimpleApplication;
 import com.jme3.audio.AudioData;
 import com.jme3.audio.AudioData;
 import com.jme3.audio.AudioNode;
 import com.jme3.audio.AudioNode;
 import com.jme3.audio.Environment;
 import com.jme3.audio.Environment;
+import com.jme3.audio.LowPassFilter;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
+import com.jme3.input.controls.Trigger;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
 import com.jme3.math.FastMath;
 import com.jme3.math.FastMath;
 import com.jme3.math.Vector3f;
 import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.Mesh;
+import com.jme3.scene.debug.Grid;
+import com.jme3.scene.shape.Sphere;
 
 
-public class TestReverb extends SimpleApplication {
-
-  private AudioNode audioSource;
-  private float time = 0;
-  private float nextTime = 1;
-
-  public static void main(String[] args) {
-    TestReverb test = new TestReverb();
-    test.start();
-  }
-
-  @Override
-  public void simpleInitApp() {
-    audioSource = new AudioNode(assetManager, "Sound/Effects/Bang.wav",
-            AudioData.DataType.Buffer);
-
-    float[] eax = new float[]{15, 38.0f, 0.300f, -1000, -3300, 0,
-      1.49f, 0.54f, 1.00f, -2560, 0.162f, 0.00f, 0.00f, 0.00f,
-      -229, 0.088f, 0.00f, 0.00f, 0.00f, 0.125f, 1.000f, 0.250f,
-      0.000f, -5.0f, 5000.0f, 250.0f, 0.00f, 0x3f};
-    audioRenderer.setEnvironment(new Environment(eax));
-    Environment env = Environment.Cavern;
-    audioRenderer.setEnvironment(env);
-  }
-
-  @Override
-  public void simpleUpdate(float tpf) {
-    time += tpf;
-
-    if (time > nextTime) {
-      Vector3f v = new Vector3f();
-      v.setX(FastMath.nextRandomFloat());
-      v.setY(FastMath.nextRandomFloat());
-      v.setZ(FastMath.nextRandomFloat());
-      v.multLocal(40, 2, 40);
-      v.subtractLocal(20, 1, 20);
-
-      audioSource.setLocalTranslation(v);
-      audioSource.playInstance();
-      time = 0;
-      nextTime = FastMath.nextRandomFloat() * 2 + 0.5f;
+/**
+ * @author capdevon
+ */
+public class TestReverb extends SimpleApplication implements ActionListener {
+
+    public static void main(String[] args) {
+        TestReverb app = new TestReverb();
+        app.start();
+    }
+
+    private AudioNode audioSource;
+    private float time = 0;
+    private float nextTime = 1;
+
+    /**
+     * ### Effects ###
+     * Changing a parameter value in the Effect Object after it has been attached to the Auxiliary Effect
+     * Slot will not affect the effect in the effect slot. To update the parameters of the effect in the effect
+     * slot, an application must update the parameters of an Effect object and then re-attach it to the
+     * Auxiliary Effect Slot.
+     */
+    private int index = 0;
+    private final Environment[] environments = {
+            Environment.Cavern,
+            Environment.AcousticLab,
+            Environment.Closet,
+            Environment.Dungeon,
+            Environment.Garage
+    };
+
+    @Override
+    public void simpleInitApp() {
+
+        configureCamera();
+
+        // Activate the Environment preset
+        audioRenderer.setEnvironment(environments[index]);
+
+        // Activate 3D audio
+        audioSource = new AudioNode(assetManager, "Sound/Effects/Bang.wav", AudioData.DataType.Buffer);
+        audioSource.setLooping(false);
+        audioSource.setVolume(1.2f);
+        audioSource.setPositional(true);
+        audioSource.setMaxDistance(100);
+        audioSource.setRefDistance(5);
+        audioSource.setReverbEnabled(true);
+        audioSource.setReverbFilter(new LowPassFilter(1f, 1f));
+        rootNode.attachChild(audioSource);
+
+        Geometry marker = makeShape("Marker", new Sphere(16, 16, 1f), ColorRGBA.Red);
+        audioSource.attachChild(marker);
+
+        Geometry grid = makeShape("DebugGrid", new Grid(21, 21, 4), ColorRGBA.Blue);
+        grid.center().move(0, 0, 0);
+        rootNode.attachChild(grid);
+
+        registerInputMappings();
+    }
+
+    private void configureCamera() {
+        flyCam.setMoveSpeed(50f);
+        flyCam.setDragToRotate(true);
+
+        cam.setLocation(Vector3f.UNIT_XYZ.mult(50f));
+        cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
+    }
+
+    private Geometry makeShape(String name, Mesh mesh, ColorRGBA color) {
+        Geometry geo = new Geometry(name, mesh);
+        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
+        mat.setColor("Color", color);
+        geo.setMaterial(mat);
+        return geo;
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+        time += tpf;
+
+        if (time > nextTime) {
+            time = 0;
+            nextTime = FastMath.nextRandomFloat() * 2 + 0.5f;
+
+            Vector3f position = getRandomPosition();
+            audioSource.setLocalTranslation(position);
+            audioSource.playInstance();
+        }
+    }
+
+    private Vector3f getRandomPosition() {
+        float x = FastMath.nextRandomFloat();
+        float y = FastMath.nextRandomFloat();
+        float z = FastMath.nextRandomFloat();
+        Vector3f vec = new Vector3f(x, y, z);
+        vec.multLocal(40, 2, 40);
+        vec.subtractLocal(20, 1, 20);
+        return vec;
+    }
+
+    @Override
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if (!isPressed) return;
+
+        if (name.equals("toggleReverbEnabled")) {
+            boolean reverbEnabled = !audioSource.isReverbEnabled();
+            audioSource.setReverbEnabled(reverbEnabled);
+            System.out.println("reverbEnabled: " + reverbEnabled);
+
+        } else if (name.equals("nextEnvironment")) {
+            index = (index + 1) % environments.length;
+            audioRenderer.setEnvironment(environments[index]);
+            System.out.println("Next Environment Index: " + index);
+        }
+    }
+
+    private void registerInputMappings() {
+        addMapping("toggleReverbEnabled", new KeyTrigger(KeyInput.KEY_SPACE));
+        addMapping("nextEnvironment", new KeyTrigger(KeyInput.KEY_N));
+    }
+
+    private void addMapping(String mappingName, Trigger... triggers) {
+        inputManager.addMapping(mappingName, triggers);
+        inputManager.addListener(this, mappingName);
     }
     }
-  }
+
 }
 }

+ 92 - 0
jme3-examples/src/main/java/jme3test/scene/instancing/TestInstanceNodeWithPbr.java

@@ -0,0 +1,92 @@
+package jme3test.scene.instancing;
+
+import java.util.Locale;
+
+import com.jme3.app.SimpleApplication;
+import com.jme3.font.BitmapText;
+import com.jme3.light.DirectionalLight;
+import com.jme3.material.Material;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Vector3f;
+import com.jme3.scene.Geometry;
+import com.jme3.scene.instancing.InstancedNode;
+import com.jme3.scene.shape.Box;
+
+/**
+ * This test specifically validates the corrected PBR rendering when combined
+ * with instancing, as addressed in issue #2435. 
+ * 
+ * It creates an InstancedNode
+ * with a PBR-materialized Box to ensure the fix in PBRLighting.vert correctly
+ * handles world position calculations for instanced geometry.
+ */
+public class TestInstanceNodeWithPbr extends SimpleApplication {
+
+    public static void main(String[] args) {
+        TestInstanceNodeWithPbr app = new TestInstanceNodeWithPbr();
+        app.start();
+    }
+
+    private BitmapText bmp;
+    private Geometry box;
+    private float pos = -5;
+    private float vel = 5;
+    
+    @Override
+    public void simpleInitApp() {
+        configureCamera();
+        bmp = createLabelText(10, 20, "<placeholder>");
+        
+        InstancedNode instancedNode = new InstancedNode("InstancedNode");
+        rootNode.attachChild(instancedNode);
+
+        Box mesh = new Box(0.5f, 0.5f, 0.5f);
+        box = new Geometry("Box", mesh);
+        Material pbrMaterial = createPbrMaterial(ColorRGBA.Red);
+        box.setMaterial(pbrMaterial);
+
+        instancedNode.attachChild(box);
+        instancedNode.instance();
+
+        DirectionalLight light = new DirectionalLight();
+        light.setDirection(new Vector3f(-1, -2, -3).normalizeLocal());
+        rootNode.addLight(light);
+    }
+
+    private Material createPbrMaterial(ColorRGBA color) {
+        Material mat = new Material(assetManager, "Common/MatDefs/Light/PBRLighting.j3md");
+        mat.setColor("BaseColor", color);
+        mat.setFloat("Roughness", 0.8f);
+        mat.setFloat("Metallic", 0.1f);
+        mat.setBoolean("UseInstancing", true);
+        return mat;
+    }
+    
+    private void configureCamera() {
+        flyCam.setMoveSpeed(15f);
+        flyCam.setDragToRotate(true);
+
+        cam.setLocation(Vector3f.UNIT_XYZ.mult(12));
+        cam.lookAt(Vector3f.ZERO, Vector3f.UNIT_Y);
+    }
+    
+    private BitmapText createLabelText(int x, int y, String text) {
+        BitmapText bmp = new BitmapText(guiFont);
+        bmp.setText(text);
+        bmp.setLocalTranslation(x, settings.getHeight() - y, 0);
+        bmp.setColor(ColorRGBA.Red);
+        guiNode.attachChild(bmp);
+        return bmp;
+    }
+    
+    @Override
+    public void simpleUpdate(float tpf) {
+        pos += tpf * vel;
+        if (pos < -10f || pos > 10f) {
+            vel *= -1;
+        }
+        box.setLocalTranslation(pos, 0f, 0f);
+        bmp.setText(String.format(Locale.ENGLISH, "BoxPosition: (%.2f, %.1f, %.1f)", pos, 0f, 0f));
+    }
+
+}

+ 2 - 2
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglCanvas.java

@@ -1,5 +1,5 @@
 /*
 /*
- * Copyright (c) 2009-2024 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  * All rights reserved.
  *
  *
  * Redistribution and use in source and binary forms, with or without
  * Redistribution and use in source and binary forms, with or without
@@ -677,7 +677,7 @@ public class LwjglCanvas extends LwjglWindow implements JmeCanvasContext, Runnab
             sb.append('\n')
             sb.append('\n')
               .append(" *  Red Size: ").append(glData.redSize);
               .append(" *  Red Size: ").append(glData.redSize);
             sb.append('\n')
             sb.append('\n')
-              .append(" *  Rreen Size: ").append(glData.greenSize);
+              .append(" *  Green Size: ").append(glData.greenSize);
             sb.append('\n')
             sb.append('\n')
               .append(" *  Blue Size: ").append(glData.blueSize);
               .append(" *  Blue Size: ").append(glData.blueSize);
             sb.append('\n')
             sb.append('\n')