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

Merge branch 'master' into platform

Night Rider 2 месяцев назад
Родитель
Сommit
70c8a10e4f

+ 17 - 19
jme3-core/src/main/java/com/jme3/cinematic/events/MotionEvent.java

@@ -52,8 +52,8 @@ import com.jme3.util.clone.JmeCloneable;
 import java.io.IOException;
 
 /**
- * A MotionEvent is a control over the spatial that manages the position and direction of the spatial while following a motion Path.
- *
+ * A MotionEvent is a control over the spatial that manages
+ * the position and direction of the spatial while following a motion Path.
  * You must first create a MotionPath and then create a MotionEvent to associate a spatial and the path.
  *
  * @author Nehon
@@ -70,6 +70,7 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     protected Direction directionType = Direction.None;
     protected MotionPath path;
     private boolean isControl = true;
+    private final Quaternion tempRotation = new Quaternion();
     /**
      * the distance traveled by the spatial on the path
      */
@@ -79,7 +80,6 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
      * Enum for the different type of target direction behavior.
      */
     public enum Direction {
-
         /**
          * The target stays in the starting direction.
          */
@@ -229,13 +229,13 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     @Override
     public void read(JmeImporter im) throws IOException {
         super.read(im);
-        InputCapsule in = im.getCapsule(this);
-        lookAt = (Vector3f) in.readSavable("lookAt", null);
-        upVector = (Vector3f) in.readSavable("upVector", Vector3f.UNIT_Y);
-        rotation = (Quaternion) in.readSavable("rotation", null);
-        directionType = in.readEnum("directionType", Direction.class, Direction.None);
-        path = (MotionPath) in.readSavable("path", null);
-        spatial = (Spatial) in.readSavable("spatial", null);
+        InputCapsule ic = im.getCapsule(this);
+        lookAt = (Vector3f) ic.readSavable("lookAt", null);
+        upVector = (Vector3f) ic.readSavable("upVector", Vector3f.UNIT_Y);
+        rotation = (Quaternion) ic.readSavable("rotation", null);
+        directionType = ic.readEnum("directionType", Direction.class, Direction.None);
+        path = (MotionPath) ic.readSavable("path", null);
+        spatial = (Spatial) ic.readSavable("spatial", null);
     }
 
     /**
@@ -249,9 +249,8 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
     private void computeTargetDirection() {
         switch (directionType) {
             case Path:
-                Quaternion q = new Quaternion();
-                q.lookAt(direction, upVector);
-                spatial.setLocalRotation(q);
+                tempRotation.lookAt(direction, upVector);
+                spatial.setLocalRotation(tempRotation);
                 break;
             case LookAt:
                 if (lookAt != null) {
@@ -260,10 +259,9 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
                 break;
             case PathAndRotation:
                 if (rotation != null) {
-                    Quaternion q2 = new Quaternion();
-                    q2.lookAt(direction, upVector);
-                    q2.multLocal(rotation);
-                    spatial.setLocalRotation(q2);
+                    tempRotation.lookAt(direction, upVector);
+                    tempRotation.multLocal(rotation);
+                    spatial.setLocalRotation(tempRotation);
                 }
                 break;
             case Rotation:
@@ -272,6 +270,7 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
                 }
                 break;
             case None:
+                // no-op
                 break;
             default:
                 break;
@@ -376,8 +375,7 @@ public class MotionEvent extends AbstractCinematicEvent implements Control, JmeC
 
     /**
      * Sets the direction of the spatial, using the Y axis as the up vector.
-     * Use MotionEvent#setDirection((Vector3f direction,Vector3f upVector) if
-     * you want a custom up vector.
+     * If a custom up vector is desired, use {@link #setDirection(Vector3f, Vector3f)}.
      * This method is used by the motion path.
      *
      * @param direction the desired forward direction (not null, unaffected)

+ 1 - 1
jme3-core/src/main/java/com/jme3/effect/ParticleEmitter.java

@@ -1124,7 +1124,7 @@ public class ParticleEmitter extends Geometry {
         lastPos.set(getWorldTranslation());
 
         //This check avoids a NaN bounds when all the particles are dead during the first update.
-        if (!min.equals(Vector3f.POSITIVE_INFINITY) && !max.equals(Vector3f.NEGATIVE_INFINITY)) {
+        if (Vector3f.isValidVector(min) && Vector3f.isValidVector(max)) {
             BoundingBox bbox = (BoundingBox) this.getMesh().getBound();
             bbox.setMinMax(min, max);
             this.setBoundRefresh();

+ 1 - 0
jme3-core/src/main/java/com/jme3/effect/shapes/EmitterSphereShape.java

@@ -137,6 +137,7 @@ public class EmitterSphereShape implements EmitterShape {
     @Override
     public void getRandomPointAndNormal(Vector3f store, Vector3f normal) {
         this.getRandomPoint(store);
+        normal.set(store).subtractLocal(center).normalizeLocal();
     }
 
     /**

+ 5 - 1
jme3-core/src/main/java/com/jme3/input/JoystickButton.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -50,6 +50,10 @@ public interface JoystickButton {
     public static final String BUTTON_9 = "9";
     public static final String BUTTON_10 = "10";
     public static final String BUTTON_11 = "11";
+    public static final String BUTTON_12 = "12";
+    public static final String BUTTON_13 = "13";
+    public static final String BUTTON_14 = "14";
+    public static final String BUTTON_15 = "15";
 
     /**
      * Assign the mapping name to receive events from the given button index

+ 85 - 63
jme3-core/src/main/java/com/jme3/material/Material.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2024 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -34,12 +34,20 @@ package com.jme3.material;
 import com.jme3.asset.AssetKey;
 import com.jme3.asset.AssetManager;
 import com.jme3.asset.CloneableSmartAsset;
-import com.jme3.export.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
 import com.jme3.light.LightList;
 import com.jme3.material.RenderState.BlendMode;
 import com.jme3.material.RenderState.FaceCullMode;
 import com.jme3.material.TechniqueDef.LightMode;
-import com.jme3.math.*;
+import com.jme3.math.ColorRGBA;
+import com.jme3.math.Matrix4f;
+import com.jme3.math.Vector2f;
+import com.jme3.math.Vector3f;
+import com.jme3.math.Vector4f;
 import com.jme3.renderer.Caps;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.Renderer;
@@ -56,7 +64,11 @@ import com.jme3.util.ListMap;
 import com.jme3.util.SafeArrayList;
 
 import java.io.IOException;
-import java.util.*;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -77,7 +89,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     public static final int SAVABLE_VERSION = 2;
     private static final Logger logger = Logger.getLogger(Material.class.getName());
 
-    private AssetKey key;
+    private AssetKey<?> key;
     private String name;
     private MaterialDef def;
     private ListMap<String, MatParam> paramValues = new ListMap<>();
@@ -90,15 +102,24 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     private int sortingId = -1;
 
     /**
-     * Track bind ids for textures and buffers
-     * Used internally 
+     * Manages and tracks texture and buffer binding units for rendering.
+     * Used internally by the Material class.
      */
     public static class BindUnits {
+        /** The current texture unit counter. */
         public int textureUnit = 0;
+        /** The current buffer unit counter. */
         public int bufferUnit = 0;
     }
     private BindUnits bindUnits = new BindUnits();
 
+    /**
+     * Constructs a new Material instance based on a provided MaterialDef.
+     * The material's parameters will be initialized with default values from the definition.
+     *
+     * @param def The material definition to use (cannot be null).
+     * @throws IllegalArgumentException if def is null.
+     */
     public Material(MaterialDef def) {
         if (def == null) {
             throw new IllegalArgumentException("Material definition cannot be null");
@@ -113,40 +134,48 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
     }
 
-    public Material(AssetManager contentMan, String defName) {
-        this(contentMan.loadAsset(new AssetKey<MaterialDef>(defName)));
+    /**
+     * Constructs a new Material by loading its MaterialDef from the asset manager.
+     *
+     * @param assetManager The asset manager to load the MaterialDef from.
+     * @param defName      The asset path of the .j3md file.
+     */
+    public Material(AssetManager assetManager, String defName) {
+        this(assetManager.loadAsset(new AssetKey<MaterialDef>(defName)));
     }
 
     /**
-     * Do not use this constructor. Serialization purposes only.
+     * For serialization only. Do not use.
      */
     public Material() {
     }
 
     /**
      * Returns the asset key name of the asset from which this material was loaded.
+     * <p>This value will be null unless this material was loaded from a .j3m file.</p>
      *
-     * <p>This value will be <code>null</code> unless this material was loaded
-     * from a .j3m file.
-     *
-     * @return Asset key name of the j3m file
+     * @return Asset key name of the .j3m file, or null if not loaded from a file.
      */
     public String getAssetName() {
         return key != null ? key.getName() : null;
     }
 
     /**
-     * @return the name of the material (not the same as the asset name), the returned value can be null
+     * Returns the user-defined name of the material.
+     * This name is distinct from the asset name and may be null or not unique.
+     *
+     * @return The name of the material, or null.
      */
     public String getName() {
         return name;
     }
 
     /**
-     * This method sets the name of the material.
+     * Sets the user-defined name of the material.
      * The name is not the same as the asset name.
-     * It can be null and there is no guarantee of its uniqueness.
-     * @param name the name of the material
+     * It can be null, and there is no guarantee of its uniqueness.
+     *
+     * @param name The name of the material.
      */
     public void setName(String name) {
         this.name = name;
@@ -228,7 +257,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     }
 
     /**
-     * Compares two materials and returns true if they are equal.
+     * Compares two materials for content equality.
      * This methods compare definition, parameters, additional render states.
      * Since materials are mutable objects, implementing equals() properly is not possible,
      * hence the name contentEquals().
@@ -405,7 +434,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     }
 
     /**
-     * Get the material definition (j3md file info) that <code>this</code>
+     * Get the material definition (.j3md file info) that <code>this</code>
      * material is implementing.
      *
      * @return the material definition this material implements.
@@ -494,7 +523,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     /**
      * Pass a parameter to the material shader.
      *
-     * @param name the name of the parameter defined in the material definition (j3md)
+     * @param name the name of the parameter defined in the material definition (.j3md)
      * @param type the type of the parameter {@link VarType}
      * @param value the value of the parameter
      */
@@ -506,7 +535,6 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         } else {
             MatParam val = getParam(name);
             if (val == null) {
-                MatParam paramDef = def.getMaterialParam(name);
                 paramValues.put(name, new MatParam(type, name, value));
             } else {
                 val.setValue(value);
@@ -533,7 +561,6 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         setParam(name, p.getVarType(), value);
     }
 
-
     /**
      * Clear a parameter from this material. The parameter must exist
      * @param name the name of the parameter to clear
@@ -569,14 +596,17 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
 
         checkSetParam(type, name);
-        MatParamTexture val = getTextureParam(name);
-        if (val == null) {
-            checkTextureParamColorSpace(name, value);
-            paramValues.put(name, new MatParamTexture(type, name, value, value.getImage() != null ? value.getImage().getColorSpace() : null));
+        MatParamTexture param = getTextureParam(name);
+
+        checkTextureParamColorSpace(name, value);
+        ColorSpace colorSpace = value.getImage() != null ? value.getImage().getColorSpace() : null;
+
+        if (param == null) {
+            param = new MatParamTexture(type, name, value, colorSpace);
+            paramValues.put(name, param);
         } else {
-            checkTextureParamColorSpace(name, value);
-            val.setTextureValue(value);
-            val.setColorSpace(value.getImage() != null ? value.getImage().getColorSpace() : null);
+            param.setTextureValue(value);
+            param.setColorSpace(colorSpace);
         }
 
         if (technique != null) {
@@ -613,8 +643,8 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     /**
      * Pass a texture to the material shader.
      *
-     * @param name the name of the texture defined in the material definition
-     * (j3md) (for example Texture for Lighting.j3md)
+     * @param name  the name of the texture defined in the material definition
+     *              (.j3md) (e.g. Texture for Lighting.j3md)
      * @param value the Texture object previously loaded by the asset manager
      */
     public void setTexture(String name, Texture value) {
@@ -707,7 +737,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
     }
 
     /**
-     * Pass an uniform buffer object to the material shader.
+     * Pass a uniform buffer object to the material shader.
      *
      * @param name  the name of the buffer object defined in the material definition (j3md).
      * @param value the buffer object.
@@ -843,7 +873,6 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
     }
 
-
     private void updateShaderMaterialParameter(Renderer renderer, VarType type, Shader shader, MatParam param, BindUnits unit, boolean override) {
         if (type == VarType.UniformBufferObject || type == VarType.ShaderStorageBufferObject) {
             ShaderBufferBlock bufferBlock = shader.getBufferBlock(param.getPrefixedName());
@@ -862,7 +891,8 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
             unit.bufferUnit++;
         } else {
             Uniform uniform = shader.getUniform(param.getPrefixedName());
-            if (!override && uniform.isSetByCurrentMaterial()) return;
+            if (!override && uniform.isSetByCurrentMaterial())
+                return;
 
             if (type.isTextureType() || type.isImageType()) {
                 try {
@@ -871,9 +901,9 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
                     } else {
                         renderer.setTextureImage(unit.textureUnit, (TextureImage) param.getValue());
                     }
-                } catch (TextureUnitException exception) {
+                } catch (TextureUnitException ex) {
                     int numTexParams = unit.textureUnit + 1;
-                    String message = "Too many texture parameters (" + numTexParams + ") assigned\n to " + toString();
+                    String message = "Too many texture parameters (" + numTexParams + ") assigned\n to " + this.toString();
                     throw new IllegalStateException(message);
                 }
                 uniform.setValue(VarType.Int, unit.textureUnit);
@@ -884,11 +914,8 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
     }
 
-
-
-
-    private BindUnits updateShaderMaterialParameters(Renderer renderer, Shader shader, SafeArrayList<MatParamOverride> worldOverrides,
-            SafeArrayList<MatParamOverride> forcedOverrides) {
+    private BindUnits updateShaderMaterialParameters(Renderer renderer, Shader shader,
+                 SafeArrayList<MatParamOverride> worldOverrides, SafeArrayList<MatParamOverride> forcedOverrides) {
 
         bindUnits.textureUnit = 0;
         bindUnits.bufferUnit = 0;
@@ -901,20 +928,15 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
 
         for (int i = 0; i < paramValues.size(); i++) {
-
             MatParam param = paramValues.getValue(i);
             VarType type = param.getVarType();
-
             updateShaderMaterialParameter(renderer, type, shader, param, bindUnits, false);
         }
 
-        // TODO HACKY HACK remove this when texture unit is handled by the
-        // uniform.
+        // TODO: HACKY HACK remove this when texture unit is handled by the uniform.
         return bindUnits;
     }
 
-
-
     private void updateRenderState(Geometry geometry, RenderManager renderManager, Renderer renderer, TechniqueDef techniqueDef) {
         RenderState finalRenderState;
         if (renderManager.getForcedRenderState() != null) {
@@ -935,8 +957,9 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
 
     /**
      * Returns true if the geometry world scale indicates that normals will be backward.
-     * @param scalar geometry world scale
-     * @return 
+     *
+     * @param scalar The geometry's world scale vector.
+     * @return true if the normals are effectively backward; false otherwise.
      */
     private boolean isNormalsBackward(Vector3f scalar) {
         // count number of negative scalar vector components
@@ -1113,6 +1136,14 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         render(geom, geom.getWorldLightList(), rm);
     }
 
+    @Override
+    public String toString() {
+        return "Material[name=" + name +
+                ", def=" + (def != null ? def.getName() : null) +
+                ", tech=" + (technique != null && technique.getDef() != null ? technique.getDef().getName() : null) +
+                "]";
+    }
+
     @Override
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
@@ -1123,14 +1154,6 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         oc.writeStringSavableMap(paramValues, "parameters", null);
     }
     
-    @Override
-    public String toString() {
-        return "Material[name=" + name + 
-                ", def=" + (def != null ? def.getName() : null) + 
-                ", tech=" + (technique != null && technique.getDef() != null ? technique.getDef().getName() : null) + 
-                "]";
-    }
-
     @Override
     @SuppressWarnings("unchecked")
     public void read(JmeImporter im) throws IOException {
@@ -1144,7 +1167,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         String defName = ic.readString("material_def", null);
         HashMap<String, MatParam> params = (HashMap<String, MatParam>) ic.readStringSavableMap("parameters", null);
 
-        boolean enableVcolor = false;
+        boolean enableVertexColor = false;
         boolean separateTexCoord = false;
         boolean applyDefaultValues = false;
         boolean guessRenderStateApply = false;
@@ -1160,7 +1183,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
             // Enable compatibility with old models
             if (defName.equalsIgnoreCase("Common/MatDefs/Misc/VertexColor.j3md")) {
                 // Using VertexColor, switch to Unshaded and set VertexColor=true
-                enableVcolor = true;
+                enableVertexColor = true;
                 defName = "Common/MatDefs/Misc/Unshaded.j3md";
             } else if (defName.equalsIgnoreCase("Common/MatDefs/Misc/SimpleTextured.j3md")
                     || defName.equalsIgnoreCase("Common/MatDefs/Misc/SolidColor.j3md")) {
@@ -1212,8 +1235,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
         }
 
         if (applyDefaultValues) {
-            // compatability with old versions where default vars were
-            // not available
+            // compatibility with old versions where default vars were not available
             for (MatParam param : def.getMaterialParams()) {
                 if (param.getValue() != null && paramValues.get(param.getName()) == null) {
                     setParam(param.getName(), param.getVarType(), param.getValue());
@@ -1232,7 +1254,7 @@ public class Material implements CloneableSmartAsset, Cloneable, Savable {
             additionalState.applyStencilTest = additionalState.stencilTest;
             additionalState.applyWireFrame = additionalState.wireframe;
         }
-        if (enableVcolor) {
+        if (enableVertexColor) {
             setBoolean("VertexColor", true);
         }
         if (separateTexCoord) {

+ 9 - 5
jme3-core/src/main/java/com/jme3/material/Materials.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -38,13 +38,17 @@ package com.jme3.material;
  */
 public class Materials {
 
-    public static final String UNSHADED = "Common/MatDefs/Misc/Unshaded.j3md";
-    public static final String LIGHTING = "Common/MatDefs/Light/Lighting.j3md";
-    public static final String PBR = "Common/MatDefs/Light/PBRLighting.j3md";
+    public static final String SHOW_NORMALS = "Common/MatDefs/Misc/ShowNormals.j3md";
+    public static final String UNSHADED     = "Common/MatDefs/Misc/Unshaded.j3md";
+    public static final String LIGHTING     = "Common/MatDefs/Light/Lighting.j3md";
+    public static final String PBR          = "Common/MatDefs/Light/PBRLighting.j3md";
+    public static final String PARTICLE     = "Common/MatDefs/Misc/Particle.j3md";
+    public static final String BILLBOARD    = "Common/MatDefs/Misc/Billboard.j3md";
+    public static final String GUI          = "Common/MatDefs/Gui/Gui.j3md";
 
     /**
      * A private constructor to inhibit instantiation of this class.
      */
     private Materials() {
     }
-}
+}

+ 1 - 1
jme3-core/src/main/java/com/jme3/post/FilterPostProcessor.java

@@ -501,7 +501,7 @@ public class FilterPostProcessor implements SceneProcessor, Savable {
             }
         }
 
-        if (numSamples <= 1 || !caps.contains(Caps.OpenGL32)) {
+        if (numSamples <= 1 || !caps.contains(Caps.OpenGL32) || !caps.contains(Caps.FrameBufferMultisample)) {
             renderFrameBuffer = new FrameBuffer(width, height, 1);
             renderFrameBuffer.setDepthTarget(FrameBufferTarget.newTarget(depthFormat));
             filterTexture = new Texture2D(width, height, fbFormat);

+ 3 - 1
jme3-core/src/main/java/com/jme3/renderer/opengl/GLRenderer.java

@@ -516,7 +516,9 @@ public final class GLRenderer implements Renderer {
                 limits.put(Limits.FrameBufferSamples, getInteger(GLExt.GL_MAX_SAMPLES_EXT));
             }
 
-            if (hasExtension("GL_ARB_texture_multisample") || caps.contains(Caps.OpenGLES31)) { // GLES31 does not fully support it
+            if (hasExtension("GL_ARB_texture_multisample") || caps.contains(Caps.OpenGLES31)
+                    || (JmeSystem.getPlatform().getOs() == Platform.Os.MacOS
+                            && caps.contains(Caps.OpenGL32))) { // GLES31 does not fully support it
                 caps.add(Caps.TextureMultisample);
                 limits.put(Limits.ColorTextureSamples, getInteger(GLExt.GL_MAX_COLOR_TEXTURE_SAMPLES));
                 limits.put(Limits.DepthTextureSamples, getInteger(GLExt.GL_MAX_DEPTH_TEXTURE_SAMPLES));

+ 157 - 60
jme3-core/src/main/java/com/jme3/scene/control/LightControl.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2023 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -49,52 +49,77 @@ import com.jme3.util.clone.Cloner;
 import java.io.IOException;
 
 /**
- * This Control maintains a reference to a Light,
- * which will be synched with the position (worldTranslation)
- * of the current spatial.
+ * `LightControl` synchronizes the world transformation (position and/or
+ * direction) of a `Light` with its attached `Spatial`. This control allows
+ * a light to follow a spatial or vice-versa, depending on the chosen
+ * {@link ControlDirection}.
+ * <p>
+ * This is particularly useful for attaching lights to animated characters,
+ * moving vehicles, or dynamically controlled objects.
+ * </p>
  *
- * @author tim
+ * @author Tim
+ * @author Markil 3
+ * @author capdevon
  */
 public class LightControl extends AbstractControl {
 
-    private static final String CONTROL_DIR_NAME = "controlDir";
-    private static final String LIGHT_NAME = "light";
-
+    /**
+     * Defines the direction of synchronization between the light and the spatial.
+     */
     public enum ControlDirection {
-
         /**
-         * Means, that the Light's transform is "copied"
-         * to the Transform of the Spatial.
+         * The light's transform is copied to the spatial's transform.
          */
         LightToSpatial,
         /**
-         * Means, that the Spatial's transform is "copied"
-         * to the Transform of the light.
+         * The spatial's transform is copied to the light's transform.
          */
         SpatialToLight
     }
 
+    /**
+     * Represents the local axis of the spatial (X, Y, or Z) to be used
+     * for determining the light's direction when `ControlDirection` is
+     * `SpatialToLight`.
+     */
+    public enum Axis {
+        X, Y, Z
+    }
+
     private Light light;
     private ControlDirection controlDir = ControlDirection.SpatialToLight;
+    private Axis axisRotation = Axis.Z;
+    private boolean invertAxisDirection = false;
 
     /**
-     * Constructor used for Serialization.
+     * For serialization only. Do not use.
      */
     public LightControl() {
     }
 
     /**
+     * Creates a new `LightControl` that synchronizes the light's transform to the spatial.
+     *
      * @param light The light to be synced.
+     * @throws IllegalArgumentException if the light type is not supported
+     * (only Point, Directional, and Spot lights are supported).
      */
     public LightControl(Light light) {
+        validateSupportedLightType(light);
         this.light = light;
     }
 
     /**
+     * Creates a new `LightControl` with a specified synchronization direction.
+     *
      * @param light The light to be synced.
-     * @param controlDir SpatialToLight or LightToSpatial
+     * @param controlDir The direction of synchronization (SpatialToLight or LightToSpatial).
+     * @throws IllegalArgumentException if the light type is not supported
+     * (only Point, Directional, and Spot lights are supported).
      */
     public LightControl(Light light, ControlDirection controlDir) {
+        validateSupportedLightType(light);
         this.light = light;
         this.controlDir = controlDir;
     }
@@ -104,6 +129,7 @@ public class LightControl extends AbstractControl {
     }
 
     public void setLight(Light light) {
+        validateSupportedLightType(light);
         this.light = light;
     }
 
@@ -115,86 +141,141 @@ public class LightControl extends AbstractControl {
         this.controlDir = controlDir;
     }
 
-    // fields used when inverting ControlDirection:
+    public Axis getAxisRotation() {
+        return axisRotation;
+    }
+
+    public void setAxisRotation(Axis axisRotation) {
+        this.axisRotation = axisRotation;
+    }
+
+    public boolean isInvertAxisDirection() {
+        return invertAxisDirection;
+    }
+
+    public void setInvertAxisDirection(boolean invertAxisDirection) {
+        this.invertAxisDirection = invertAxisDirection;
+    }
+
+    private void validateSupportedLightType(Light light) {
+        if (light == null) {
+            return;
+        }
+
+        switch (light.getType()) {
+            case Point:
+            case Directional:
+            case Spot:
+                // These types are supported, validation passes.
+                break;
+            default:
+                throw new IllegalArgumentException(
+                        "Unsupported Light type: " + light.getType());
+        }
+    }
+
     @Override
     protected void controlUpdate(float tpf) {
-        if (spatial != null && light != null) {
-            switch (controlDir) {
-                case SpatialToLight:
-                    spatialToLight(light);
-                    break;
-                case LightToSpatial:
-                    lightToSpatial(light);
-                    break;
-            }
+        if (light == null) {
+            return;
+        }
+
+        switch (controlDir) {
+            case SpatialToLight:
+                spatialToLight(light);
+                break;
+            case LightToSpatial:
+                lightToSpatial(light);
+                break;
         }
     }
 
     /**
-     * Sets the light to adopt the spatial's world transformations.
+     * Updates the light's position and/or direction to match the spatial's
+     * world transformation.
      *
-     * @author Markil 3
-     * @author pspeed42
+     * @param light The light whose properties will be set.
      */
     private void spatialToLight(Light light) {
         TempVars vars = TempVars.get();
 
-        final Vector3f worldTranslation = vars.vect1;
-        worldTranslation.set(spatial.getWorldTranslation());
-        final Vector3f worldDirection = vars.vect2;
-        spatial.getWorldRotation().mult(Vector3f.UNIT_Z, worldDirection).negateLocal();
+        final Vector3f worldPosition = vars.vect1;
+        worldPosition.set(spatial.getWorldTranslation());
+
+        final Vector3f lightDirection = vars.vect2;
+        spatial.getWorldRotation().getRotationColumn(axisRotation.ordinal(), lightDirection);
+        if (invertAxisDirection) {
+            lightDirection.negateLocal();
+        }
 
         if (light instanceof PointLight) {
-            ((PointLight) light).setPosition(worldTranslation);
+            ((PointLight) light).setPosition(worldPosition);
+
         } else if (light instanceof DirectionalLight) {
-            ((DirectionalLight) light).setDirection(worldDirection);
+            ((DirectionalLight) light).setDirection(lightDirection);
+
         } else if (light instanceof SpotLight) {
-            final SpotLight spotLight = (SpotLight) light;
-            spotLight.setPosition(worldTranslation);
-            spotLight.setDirection(worldDirection);
+            SpotLight sl = (SpotLight) light;
+            sl.setPosition(worldPosition);
+            sl.setDirection(lightDirection);
         }
         vars.release();
     }
 
     /**
-     * Sets the spatial to adopt the light's world transformations.
+     * Updates the spatial's local transformation (position and/or rotation)
+     * to match the light's world transformation.
      *
-     * @author Markil 3
+     * @param light The light from which properties will be read.
      */
     private void lightToSpatial(Light light) {
         TempVars vars = TempVars.get();
-        Vector3f translation = vars.vect1;
-        Vector3f direction = vars.vect2;
+        Vector3f lightPosition = vars.vect1;
+        Vector3f lightDirection = vars.vect2;
         Quaternion rotation = vars.quat1;
-        boolean rotateSpatial = false, translateSpatial = false;
+        boolean rotateSpatial = false;
+        boolean translateSpatial = false;
 
         if (light instanceof PointLight) {
-            PointLight pLight = (PointLight) light;
-            translation.set(pLight.getPosition());
+            PointLight pl = (PointLight) light;
+            lightPosition.set(pl.getPosition());
             translateSpatial = true;
+
         } else if (light instanceof DirectionalLight) {
-            DirectionalLight dLight = (DirectionalLight) light;
-            direction.set(dLight.getDirection()).negateLocal();
+            DirectionalLight dl = (DirectionalLight) light;
+            lightDirection.set(dl.getDirection());
+            if (invertAxisDirection) {
+                lightDirection.negateLocal();
+            }
             rotateSpatial = true;
+
         } else if (light instanceof SpotLight) {
-            SpotLight sLight = (SpotLight) light;
-            translation.set(sLight.getPosition());
-            direction.set(sLight.getDirection()).negateLocal();
-            translateSpatial = rotateSpatial = true;
+            SpotLight sl = (SpotLight) light;
+            lightPosition.set(sl.getPosition());
+            lightDirection.set(sl.getDirection());
+            if (invertAxisDirection) {
+                lightDirection.negateLocal();
+            }
+            translateSpatial = true;
+            rotateSpatial = true;
         }
+
+        // Transform light's world properties to spatial's parent's local space
         if (spatial.getParent() != null) {
+            // Get inverse of parent's world matrix
             spatial.getParent().getLocalToWorldMatrix(vars.tempMat4).invertLocal();
-            vars.tempMat4.rotateVect(translation);
-            vars.tempMat4.translateVect(translation);
-            vars.tempMat4.rotateVect(direction);
+            vars.tempMat4.rotateVect(lightPosition);
+            vars.tempMat4.translateVect(lightPosition);
+            vars.tempMat4.rotateVect(lightDirection);
         }
 
+        // Apply transformed properties to spatial's local transformation
         if (rotateSpatial) {
-            rotation.lookAt(direction, Vector3f.UNIT_Y).normalizeLocal();
+            rotation.lookAt(lightDirection, Vector3f.UNIT_Y).normalizeLocal();
             spatial.setLocalRotation(rotation);
         }
         if (translateSpatial) {
-            spatial.setLocalTranslation(translation);
+            spatial.setLocalTranslation(lightPosition);
         }
         vars.release();
     }
@@ -214,15 +295,31 @@ public class LightControl extends AbstractControl {
     public void read(JmeImporter im) throws IOException {
         super.read(im);
         InputCapsule ic = im.getCapsule(this);
-        controlDir = ic.readEnum(CONTROL_DIR_NAME, ControlDirection.class, ControlDirection.SpatialToLight);
-        light = (Light) ic.readSavable(LIGHT_NAME, null);
+        light = (Light) ic.readSavable("light", null);
+        controlDir = ic.readEnum("controlDir", ControlDirection.class, ControlDirection.SpatialToLight);
+        axisRotation = ic.readEnum("axisRotation", Axis.class, Axis.Z);
+        invertAxisDirection = ic.readBoolean("invertAxisDirection", false);
     }
 
     @Override
     public void write(JmeExporter ex) throws IOException {
         super.write(ex);
         OutputCapsule oc = ex.getCapsule(this);
-        oc.write(controlDir, CONTROL_DIR_NAME, ControlDirection.SpatialToLight);
-        oc.write(light, LIGHT_NAME, null);
+        oc.write(light, "light", null);
+        oc.write(controlDir, "controlDir", ControlDirection.SpatialToLight);
+        oc.write(axisRotation, "axisRotation", Axis.Z);
+        oc.write(invertAxisDirection, "invertAxisDirection", false);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() +
+                "[light=" + (light != null ? light.getType() : null) +
+                ", controlDir=" + controlDir +
+                ", axisRotation=" + axisRotation +
+                ", invertAxisDirection=" + invertAxisDirection +
+                ", enabled=" + enabled +
+                ", spatial=" + spatial +
+                "]";
     }
-}
+}

+ 113 - 27
jme3-core/src/main/java/com/jme3/scene/debug/WireFrustum.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2012 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,74 +32,160 @@
 package com.jme3.scene.debug;
 
 import com.jme3.math.Vector3f;
+import com.jme3.renderer.Camera;
+import com.jme3.scene.Geometry;
 import com.jme3.scene.Mesh;
 import com.jme3.scene.VertexBuffer;
 import com.jme3.scene.VertexBuffer.Type;
 import com.jme3.scene.VertexBuffer.Usage;
+import com.jme3.shadow.ShadowUtil;
 import com.jme3.util.BufferUtils;
 import java.nio.FloatBuffer;
 
+/**
+ * A specialized Mesh that renders a camera frustum as a wireframe.
+ * This class extends jME3's Mesh and is designed to visually represent
+ * the viewing volume of a camera, which can be useful for debugging
+ * or visualization purposes.
+ * <p>
+ * The frustum is defined by eight points: four for the near plane
+ * and four for the far plane. These points are connected by lines
+ * to form a wireframe cube-like structure.
+ */
 public class WireFrustum extends Mesh {
 
     /**
-     * This constructor is for serialization only. Do not use.
+     * For Serialization only. Do not use.
      */
     protected WireFrustum() {
     }
 
-    public WireFrustum(Vector3f[] points){
-        initGeom(this, points);
-    }
-
-    public static Mesh makeFrustum(Vector3f[] points){
-        Mesh m = new Mesh();
-        initGeom(m, points);
-        return m;
+    /**
+     * Constructs a new `WireFrustum` mesh using the specified frustum corner points.
+     * The points should represent the 8 corners of the frustum.
+     * The expected order of points is typically:
+     * 0-3: Near plane (e.g., bottom-left, bottom-right, top-right, top-left)
+     * 4-7: Far plane (e.g., bottom-left, bottom-right, top-right, top-left)
+     *
+     * @param points An array of 8 `Vector3f` objects representing the frustum's corners.
+     * If the array is null or does not contain 8 points, an
+     * `IllegalArgumentException` will be thrown.
+     */
+    public WireFrustum(Vector3f[] points) {
+        if (points == null || points.length != 8) {
+            throw new IllegalArgumentException("Frustum points array must not be null and must contain 8 points.");
+        }
+        setGeometryData(points);
     }
 
-    private static void initGeom(Mesh m, Vector3f[] points) {
-        if (points != null)
-            m.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points));
+    /**
+     * Initializes the mesh's geometric data, setting up the vertex positions and indices.
+     * This method is called during the construction of the `WireFrustum`.
+     *
+     * @param points The 8 `Vector3f` points defining the frustum's corners.
+     */
+    private void setGeometryData(Vector3f[] points) {
+        // Set vertex positions
+        setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points));
 
-        m.setBuffer(Type.Index, 2,
+        // Set indices to draw lines connecting the frustum corners
+        // The indices define 12 lines: 4 for near plane, 4 for far plane, and 4 connecting near to far.
+        setBuffer(Type.Index, 2,
                 new short[]{
+                        // Near plane
                         0, 1,
                         1, 2,
                         2, 3,
                         3, 0,
 
+                        // Far plane
                         4, 5,
                         5, 6,
                         6, 7,
                         7, 4,
 
+                        // Connecting lines (near to far)
                         0, 4,
                         1, 5,
                         2, 6,
                         3, 7,
                 }
         );
-        m.getBuffer(Type.Index).setUsage(Usage.Static);
-        m.setMode(Mode.Lines);
+        getBuffer(Type.Index).setUsage(Usage.Static);
+        setMode(Mode.Lines);
+        updateBound();
     }
 
-    public void update(Vector3f[] points){
+    /**
+     * Updates the vertex positions of the existing `WireFrustum` mesh.
+     * This is more efficient than creating a new `WireFrustum` instance
+     * if only the frustum's position or orientation changes.
+     *
+     * @param points An array of 8 `Vector3f` objects representing the new frustum's corners.
+     * If the array is null or does not contain 8 points, an
+     * `IllegalArgumentException` will be thrown.
+     */
+    public void update(Vector3f[] points) {
+        if (points == null || points.length != 8) {
+            throw new IllegalArgumentException("Frustum points array must not be null and must contain 8 points.");
+        }
+
         VertexBuffer vb = getBuffer(Type.Position);
-        if (vb == null){
-            setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(points));
+        if (vb == null) {
+            // If for some reason the position buffer is missing, re-create it.
+            // This case should ideally not happen if the object is constructed properly.
+            setGeometryData(points);
             return;
         }
 
-        FloatBuffer b = BufferUtils.createFloatBuffer(points);
-        FloatBuffer a = (FloatBuffer) vb.getData();
-        b.rewind();
-        a.rewind();
-        a.put(b);
-        a.rewind();
+        // Create a new FloatBuffer from the updated points
+        FloatBuffer newBuff = BufferUtils.createFloatBuffer(points);
+        // Get the existing FloatBuffer from the VertexBuffer
+        FloatBuffer currBuff = (FloatBuffer) vb.getData();
 
-        vb.updateData(a);
-        
+        currBuff.clear();       // Clear
+        currBuff.put(newBuff);  // Copy
+        currBuff.rewind();      // Rewind
+
+        // Update the VertexBuffer with the modified FloatBuffer data
+        vb.updateData(currBuff);
+
+        // Update the mesh's bounding volume to reflect the new vertex positions
         updateBound();
     }
 
+    /**
+     * A static factory method to create a new `WireFrustum` mesh.
+     * This method provides a cleaner way to instantiate a `WireFrustum`.
+     *
+     * @param points An array of 8 `Vector3f` objects representing the frustum's corners.
+     * @return A new `WireFrustum` instance.
+     */
+    public static Mesh makeFrustum(Vector3f[] points) {
+        return new WireFrustum(points);
+    }
+
+    /**
+     * Creates a `Geometry` object representing the wireframe frustum of a given camera.
+     * The frustum points are calculated based on the camera's current view settings.
+     * The returned `Geometry` can be directly attached to a scene graph.
+     *
+     * @param camera The `Camera` whose frustum is to be visualized.
+     * @return A `Geometry` object containing the `WireFrustum` mesh.
+     */
+    public static Geometry makeGeometry(Camera camera) {
+        Vector3f[] frustumCorners = new Vector3f[8];
+        for (int i = 0; i < 8; i++) {
+            frustumCorners[i] = new Vector3f();
+        }
+
+        Camera tempCam = camera.clone();
+        tempCam.setLocation(new Vector3f(0, 0, 0));
+        tempCam.lookAt(Vector3f.UNIT_Z, Vector3f.UNIT_Y);
+        ShadowUtil.updateFrustumPoints2(tempCam, frustumCorners);
+
+        WireFrustum mesh = new WireFrustum(frustumCorners);
+        return new Geometry("Viewing Frustum", mesh);
+    }
+
 }

+ 110 - 13
jme3-core/src/main/java/com/jme3/scene/mesh/MorphTarget.java

@@ -1,46 +1,142 @@
+/*
+ * Copyright (c) 2009-2025 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.scene.mesh;
 
-import com.jme3.export.*;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import com.jme3.export.Savable;
 import com.jme3.scene.VertexBuffer;
 
 import java.io.IOException;
-import java.nio.Buffer;
 import java.nio.FloatBuffer;
 import java.util.EnumMap;
 import java.util.Map;
 
+/**
+ * `MorphTarget` represents a single morph target within a `Mesh`.
+ * A morph target contains a set of `FloatBuffer` instances, each corresponding
+ * to a `VertexBuffer.Type` (e.g., `POSITION`, `NORMAL`, `TANGENT`).
+ * These buffers store the delta (difference) values that, when added to the
+ * base mesh's corresponding vertex buffers, create a deformed version of the mesh.
+ * <p>
+ * Morph targets are primarily used for skeletal animation blending, facial animation,
+ * or other mesh deformation effects. Each `MorphTarget` can optionally have a name
+ * for identification and control.
+ */
 public class MorphTarget implements Savable {
+
+    /**
+     * Stores the `FloatBuffer` instances for each `VertexBuffer.Type` that
+     * this morph target affects.
+     */
     private final EnumMap<VertexBuffer.Type, FloatBuffer> buffers = new EnumMap<>(VertexBuffer.Type.class);
-    private String name = null;
-    
+    /**
+     * An optional name for this morph target, useful for identification
+     * and targeting in animations.
+     */
+    private String name;
+
+    /**
+     * Required for jME deserialization.
+     */
     public MorphTarget() {
-        
     }
-    
+
+    /**
+     * Creates a new `MorphTarget` with the specified name.
+     *
+     * @param name The name of this morph target (can be null).
+     */
     public MorphTarget(String name) {
         this.name = name;
     }
-    
+
+    /**
+     * Sets the name of this morph target.
+     *
+     * @param name The new name for this morph target (can be null).
+     */
     public void setName(String name) {
         this.name = name;
     }
-    
+
+    /**
+     * Returns the name of this morph target.
+     *
+     * @return The name of this morph target, or null if not set.
+     */
     public String getName() {
         return name;
     }
 
+    /**
+     * Associates a `FloatBuffer` with a specific `VertexBuffer.Type` for this morph target.
+     * This buffer typically contains the delta values for the specified vertex attribute.
+     *
+     * @param type The type of vertex buffer (e.g., `POSITION`, `NORMAL`).
+     * @param buffer The `FloatBuffer` containing the delta data for the given type.
+     */
     public void setBuffer(VertexBuffer.Type type, FloatBuffer buffer) {
         buffers.put(type, buffer);
     }
 
+    /**
+     * Retrieves the `FloatBuffer` associated with a specific `VertexBuffer.Type` for this morph target.
+     *
+     * @param type The type of vertex buffer.
+     * @return The `FloatBuffer` for the given type, or null if not set.
+     */
     public FloatBuffer getBuffer(VertexBuffer.Type type) {
         return buffers.get(type);
     }
 
+    /**
+     * Returns the `EnumMap` containing all the `FloatBuffer` instances
+     * associated with their `VertexBuffer.Type` for this morph target.
+     *
+     * @return An `EnumMap` of vertex buffer types to their corresponding `FloatBuffer`s.
+     */
     public EnumMap<VertexBuffer.Type, FloatBuffer> getBuffers() {
         return buffers;
     }
 
+    /**
+     * Returns the number of `FloatBuffer`s (i.e., vertex attribute types)
+     * contained within this morph target.
+     *
+     * @return The count of buffers in this morph target.
+     */
     public int getNumBuffers() {
         return buffers.size();
     }
@@ -49,8 +145,9 @@ public class MorphTarget implements Savable {
     public void write(JmeExporter ex) throws IOException {
         OutputCapsule oc = ex.getCapsule(this);
         for (Map.Entry<VertexBuffer.Type, FloatBuffer> entry : buffers.entrySet()) {
-            Buffer roData = entry.getValue().asReadOnlyBuffer();
-            oc.write((FloatBuffer) roData, entry.getKey().name(),null);
+            VertexBuffer.Type type = entry.getKey();
+            FloatBuffer roData = entry.getValue().asReadOnlyBuffer();
+            oc.write(roData, type.name(), null);
         }
         oc.write(name, "morphName", null);
     }
@@ -59,9 +156,9 @@ public class MorphTarget implements Savable {
     public void read(JmeImporter im) throws IOException {
         InputCapsule ic = im.getCapsule(this);
         for (VertexBuffer.Type type : VertexBuffer.Type.values()) {
-            FloatBuffer b = ic.readFloatBuffer(type.name(), null);
-            if(b!= null){
-                setBuffer(type, b);
+            FloatBuffer fb = ic.readFloatBuffer(type.name(), null);
+            if (fb != null) {
+                setBuffer(type, fb);
             }
         }
         name = ic.readString("morphName", null);

+ 79 - 69
jme3-effects/src/main/java/com/jme3/post/ssao/SSAOFilter.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -40,7 +40,6 @@ import com.jme3.material.Material;
 import com.jme3.math.Vector2f;
 import com.jme3.math.Vector3f;
 import com.jme3.post.Filter;
-import com.jme3.post.Filter.Pass;
 import com.jme3.renderer.RenderManager;
 import com.jme3.renderer.Renderer;
 import com.jme3.renderer.ViewPort;
@@ -62,23 +61,22 @@ import java.util.ArrayList;
 public class SSAOFilter extends Filter {
 
     private Pass normalPass;
-    private Vector3f frustumCorner;
-    private Vector2f frustumNearFar;
-    private Vector2f[] samples = {new Vector2f(1.0f, 0.0f), new Vector2f(-1.0f, 0.0f), new Vector2f(0.0f, 1.0f), new Vector2f(0.0f, -1.0f)};
+    private final Vector2f[] samples = {
+            new Vector2f(1.0f, 0.0f),
+            new Vector2f(-1.0f, 0.0f),
+            new Vector2f(0.0f, 1.0f),
+            new Vector2f(0.0f, -1.0f)
+    };
     private float sampleRadius = 5.1f;
     private float intensity = 1.5f;
     private float scale = 0.2f;
     private float bias = 0.1f;
+    private boolean approximateNormals = false;
     private boolean useOnlyAo = false;
     private boolean useAo = true;
     private Material ssaoMat;
-    private Pass ssaoPass;
-//    private Material downSampleMat;
-//    private Pass downSamplePass;
-    private float downSampleFactor = 1f;
     private RenderManager renderManager;
     private ViewPort viewPort;
-    private boolean approximateNormals = false;
 
     /**
      * Create a Screen Space Ambient Occlusion Filter
@@ -89,10 +87,11 @@ public class SSAOFilter extends Filter {
 
     /**
      * Create a Screen Space Ambient Occlusion Filter
+     *
      * @param sampleRadius The radius of the area where random samples will be picked. default 5.1f
-     * @param intensity intensity of the resulting AO. default 1.2f
-     * @param scale distance between occluders and occludee. default 0.2f
-     * @param bias the width of the occlusion cone considered by the occludee. default 0.1f
+     * @param intensity    intensity of the resulting AO. default 1.5f
+     * @param scale        distance between occluders and occludee. default 0.2f
+     * @param bias         the width of the occlusion cone considered by the occludee. default 0.1f
      */
     public SSAOFilter(float sampleRadius, float intensity, float scale, float bias) {
         this();
@@ -126,38 +125,32 @@ public class SSAOFilter extends Filter {
     }
 
     @Override
-    protected void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) {
+    protected void initFilter(AssetManager assetManager, RenderManager renderManager, ViewPort vp, int w, int h) {
         this.renderManager = renderManager;
         this.viewPort = vp;
         int screenWidth = w;
         int screenHeight = h;
+        float downSampleFactor = 1f;
         postRenderPasses = new ArrayList<Pass>();
 
         normalPass = new Pass();
         normalPass.init(renderManager.getRenderer(), (int) (screenWidth / downSampleFactor), (int) (screenHeight / downSampleFactor), Format.RGBA8, Format.Depth);
 
-
-        frustumNearFar = new Vector2f();
-
+        Vector2f frustumNearFar = new Vector2f();
         float farY = (vp.getCamera().getFrustumTop() / vp.getCamera().getFrustumNear()) * vp.getCamera().getFrustumFar();
         float farX = farY * (screenWidth / (float) screenHeight);
-        frustumCorner = new Vector3f(farX, farY, vp.getCamera().getFrustumFar());
+        Vector3f frustumCorner = new Vector3f(farX, farY, vp.getCamera().getFrustumFar());
         frustumNearFar.x = vp.getCamera().getFrustumNear();
         frustumNearFar.y = vp.getCamera().getFrustumFar();
 
-
-
-
-
         //ssao Pass
-        ssaoMat = new Material(manager, "Common/MatDefs/SSAO/ssao.j3md");
+        ssaoMat = new Material(assetManager, "Common/MatDefs/SSAO/ssao.j3md");
         ssaoMat.setTexture("Normals", normalPass.getRenderedTexture());
-        Texture random = manager.loadTexture("Common/MatDefs/SSAO/Textures/random.png");
+        Texture random = assetManager.loadTexture("Common/MatDefs/SSAO/Textures/random.png");
         random.setWrap(Texture.WrapMode.Repeat);
         ssaoMat.setTexture("RandomMap", random);
 
-        ssaoPass = new Pass("SSAO pass") {
-
+        Pass ssaoPass = new Pass("SSAO pass") {
             @Override
             public boolean requiresDepthAsTexture() {
                 return true;
@@ -168,18 +161,18 @@ public class SSAOFilter extends Filter {
 //        ssaoPass.getRenderedTexture().setMinFilter(Texture.MinFilter.Trilinear);
 //        ssaoPass.getRenderedTexture().setMagFilter(Texture.MagFilter.Bilinear);
         postRenderPasses.add(ssaoPass);
-        material = new Material(manager, "Common/MatDefs/SSAO/ssaoBlur.j3md");
+        material = new Material(assetManager, "Common/MatDefs/SSAO/ssaoBlur.j3md");
         material.setTexture("SSAOMap", ssaoPass.getRenderedTexture());
+        material.setVector2("FrustumNearFar", frustumNearFar);
+        material.setBoolean("UseAo", useAo);
+        material.setBoolean("UseOnlyAo", useOnlyAo);
 
         ssaoMat.setVector3("FrustumCorner", frustumCorner);
         ssaoMat.setFloat("SampleRadius", sampleRadius);
         ssaoMat.setFloat("Intensity", intensity);
         ssaoMat.setFloat("Scale", scale);
         ssaoMat.setFloat("Bias", bias);
-        material.setBoolean("UseAo", useAo);
-        material.setBoolean("UseOnlyAo", useOnlyAo);
         ssaoMat.setVector2("FrustumNearFar", frustumNearFar);
-        material.setVector2("FrustumNearFar", frustumNearFar);
         ssaoMat.setParam("Samples", VarType.Vector2Array, samples);
         ssaoMat.setBoolean("ApproximateNormals", approximateNormals);
 
@@ -189,7 +182,6 @@ public class SSAOFilter extends Filter {
         float blurScale = 2f;
         material.setFloat("XScale", blurScale * xScale);
         material.setFloat("YScale", blurScale * yScale);
-
     }
 
     @Override
@@ -198,18 +190,20 @@ public class SSAOFilter extends Filter {
     }    
     
     /**
-     * Return the bias<br>
-     * see {@link  #setBias(float bias)}
-     * @return  bias
+     * Returns the bias value used in the SSAO calculation.
+     *
+     * @return The bias value.
+     * @see #setBias(float)
      */
     public float getBias() {
         return bias;
     }
 
     /**
-     * Sets the width of the occlusion cone considered by the occludee default is 0.1f
+     * Sets the width of the occlusion cone considered by the occludee.
+     * A higher bias means a wider cone, resulting in less self-occlusion.
      *
-     * @param bias the desired width (default=0.1)
+     * @param bias The desired bias value (default: 0.1f).
      */
     public void setBias(float bias) {
         this.bias = bias;
@@ -219,62 +213,65 @@ public class SSAOFilter extends Filter {
     }
 
     /**
-     * returns the ambient occlusion intensity
-     * @return intensity
+     * Returns the ambient occlusion intensity.
+     *
+     * @return The intensity value.
      */
     public float getIntensity() {
         return intensity;
     }
 
     /**
-     * Sets the Ambient occlusion intensity default is 1.5
+     * Sets the ambient occlusion intensity. A higher intensity makes the ambient
+     * occlusion effect more pronounced.
      *
-     * @param intensity the desired intensity (default=1.5)
+     * @param intensity The desired intensity (default: 1.5f).
      */
     public void setIntensity(float intensity) {
         this.intensity = intensity;
         if (ssaoMat != null) {
             ssaoMat.setFloat("Intensity", intensity);
         }
-
     }
 
     /**
-     * returns the sample radius<br>
-     * see {link setSampleRadius(float sampleRadius)}
-     * @return the sample radius
+     * Returns the sample radius used in the SSAO calculation.
+     *
+     * @return The sample radius.
+     * @see #setSampleRadius(float)
      */
     public float getSampleRadius() {
         return sampleRadius;
     }
 
     /**
-     * Sets the radius of the area where random samples will be picked default 5.1f 
+     * Sets the radius of the area where random samples will be picked for SSAO.
+     * A larger radius considers more distant occluders.
      *
-     * @param sampleRadius the desired radius (default=5.1)
+     * @param sampleRadius The desired radius (default: 5.1f).
      */
     public void setSampleRadius(float sampleRadius) {
         this.sampleRadius = sampleRadius;
         if (ssaoMat != null) {
             ssaoMat.setFloat("SampleRadius", sampleRadius);
         }
-
     }
 
     /**
-     * returns the scale<br>
-     * see {@link #setScale(float scale)}
-     * @return scale
+     * Returns the scale value used in the SSAO calculation.
+     *
+     * @return The scale value.
+     * @see #setScale(float)
      */
     public float getScale() {
         return scale;
     }
 
     /**
-     * 
-     * Returns the distance between occluders and occludee. default 0.2f
+     * Sets the distance between occluders and occludee for SSAO.
+     * This essentially controls the "thickness" of the ambient occlusion.
      *
-     * @param scale the desired distance (default=0.2)
+     * @param scale The desired distance (default: 0.2f).
      */
     public void setScale(float scale) {
         this.scale = scale;
@@ -284,7 +281,30 @@ public class SSAOFilter extends Filter {
     }
 
     /**
-     * debugging only , will be removed
+     * Sets whether to use approximate normals for the SSAO calculation.
+     * If `true`, normals are derived from the depth buffer. If `false`, a separate
+     * normal pass is rendered.
+     *
+     * @param approximateNormals `true` to use approximate normals, `false` to use a normal pass.
+     */
+    public void setApproximateNormals(boolean approximateNormals) {
+        this.approximateNormals = approximateNormals;
+        if (ssaoMat != null) {
+            ssaoMat.setBoolean("ApproximateNormals", approximateNormals);
+        }
+    }
+
+    /**
+     * Checks if approximate normals are being used for SSAO calculation.
+     *
+     * @return `true` if approximate normals are used, `false` otherwise.
+     */
+    public boolean isApproximateNormals() {
+        return approximateNormals;
+    }
+
+    /**
+     * debugging only, will be removed
      * @return true if using ambient occlusion
      */
     public boolean isUseAo() {
@@ -292,7 +312,7 @@ public class SSAOFilter extends Filter {
     }
 
     /**
-     * debugging only , will be removed
+     * debugging only, will be removed
      *
      * @param useAo true to enable, false to disable (default=true)
      */
@@ -301,22 +321,10 @@ public class SSAOFilter extends Filter {
         if (material != null) {
             material.setBoolean("UseAo", useAo);
         }
-
-    }
-
-    public void setApproximateNormals(boolean approximateNormals) {
-        this.approximateNormals = approximateNormals;
-        if (ssaoMat != null) {
-            ssaoMat.setBoolean("ApproximateNormals", approximateNormals);
-        }
-    }
-
-    public boolean isApproximateNormals() {
-        return approximateNormals;
     }
 
     /**
-     * debugging only , will be removed
+     * debugging only, will be removed
      * @return useOnlyAo
      */
     public boolean isUseOnlyAo() {
@@ -324,7 +332,7 @@ public class SSAOFilter extends Filter {
     }
 
     /**
-     * debugging only , will be removed
+     * debugging only, will be removed
      *
      * @param useOnlyAo true to enable, false to disable (default=false)
      */
@@ -343,6 +351,7 @@ public class SSAOFilter extends Filter {
         oc.write(intensity, "intensity", 1.5f);
         oc.write(scale, "scale", 0.2f);
         oc.write(bias, "bias", 0.1f);
+        oc.write(approximateNormals, "approximateNormals", false);
     }
 
     @Override
@@ -353,5 +362,6 @@ public class SSAOFilter extends Filter {
         intensity = ic.readFloat("intensity", 1.5f);
         scale = ic.readFloat("scale", 0.2f);
         bias = ic.readFloat("bias", 0.1f);
+        approximateNormals = ic.readBoolean("approximateNormals", false);
     }
 }

+ 63 - 0
jme3-effects/src/test/java/com/jme3/post/filters/SSAOFilterTest.java

@@ -0,0 +1,63 @@
+package com.jme3.post.filters;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.DesktopAssetManager;
+import com.jme3.export.binary.BinaryExporter;
+import com.jme3.post.ssao.SSAOFilter;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Automated tests for the {@code SSAOFilter} class.
+ *
+ * @author capdevon
+ */
+public class SSAOFilterTest {
+
+    /**
+     * Tests serialization and de-serialization of an {@code SSAOFilter}.
+     */
+    @Test
+    public void testSaveAndLoad() {
+        SSAOFilter filter = new SSAOFilter();
+
+        // Verify the default parameter values:
+        verifyDefaults(filter);
+
+        // Set parameters to new values:
+        filter.setEnabled(false);
+        filter.setSampleRadius(4.5f);
+        filter.setIntensity(1.8f);
+        filter.setScale(0.4f);
+        filter.setBias(0.5f);
+        filter.setApproximateNormals(true);
+
+        // Create a duplicate filter using serialization:
+        AssetManager assetManager = new DesktopAssetManager();
+        SSAOFilter copy = BinaryExporter.saveAndLoad(assetManager, filter);
+
+        // Verify the parameter values of the copy:
+        Assert.assertEquals("SSAOFilter", copy.getName());
+        Assert.assertEquals(4.5f, copy.getSampleRadius(), 0f);
+        Assert.assertEquals(1.8f, copy.getIntensity(), 0f);
+        Assert.assertEquals(0.4f, copy.getScale(), 0f);
+        Assert.assertEquals(0.5f, copy.getBias(), 0f);
+        Assert.assertTrue(copy.isApproximateNormals());
+        Assert.assertFalse(copy.isEnabled());
+    }
+
+    /**
+     * Verify some default values of a newly instantiated {@code SSAOFilter}.
+     *
+     * @param filter (not null, unaffected)
+     */
+    private void verifyDefaults(SSAOFilter filter) {
+        Assert.assertEquals("SSAOFilter", filter.getName());
+        Assert.assertEquals(5.1f, filter.getSampleRadius(), 0f);
+        Assert.assertEquals(1.5f, filter.getIntensity(), 0f);
+        Assert.assertEquals(0.2f, filter.getScale(), 0f);
+        Assert.assertEquals(0.1f, filter.getBias(), 0f);
+        Assert.assertFalse(filter.isApproximateNormals());
+        Assert.assertTrue(filter.isEnabled());
+    }
+}

+ 85 - 43
jme3-examples/src/main/java/jme3test/stress/TestLodGeneration.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2021 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -44,10 +44,10 @@ import com.jme3.input.ChaseCamera;
 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.light.AmbientLight;
 import com.jme3.light.DirectionalLight;
 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;
@@ -58,44 +58,48 @@ import com.jme3.scene.VertexBuffer;
 
 import jme3tools.optimize.LodGenerator;
 
-public class TestLodGeneration extends SimpleApplication {
+public class TestLodGeneration extends SimpleApplication implements ActionListener {
 
     public static void main(String[] args) {
         TestLodGeneration app = new TestLodGeneration();
         app.start();
     }
 
-    private boolean wireFrame = false;
+    private boolean wireframe = false;
+    // Current reduction value for LOD generation (0.0 to 1.0)
     private float reductionValue = 0.0f;
     private int lodLevel = 0;
     private BitmapText hudText;
-    final private List<Geometry> listGeoms = new ArrayList<>();
-    final private ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(5);
+    private final List<Geometry> listGeoms = new ArrayList<>();
+    private final ScheduledThreadPoolExecutor exec = new ScheduledThreadPoolExecutor(5);
 
     @Override
     public void simpleInitApp() {
 
+        // --- Lighting Setup ---
         DirectionalLight dl = new DirectionalLight();
         dl.setDirection(new Vector3f(-1, -1, -1).normalizeLocal());
         rootNode.addLight(dl);
 
         AmbientLight al = new AmbientLight();
-        al.setColor(ColorRGBA.White.mult(0.6f));
         rootNode.addLight(al);
 
+        // --- Model Loading and Setup ---
         // model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml");
         Node model = (Node) assetManager.loadModel("Models/Jaime/Jaime.j3o");
         BoundingBox b = ((BoundingBox) model.getWorldBound());
         model.setLocalScale(1.2f / (b.getYExtent() * 2));
         // model.setLocalTranslation(0,-(b.getCenter().y - b.getYExtent())* model.getLocalScale().y, 0);
+
+        // Iterate through the model's children and collect all Geometry objects
         for (Spatial spatial : model.getChildren()) {
             if (spatial instanceof Geometry) {
                 listGeoms.add((Geometry) spatial);
             }
         }
 
-        ChaseCamera chaseCam = new ChaseCamera(cam, inputManager);
-        model.addControl(chaseCam);
+        // --- Camera Setup ---
+        ChaseCamera chaseCam = new ChaseCamera(cam, model, inputManager);
         chaseCam.setLookAtOffset(b.getCenter());
         chaseCam.setDefaultDistance(5);
         chaseCam.setMinVerticalRotation(-FastMath.HALF_PI + 0.01f);
@@ -103,11 +107,17 @@ public class TestLodGeneration extends SimpleApplication {
 
         SkinningControl skControl = model.getControl(SkinningControl.class);
         if (skControl != null) {
+            // Disable skinning control if found. This is an optimization for static LOD generation
+            // as skinning computation is not needed when generating LODs.
             skControl.setEnabled(false);
         }
 
+        // --- Initial LOD Generation ---
+        // Set initial reduction value and LOD level
         reductionValue = 0.80f;
         lodLevel = 1;
+
+        // Generate LODs for each geometry in the model
         for (final Geometry geom : listGeoms) {
             LodGenerator lodGenerator = new LodGenerator(geom);
             lodGenerator.bakeLods(LodGenerator.TriangleReductionMethod.PROPORTIONAL, reductionValue);
@@ -115,45 +125,49 @@ public class TestLodGeneration extends SimpleApplication {
         }
 
         rootNode.attachChild(model);
+        // Disable the default fly camera as we are using a chase camera
         flyCam.setEnabled(false);
 
-        guiFont = assetManager.loadFont("Interface/Fonts/Default.fnt");
+        // --- HUD Setup ---
         hudText = new BitmapText(guiFont);
-        hudText.setSize(guiFont.getCharSet().getRenderedSize());
         hudText.setText(computeNbTri() + " tris");
-        hudText.setLocalTranslation(cam.getWidth() / 2, hudText.getLineHeight(), 0);
+        hudText.setLocalTranslation(cam.getWidth() / 2f, hudText.getLineHeight(), 0);
         guiNode.attachChild(hudText);
 
-        inputManager.addListener(new ActionListener() {
-            @Override
-            public void onAction(String name, boolean isPressed, float tpf) {
-                if (isPressed) {
-                    if (name.equals("plus")) {
-                        reductionValue += 0.05f;
-                        updateLod();
-                    }
-                    if (name.equals("minus")) {
-                        reductionValue -= 0.05f;
-                        updateLod();
-                    }
-                    if (name.equals("wireFrame")) {
-                        wireFrame = !wireFrame;
-                        for (Geometry geom : listGeoms) {
-                            Material mat = geom.getMaterial();
-                            mat.getAdditionalRenderState().setWireframe(wireFrame);
-                        }
-                    }
-                }
+        // Register input mappings for user interaction
+        registerInputMappings();
+    }
+
+    @Override
+    public void onAction(String name, boolean isPressed, float tpf) {
+        if (!isPressed) return;
+
+        if (name.equals("plus")) {
+            reductionValue += 0.05f;
+            updateLod();
+
+        } else if (name.equals("minus")) {
+            reductionValue -= 0.05f;
+            updateLod();
+
+        } else if (name.equals("wireframe")) {
+            wireframe = !wireframe;
+            for (Geometry geom : listGeoms) {
+                Material mat = geom.getMaterial();
+                mat.getAdditionalRenderState().setWireframe(wireframe);
             }
-        }, "plus", "minus", "wireFrame");
+        }
+    }
 
-        inputManager.addMapping("plus", new KeyTrigger(KeyInput.KEY_ADD));
-        inputManager.addMapping("minus", new KeyTrigger(KeyInput.KEY_SUBTRACT));
-        inputManager.addMapping("wireFrame", new KeyTrigger(KeyInput.KEY_SPACE));
+    private void registerInputMappings() {
+        addMapping("plus", new KeyTrigger(KeyInput.KEY_P));
+        addMapping("minus", new KeyTrigger(KeyInput.KEY_L));
+        addMapping("wireframe", new KeyTrigger(KeyInput.KEY_SPACE));
     }
 
-    @Override
-    public void simpleUpdate(float tpf) {
+    private void addMapping(String mappingName, Trigger... triggers) {
+        inputManager.addMapping(mappingName, triggers);
+        inputManager.addListener(this, mappingName);
     }
 
     @Override
@@ -163,14 +177,20 @@ public class TestLodGeneration extends SimpleApplication {
     }
 
     private void updateLod() {
+        // Clamp the reduction value between 0.0 and 1.0 to ensure it's within valid range
         reductionValue = FastMath.clamp(reductionValue, 0.0f, 1.0f);
         makeLod(LodGenerator.TriangleReductionMethod.PROPORTIONAL, reductionValue, 1);
     }
 
+    /**
+     * Computes the total number of triangles currently displayed by all geometries.
+     * @return The total number of triangles.
+     */
     private int computeNbTri() {
         int nbTri = 0;
         for (Geometry geom : listGeoms) {
             Mesh mesh = geom.getMesh();
+            // Check if the mesh has LOD levels
             if (mesh.getNumLodLevels() > 0) {
                 nbTri += mesh.getLodLevel(lodLevel).getNumElements();
             } else {
@@ -180,24 +200,46 @@ public class TestLodGeneration extends SimpleApplication {
         return nbTri;
     }
 
-    private void makeLod(final LodGenerator.TriangleReductionMethod method, final float value, final int ll) {
+    /**
+     * Generates and applies LOD levels to the geometries in a background thread.
+     *
+     * @param reductionMethod     The triangle reduction method to use (e.g., PROPORTIONAL).
+     * @param reductionPercentage The percentage of triangles to reduce (0.0 to 1.0).
+     * @param targetLodLevel      The index of the LOD level to set active after generation.
+     */
+    private void makeLod(final LodGenerator.TriangleReductionMethod reductionMethod,
+                         final float reductionPercentage, final int targetLodLevel) {
+
+        // --- Asynchronous LOD Generation ---
+        // Execute the LOD generation process in the background thread pool.
         exec.execute(new Runnable() {
             @Override
             public void run() {
                 for (final Geometry geom : listGeoms) {
                     LodGenerator lodGenerator = new LodGenerator(geom);
-                    final VertexBuffer[] lods = lodGenerator.computeLods(method, value);
+                    final VertexBuffer[] lods = lodGenerator.computeLods(reductionMethod, reductionPercentage);
 
+                    // --- JME Thread Synchronization ---
+                    // Mesh modifications and scene graph updates must be done on the main thread.
                     enqueue(new Callable<Void>() {
                         @Override
                         public Void call() throws Exception {
                             geom.getMesh().setLodLevels(lods);
+
+                            // Reset lodLevel to 0 initially
                             lodLevel = 0;
-                            if (geom.getMesh().getNumLodLevels() > ll) {
-                                lodLevel = ll;
+                            // If the generated LOD levels are more than the target, set to target LOD
+                            if (geom.getMesh().getNumLodLevels() > targetLodLevel) {
+                                lodLevel = targetLodLevel;
                             }
                             geom.setLodLevel(lodLevel);
-                            hudText.setText(computeNbTri() + " tris");
+
+                            int nbTri = computeNbTri();
+                            hudText.setText(nbTri + " tris");
+
+                            // Print debug information to the console
+                            System.out.println(geom + " lodLevel: " + lodLevel + ", numLodLevels: " + geom.getMesh().getNumLodLevels()
+                                    + ", reductionValue: " + reductionValue + ", triangles: " + nbTri);
                             return null;
                         }
                     });

+ 6 - 3
jme3-lwjgl3/src/main/java/com/jme3/system/lwjgl/LwjglWindow.java

@@ -275,7 +275,7 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
         );
                 
         if (glfwPlatformSupported(GLFW_PLATFORM_WAYLAND)) {
-            
+
             /*
              * Change the platform GLFW uses to enable GLX on Wayland as long as you 
              * have XWayland (X11 compatibility)
@@ -283,9 +283,12 @@ public abstract class LwjglWindow extends LwjglContext implements Runnable {
             if (settings.isX11PlatformPreferred() && glfwPlatformSupported(GLFW_PLATFORM_X11)) {
                 glfwInitHint(GLFW_PLATFORM, GLFW_PLATFORM_X11);
             }
-            
+
+            // Disables the libdecor bar when creating a fullscreen context
+            // https://www.glfw.org/docs/latest/intro_guide.html#init_hints_wayland
+            glfwInitHint(GLFW_WAYLAND_LIBDECOR, settings.isFullscreen() ? GLFW_WAYLAND_DISABLE_LIBDECOR : GLFW_WAYLAND_PREFER_LIBDECOR);
         }
-        
+
         if (!glfwInit()) {
             throw new IllegalStateException("Unable to initialize GLFW");
         }