소스 검색

Merge master into lwjglx

wil 6 달 전
부모
커밋
b863da7195

+ 1 - 1
gradle/libs.versions.toml

@@ -3,7 +3,7 @@
 [versions]
 
 checkstyle = "9.3"
-lwjgl3 = "3.3.4"
+lwjgl3 = "3.3.6"
 nifty = "1.4.3"
 
 [libraries]

+ 11 - 2
jme3-android/src/main/java/com/jme3/system/android/OGLESContext.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
@@ -247,10 +247,19 @@ public class OGLESContext implements JmeContext, GLSurfaceView.Renderer, SoftTex
             }
 
             listener.destroy();
-
+            // releases the view holder from the Android Input Resources
+            // releasing the view enables the context instance to be
+            // reclaimed by the GC.
+            // if not released; it leads to a weak reference leak
+            // disabling the destruction of the Context View Holder.
+            androidInput.setView(null);
+
+            // nullifying the references
+            // signals their memory to be reclaimed
             listener = null;
             renderer = null;
             timer = null;
+            androidInput = null;
 
             // do android specific cleaning here
             logger.fine("Display destroyed.");

+ 38 - 23
jme3-android/src/main/java/com/jme3/view/surfaceview/JmeSurfaceView.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2009-2022 jMonkeyEngine
+ * Copyright (c) 2009-2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -239,11 +239,7 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
     }
 
     private void removeGLSurfaceView() {
-        ((Activity) getContext()).runOnUiThread(() -> {
-            if (glSurfaceView != null) {
-                JmeSurfaceView.this.removeView(glSurfaceView);
-            }
-        });
+        ((Activity) getContext()).runOnUiThread(() -> JmeSurfaceView.this.removeView(glSurfaceView));
     }
 
     @Override
@@ -265,19 +261,34 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
     public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
         switch (event) {
             case ON_DESTROY:
-                /*destroy only if the policy flag is enabled*/
-                if (destructionPolicy == DestructionPolicy.DESTROY_WHEN_FINISH) {
-                    legacyApplication.stop(!isGLThreadPaused());
-                }
+                // activity is off the foreground stack
+                // activity is being destructed completely as a result of Activity#finish()
+                // this is a killable automata state!
+                jmeSurfaceViewLogger.log(Level.INFO, "Hosting Activity has been destructed.");
                 break;
             case ON_PAUSE:
-                loseFocus();
+                // activity is still on the foreground stack but not
+                // on the topmost level or before transition to stopped/hidden or destroyed state
+                // as a result of dispatch to Activity#finish()
+                // activity is no longer visible and is out of foreground
+                if (((Activity) getContext()).isFinishing()) {
+                    if (destructionPolicy == DestructionPolicy.DESTROY_WHEN_FINISH) {
+                        legacyApplication.stop(!isGLThreadPaused());
+                    } else if (destructionPolicy == DestructionPolicy.KEEP_WHEN_FINISH) {
+                        jmeSurfaceViewLogger.log(Level.INFO, "Context stops, but game is still running.");
+                    }
+                } else {
+                    loseFocus();
+                }
                 break;
             case ON_RESUME:
+                // activity is back to the topmost of the
+                // foreground stack
                 gainFocus();
                 break;
             case ON_STOP:
-                jmeSurfaceViewLogger.log(Level.INFO, "Context stops, but game is still running");
+                // activity is out off the foreground stack or being destructed by a finishing dispatch
+                // this is a killable automata state!
                 break;
         }
     }
@@ -404,13 +415,13 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
 
     @Override
     public void destroy() {
-        /*skip the destroy block if the invoking instance is null*/
-        if (legacyApplication == null) {
-            return;
+        if (glSurfaceView != null) {
+            removeGLSurfaceView();
+        }
+        if (legacyApplication != null) {
+            legacyApplication.destroy();
         }
-        removeGLSurfaceView();
-        legacyApplication.destroy();
-        /*help the Dalvik Garbage collector to destruct the pointers, by making them nullptr*/
+        /*help the Dalvik Garbage collector to destruct the objects, by releasing their references*/
         /*context instances*/
         legacyApplication = null;
         appSettings = null;
@@ -430,10 +441,10 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
         onRendererCompleted = null;
         onExceptionThrown = null;
         onLayoutDrawn = null;
-        /*nullifying the static memory (pushing zero to registers to prepare for a clean use)*/
         GameState.setLegacyApplication(null);
         GameState.setFirstUpdatePassed(false);
-        jmeSurfaceViewLogger.log(Level.INFO, "Context and Game have been destructed");
+        JmeAndroidSystem.setView(null);
+        jmeSurfaceViewLogger.log(Level.INFO, "Context and Game have been destructed.");
     }
 
     @Override
@@ -516,11 +527,13 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
             /*register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
             if (getContext() instanceof LifecycleOwner) {
                 ((LifecycleOwner) getContext()).getLifecycle().addObserver(JmeSurfaceView.this);
+                jmeSurfaceViewLogger.log(Level.INFO, "Command binding SurfaceView to the Activity Lifecycle.");
             }
         } else {
             /*un-register this Ui Component as an observer to the context of jmeSurfaceView only if this context is a LifeCycleOwner*/
             if (getContext() instanceof LifecycleOwner) {
                 ((LifecycleOwner) getContext()).getLifecycle().removeObserver(JmeSurfaceView.this);
+                jmeSurfaceViewLogger.log(Level.INFO, "Command removing SurfaceView from the Activity Lifecycle.");
             }
         }
     }
@@ -917,7 +930,7 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
     }
 
     /**
-     * Determines whether the app context would be destructed
+     * Determines whether the app context would be destructed as a result of dispatching {@link Activity#finish()}
      * with the holder activity context in case of {@link DestructionPolicy#DESTROY_WHEN_FINISH} or be
      * spared for a second use in case of {@link DestructionPolicy#KEEP_WHEN_FINISH}.
      * Default value is : {@link DestructionPolicy#DESTROY_WHEN_FINISH}.
@@ -926,12 +939,14 @@ public class JmeSurfaceView extends RelativeLayout implements SystemListener, Di
      */
     public enum DestructionPolicy {
         /**
-         * Finishes the game context with the activity context (ignores the static memory {@link GameState#legacyApplication}).
+         * Finishes the game context with the activity context (ignores the static memory {@link GameState#legacyApplication})
+         * as a result of dispatching {@link Activity#finish()}.
          */
         DESTROY_WHEN_FINISH,
         /**
          * Spares the game context inside a static memory {@link GameState#legacyApplication}
-         * when the activity context is destroyed, but the app stills in the background.
+         * when the activity context is destroyed dispatching {@link Activity#finish()}, but the {@link android.app.Application}
+         * stills in the background.
          */
         KEEP_WHEN_FINISH
     }

+ 6 - 9
jme3-core/src/main/java/com/jme3/anim/AnimComposer.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
@@ -313,24 +313,21 @@ public class AnimComposer extends AbstractControl {
     /**
      * Add a layer to this composer.
      *
-     * @param name The desired name for the new layer
-     * @param mask The desired mask for the new layer (alias created)
-     * @return a new layer
+     * @param name the desired name for the new layer
+     * @param mask the desired mask for the new layer (alias created)
      */
-    public AnimLayer makeLayer(String name, AnimationMask mask) {
+    public void makeLayer(String name, AnimationMask mask) {
         AnimLayer l = new AnimLayer(name, mask);
         layers.put(name, l);
-        return l;
     }
 
     /**
      * Remove specified layer. This will stop the current action on this layer.
      *
      * @param name The name of the layer to remove.
-     * @return The removed layer.
      */
-    public AnimLayer removeLayer(String name) {
-        return layers.remove(name);
+    public void removeLayer(String name) {
+        layers.remove(name);
     }
 
     /**

+ 73 - 135
jme3-core/src/main/java/com/jme3/anim/SingleLayerInfluenceMask.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2024 jMonkeyEngine
+ * Copyright (c) 2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -31,188 +31,126 @@
  */
 package com.jme3.anim;
 
-import com.jme3.scene.Spatial;
+import com.jme3.export.InputCapsule;
+import com.jme3.export.JmeExporter;
+import com.jme3.export.JmeImporter;
+import com.jme3.export.OutputCapsule;
+import java.io.IOException;
 
 /**
- * Mask that excludes joints from participating in the layer
- * if a higher layer is using those joints in an animation.
- * 
+ * Mask that excludes joints from participating in the layer if a higher layer
+ * is using those joints in an animation.
+ *
  * @author codex
  */
 public class SingleLayerInfluenceMask extends ArmatureMask {
-    
-    private final String layer;
-    private final AnimComposer anim;
-    private final SkinningControl skin;
-    private boolean checkUpperLayers = true;
-    
-    /**
-     * @param layer The layer this mask is targeted for. It is important
-     * that this match the name of the layer this mask is (or will be) part of. You
-     * can use {@link makeLayer} to ensure this.
-     * @param spatial Spatial containing necessary controls ({@link AnimComposer} and {@link SkinningControl})
-     */
-    public SingleLayerInfluenceMask(String layer, Spatial spatial) {
-        super();
-        this.layer = layer;
-        anim = spatial.getControl(AnimComposer.class);
-        skin = spatial.getControl(SkinningControl.class);
-    }
-    /**
-     * @param layer The layer this mask is targeted for. It is important
-     * that this match the name of the layer this mask is (or will be) part of. You
-     * can use {@link makeLayer} to ensure this.
-     * @param anim anim composer this mask is assigned to
-     * @param skin skinning control complimenting the anim composer.
-     */
-    public SingleLayerInfluenceMask(String layer, AnimComposer anim, SkinningControl skin) {
-        super();
-        this.layer = layer;
-        this.anim = anim;
-        this.skin = skin;
-    }
-    
-    /**
-     * Makes a layer from this mask.
-     */
-    public void makeLayer() {
-        anim.makeLayer(layer, this);
-    }
-    
-    /**
-     * Adds all joints to this mask.
-     * @return this.instance
-     */
-    public SingleLayerInfluenceMask addAll() {
-        for (Joint j : skin.getArmature().getJointList()) {
-            super.addBones(skin.getArmature(), j.getName());
-        }
-        return this;
-    }
+
+    private String targetLayer;
+    private AnimComposer animComposer;
     
     /**
-     * Adds the given joint and all its children to this mask.
-     * @param joint
-     * @return this instance
+     * For serialization only. Do not use
      */
-    public SingleLayerInfluenceMask addFromJoint(String joint) {
-        super.addFromJoint(skin.getArmature(), joint);
-        return this;
+    protected SingleLayerInfluenceMask() {
     }
     
     /**
-     * Adds the given joints to this mask.
-     * @param joints
-     * @return this instance
+     * Instantiate a mask that affects all joints in the specified Armature.
+     *
+     * @param targetLayer  The layer this mask is targeted for.
+     * @param animComposer The animation composer associated with this mask.
+     * @param armature     The Armature containing the joints.
      */
-    public SingleLayerInfluenceMask addJoints(String... joints) {
-        super.addBones(skin.getArmature(), joints);
-        return this;
+    public SingleLayerInfluenceMask(String targetLayer, AnimComposer animComposer, Armature armature) {
+        super(armature);
+        this.targetLayer = targetLayer;
+        this.animComposer = animComposer;
     }
     
     /**
-     * Makes this mask check if each joint is being used by a higher layer
-     * before it uses them.
-     * <p>Not checking is more efficient, but checking can avoid some
-     * interpolation issues between layers. Default=true
-     * @param check 
-     * @return this instance
+     * Instantiate a mask that affects no joints.
+     * 
+     * @param targetLayer  The layer this mask is targeted for.
+     * @param animComposer The animation composer associated with this mask.
      */
-    public SingleLayerInfluenceMask setCheckUpperLayers(boolean check) {
-        checkUpperLayers = check;
-        return this;
+    public SingleLayerInfluenceMask(String targetLayer, AnimComposer animComposer) {
+        this.targetLayer = targetLayer;
+        this.animComposer = animComposer;
     }
-    
+
     /**
      * Get the layer this mask is targeted for.
-     * <p>It is extremely important that this value match the actual layer
-     * this is included in, because checking upper layers may not work if
-     * they are different.
-     * @return target layer
+     *
+     * @return The target layer
      */
     public String getTargetLayer() {
-        return layer;
+        return targetLayer;
     }
-    
-    /**
-     * Get the {@link AnimComposer} this mask is for.
-     * @return anim composer
-     */
-    public AnimComposer getAnimComposer() {
-        return anim;
-    }
-    
+
     /**
-     * Get the {@link SkinningControl} this mask is for.
-     * @return skinning control
+     * Sets the animation composer for this mask.
+     *
+     * @param animComposer The new animation composer.
      */
-    public SkinningControl getSkinningControl() {
-        return skin;
+    public void setAnimComposer(AnimComposer animComposer) {
+        this.animComposer = animComposer;
     }
-    
+
     /**
-     * Returns true if this mask is checking upper layers for joint use.
-     * @return 
+     * Checks if the specified target is contained within this mask.
+     *
+     * @param target The target to check.
+     * @return True if the target is contained within this mask, false otherwise.
      */
-    public boolean isCheckUpperLayers() {
-        return checkUpperLayers;
-    }
-    
     @Override
     public boolean contains(Object target) {
-        return simpleContains(target) && (!checkUpperLayers || !isAffectedByUpperLayers(target));
+        return simpleContains(target) && (animComposer == null || !isAffectedByUpperLayers(target));
     }
-    
+
     private boolean simpleContains(Object target) {
         return super.contains(target);
     }
-    
+
     private boolean isAffectedByUpperLayers(Object target) {
         boolean higher = false;
-        for (String name : anim.getLayerNames()) {
-            if (name.equals(layer)) {
+        for (String layerName : animComposer.getLayerNames()) {
+            if (layerName.equals(targetLayer)) {
                 higher = true;
                 continue;
             }
             if (!higher) {
                 continue;
             }
-            AnimLayer lyr = anim.getLayer(name);  
-            // if there is no action playing, no joints are used, so we can skip
-            if (lyr.getCurrentAction() == null) continue;
-            if (lyr.getMask() instanceof SingleLayerInfluenceMask) {
-                // dodge some needless recursion by calling a simpler method
-                if (((SingleLayerInfluenceMask)lyr.getMask()).simpleContains(target)) {
+            
+            AnimLayer animLayer = animComposer.getLayer(layerName);
+            if (animLayer.getCurrentAction() != null) {
+                AnimationMask mask = animLayer.getMask();
+                
+                if (mask instanceof SingleLayerInfluenceMask) {
+                    // dodge some needless recursion by calling a simpler method
+                    if (((SingleLayerInfluenceMask) mask).simpleContains(target)) {
+                        return true;
+                    }
+                } else if (mask != null && mask.contains(target)) {
                     return true;
                 }
             }
-            else if (lyr.getMask().contains(target)) {
-                return true;
-            }
         }
         return false;
     }
     
-    /**
-     * Creates an {@code SingleLayerInfluenceMask} for all joints.
-     * @param layer layer the returned mask is, or will be, be assigned to
-     * @param spatial spatial containing anim composer and skinning control
-     * @return new mask
-     */
-    public static SingleLayerInfluenceMask all(String layer, Spatial spatial) {
-        return new SingleLayerInfluenceMask(layer, spatial).addAll();
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(targetLayer, "targetLayer", null);
     }
-    
-    /**
-     * Creates an {@code SingleLayerInfluenceMask} for all joints.
-     * @param layer layer the returned mask is, or will be, assigned to
-     * @param anim anim composer
-     * @param skin skinning control
-     * @return new mask
-     */
-    public static SingleLayerInfluenceMask all(String layer, AnimComposer anim, SkinningControl skin) {
-        return new SingleLayerInfluenceMask(layer, anim, skin).addAll();
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule ic = im.getCapsule(this);
+        targetLayer = ic.readString("targetLayer", null);
     }
-    
-}
 
+}

+ 85 - 0
jme3-core/src/main/java/com/jme3/anim/tween/action/BlendSpace.java

@@ -1,10 +1,95 @@
+/*
+ * 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.anim.tween.action;
 
+/**
+ * A provider interface which provides a value {@link BlendSpace#getWeight()} to control the blending between 2 successive actions in a {@link BlendAction}.
+ * The blending weight is a read-only value, and it can be manipulated using the arbitrary value {@link BlendSpace#setValue(float)} during the application runtime.
+ * 
+ * <p>
+ * Notes about the blending action and its relations with the blending weight:
+ * <ul>
+ * <li> Blending is the action of mixing between 2 successive animation {@link BlendableAction}s by interpolating their transforms and 
+ * then applying the result on the assigned {@link HasLocalTransform} object, the {@link BlendSpace} provides this blending action with a blend weight value. </li>
+ * <li> The blend weight is the value for the interpolation for the target transforms. </li>
+ * <li> The blend weight value must be in this interval [0, 1]. </li>
+ * </ul>
+ * </p>
+ * 
+ * <p>
+ * Different blending weight case scenarios managed by {@link BlendAction} internally:
+ * <ul>
+ * <li> In case of (0 < Blending weight < 1), the blending is executed each update among 2 actions, the first action will use 
+ * a blend value of 1 and the second action will use the blend space weight as a value for the interpolation. </li>
+ * <li> In case of (Blending weight = 0), the blending hasn't started yet, only the first action will be interpolated at (weight = 1). </li>
+ * <li> In case of (Blending weight = 1), the blending is finished and only the second action will continue to run at (weight = 1). </li>
+ * </ul>
+ * </p>
+ * 
+ * <p>
+ * Notes about the blending weight value:
+ * <ul>
+ * <li> Negative values and values greater than 1 aren't allowed (i.e., extrapolations aren't allowed). </li>
+ * <li> For more details, see {@link BlendAction#doInterpolate(double)} and {@link BlendAction#collectTransform(HasLocalTransform, Transform, float, BlendableAction)}. </li>
+ * </ul>
+ * </p> 
+ *
+ * Created by Nehon.
+ * @see LinearBlendSpace an example of blendspace implementation
+ */
 public interface BlendSpace {
 
+    /**
+     * Adjusts the target blend action instance that will utilize the blend weight value provided by this blend-space implementation.
+     * 
+     * @param action the blend action instance that will utilize this blend-space (not null).
+     */
     public void setBlendAction(BlendAction action);
 
+    /**
+     * Provides the blend weight value to the assigned {@link BlendAction} instance,
+     * this value will be used for interpolating a collection of actions' transformations (i.e., keyframes).
+     * 
+     * @return the blending weight value in the range from 0 to 1, 
+     *         negative values and values above 1 aren't allowed.
+     * @see LinearBlendSpace#getWeight()
+     */
     public float getWeight();
 
+    /**
+     * An arbitrary value used for adjusting the blending weight value.
+     * 
+     * @param value the value in floats.
+     * @see LinearBlendSpace#setValue(float)
+     */
     public void setValue(float value);
 }

+ 91 - 0
jme3-core/src/main/java/com/jme3/input/RawInputListenerAdapter.java

@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2009-2024 jMonkeyEngine
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ *   notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ *   notice, this list of conditions and the following disclaimer in the
+ *   documentation and/or other materials provided with the distribution.
+ *
+ * * Neither the name of 'jMonkeyEngine' nor the names of its contributors
+ *   may be used to endorse or promote products derived from this software
+ *   without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+ * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.jme3.input;
+
+import com.jme3.input.event.JoyAxisEvent;
+import com.jme3.input.event.JoyButtonEvent;
+import com.jme3.input.event.KeyInputEvent;
+import com.jme3.input.event.MouseButtonEvent;
+import com.jme3.input.event.MouseMotionEvent;
+import com.jme3.input.event.TouchEvent;
+
+/**
+ * An abstract adapter class for {@link RawInputListener}.
+ *
+ * This class provides empty implementations for all methods in the
+ * {@link RawInputListener} interface, making it easier to create custom
+ * listeners by extending this class and overriding only the methods of
+ * interest.
+ */
+public abstract class RawInputListenerAdapter implements RawInputListener {
+
+    @Override
+    public void beginInput() {
+        // No-op implementation
+    }
+
+    @Override
+    public void endInput() {
+        // No-op implementation
+    }
+
+    @Override
+    public void onJoyAxisEvent(JoyAxisEvent evt) {
+        // No-op implementation
+    }
+
+    @Override
+    public void onJoyButtonEvent(JoyButtonEvent evt) {
+        // No-op implementation
+    }
+
+    @Override
+    public void onMouseMotionEvent(MouseMotionEvent evt) {
+        // No-op implementation
+    }
+
+    @Override
+    public void onMouseButtonEvent(MouseButtonEvent evt) {
+        // No-op implementation
+    }
+
+    @Override
+    public void onKeyEvent(KeyInputEvent evt) {
+        // No-op implementation
+    }
+
+    @Override
+    public void onTouchEvent(TouchEvent evt) {
+        // No-op implementation
+    }
+
+}

+ 25 - 4
jme3-core/src/main/java/com/jme3/math/Spline.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
@@ -480,7 +480,18 @@ public class Spline implements Savable {
         oc.writeSavableArrayList((ArrayList) CRcontrolPoints, "CRControlPoints", null);
         oc.write(curveTension, "curveTension", 0.5f);
         oc.write(cycle, "cycle", false);
-        oc.writeSavableArrayList((ArrayList<Float>) knots, "knots", null);
+
+        float[] knotArray;
+        if (knots == null) {
+            knotArray = null;
+        } else {
+            knotArray = new float[knots.size()];
+            for (int i = 0; i < knotArray.length; ++i) {
+                knotArray[i] = knots.get(i);
+            }
+        }
+        oc.write(knotArray, "knots", null);
+
         oc.write(weights, "weights", null);
         oc.write(basisFunctionDegree, "basisFunctionDegree", 0);
     }
@@ -506,12 +517,22 @@ public class Spline implements Savable {
                 segmentsLength.add(list[i]);
             }
         }
-        type = in.readEnum("pathSplineType", SplineType.class, SplineType.CatmullRom);
+        type = in.readEnum("type", SplineType.class, SplineType.CatmullRom);
         totalLength = in.readFloat("totalLength", 0);
         CRcontrolPoints = in.readSavableArrayList("CRControlPoints", null);
         curveTension = in.readFloat("curveTension", 0.5f);
         cycle = in.readBoolean("cycle", false);
-        knots = in.readSavableArrayList("knots", null);
+
+        float[] knotArray = in.readFloatArray("knots", null);
+        if (knotArray == null) {
+            this.knots = null;
+        } else {
+            this.knots = new ArrayList<>(knotArray.length);
+            for (float knot : knotArray) {
+                knots.add(knot);
+            }
+        }
+
         weights = in.readFloatArray("weights", null);
         basisFunctionDegree = in.readInt("basisFunctionDegree", 0);
     }

+ 4 - 30
jme3-core/src/main/resources/Common/MatDefs/Light/Lighting.frag

@@ -6,25 +6,9 @@
     #import "Common/ShaderLib/Lighting.glsllib"
 #endif
 
-// fog - jayfella
 #ifdef USE_FOG
-#import "Common/ShaderLib/MaterialFog.glsllib"
-varying float fog_distance;
-uniform vec4 m_FogColor;
-
-#ifdef FOG_LINEAR
-uniform vec2 m_LinearFog;
-#endif
-
-#ifdef FOG_EXP
-uniform float m_ExpFog;
-#endif
-
-#ifdef FOG_EXPSQ
-uniform float m_ExpSqFog;
-#endif
-
-#endif // end fog
+    #import "Common/ShaderLib/MaterialFog.glsllib"
+#endif 
 
 varying vec2 texCoord;
 #ifdef SEPARATE_TEXCOORD
@@ -231,21 +215,11 @@ void main(){
                            SpecularSum2.rgb * specularColor.rgb * vec3(light.y);
     #endif
 
-
     // add fog after the lighting because shadows will cause the fog to darken
     // which just results in the geometry looking like it's changed color
     #ifdef USE_FOG
-        #ifdef FOG_LINEAR
-            gl_FragColor = getFogLinear(gl_FragColor, m_FogColor, m_LinearFog.x, m_LinearFog.y, fog_distance);
-        #endif
-        #ifdef FOG_EXP
-            gl_FragColor = getFogExp(gl_FragColor, m_FogColor, m_ExpFog, fog_distance);
-        #endif
-        #ifdef FOG_EXPSQ
-            gl_FragColor = getFogExpSquare(gl_FragColor, m_FogColor, m_ExpSqFog, fog_distance);
-        #endif
-    #endif // end fog
-
+        gl_FragColor = MaterialFog_calculateFogColor(vec4(gl_FragColor));
+    #endif
 
     gl_FragColor.a = alpha;
 }

+ 3 - 4
jme3-core/src/main/resources/Common/ShaderLib/MaterialFog.glsllib

@@ -1,7 +1,7 @@
+//author @jayfella
 #ifndef __MATERIAL_FOG_UTIL__
     #define __MATERIAL_FOG_UTIL__
 
-
     vec4 getFogLinear(in vec4 diffuseColor, in vec4 fogColor, in float start, in float end, in float distance) {
 
         float fogFactor = (end - distance) / (end - start);
@@ -39,8 +39,7 @@
 
         #ifdef FOG_EXPSQ
             uniform float m_ExpSqFog;        
-        #endif
-        
+        #endif        
 
         vec4 MaterialFog_calculateFogColor(in vec4 fragColor){
             #ifdef FOG_LINEAR
@@ -54,7 +53,7 @@
             #endif
 
             return fragColor;
-        }           
+        }          
 
     #endif   
 #endif

+ 28 - 10
jme3-core/src/main/resources/Common/ShaderLib/module/pbrlighting/PBRLightingUtils.glsllib

@@ -236,7 +236,7 @@
             Math_lengthAndNormalize(light.vector,dist,L);
 
             float invRange=light.invRadius; // position.w
-            const float light_threshold=0.01;
+            const float light_threshold = 0.01;
 
             #ifdef SRGB
                 light.fallOff = (1.0 - invRange * dist) / (1.0 + invRange * dist * dist); // lightDir.w
@@ -272,11 +272,26 @@
 
     PBRSurface PBRLightingUtils_createPBRSurface(in vec3 wViewDir){
 
-        PBRSurface surface;
+        PBRSurface surface; //creates a new PBRSurface
+        
         surface.position = wPosition;
         surface.viewDir = wViewDir;
         surface.geometryNormal = normalize(wNormal);
 
+        //set default values
+        surface.hasTangents = false;
+        surface.hasBasicLightMap = false;        
+        surface.albedo = vec3(1.0);
+        surface.normal = surface.geometryNormal;
+        surface.emission = vec3(0.0);        
+        surface.ao = vec3(1.0);
+        surface.lightMapColor = vec3(0.0);
+        surface.alpha = 1.0;
+        surface.roughness = 1.0;
+        surface.metallic = 0.0;
+        surface.alpha = 1.0;
+        surface.exposure = 1.0;        
+        
         return surface;
     }
 
@@ -285,9 +300,9 @@
         
         void PBRLightingUtils_readPBRSurface(inout PBRSurface surface){
 
-            surface.bakedLightContribution = vec3(0);
-            surface.directLightContribution = vec3(0);
-            surface.envLightContribution = vec3(0);
+            surface.bakedLightContribution = vec3(0.0);
+            surface.directLightContribution = vec3(0.0);
+            surface.envLightContribution = vec3(0.0);
 
             #ifdef ENABLE_PBRLightingUtils_getWorldTangent
                 vec3 tan = normalize(wTangent.xyz); 
@@ -297,7 +312,7 @@
 
             
             #if (defined(PARALLAXMAP) || (defined(NORMALMAP_PARALLAX) && defined(NORMALMAP)))
-                vec3 vViewDir =  wViewDir * surface.tbnMat;  
+                vec3 vViewDir =  surface.viewDir * surface.tbnMat;  
                 #ifdef STEEP_PARALLAX
                     #ifdef NORMALMAP_PARALLAX
                         //parallax map is stored in the alpha channel of the normal map         
@@ -429,7 +444,7 @@
                 #endif
                 surface.emission = emissive.rgb * pow(emissive.a, m_EmissivePower) * m_EmissiveIntensity;
             #else 
-                surface.emission = vec3(0);
+                surface.emission = vec3(0.0);
             #endif
 
             PBRLightingUtils_readSunLightExposureParams(surface);
@@ -573,8 +588,8 @@
 
 
             #if NB_PROBES > 0
-                float probeNdfSum=0;
-                float invProbeNdfSum=0;    
+                float probeNdfSum = 0.0;
+                float invProbeNdfSum = 0.0;    
 
                 #for i=1..4 ( #if NB_PROBES >= $i $0 #endif )
                     vec3 probeColor$i;
@@ -607,7 +622,7 @@
                 #endfor
 
                 #if NB_PROBES > 1
-                    float probeWeightSum=0;
+                    float probeWeightSum = 0.0;
                     #for i=1..4 ( #if NB_PROBES >= $i $0 #endif )
                         float probeWeight$i = ((1.0 - (probeNdf$i / probeNdfSum)) / (NB_PROBES - 1)) *  ( probeInvNdf$i / invProbeNdfSum);
                         probeWeightSum += probeWeight$i;    
@@ -650,6 +665,9 @@
         else if(debugValuesMode == 7){
             outputColorForLayer.rgb = vec3(surface.alpha);          
         } 
+        else if(debugValuesMode == 8){
+            outputColorForLayer.rgb = vec3(surface.geometryNormal);          
+        }
 
         if(debugValuesMode >= 0){
             gl_FragColor.a = 1.0;

+ 175 - 0
jme3-core/src/test/java/com/jme3/math/SplineTest.java

@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 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.math;
+
+import com.jme3.asset.AssetManager;
+import com.jme3.asset.DesktopAssetManager;
+import com.jme3.export.binary.BinaryExporter;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Verifies that the {@link Spline} class works correctly.
+ *
+ * @author Stephen Gold
+ */
+public class SplineTest {
+    // *************************************************************************
+    // fields
+
+    private static final AssetManager assetManager = new DesktopAssetManager();
+    // *************************************************************************
+    // tests
+
+    /**
+     * Verifies that spline serialization/deserialization works correctly.
+     */
+    @Test
+    public void saveAndLoadSplines() {
+        // Serialize and deserialize a Bezier spline:
+        {
+            Vector3f[] controlPoints1 = {
+                new Vector3f(0f, 1f, 0f), new Vector3f(1f, 2f, 1f),
+                new Vector3f(1.5f, 1.5f, 1.5f), new Vector3f(2f, 0f, 1f)
+            };
+
+            Spline test1 = new Spline(
+                    Spline.SplineType.Bezier, controlPoints1, 0.1f, true);
+            Spline copy1 = BinaryExporter.saveAndLoad(assetManager, test1);
+            assertSplineEquals(test1, copy1);
+        }
+
+        // Serialize and deserialize a NURB spline:
+        {
+            List<Vector4f> controlPoints2 = new ArrayList<>(5);
+            controlPoints2.add(new Vector4f(0f, 1f, 2f, 3f));
+            controlPoints2.add(new Vector4f(3f, 1f, 4f, 0f));
+            controlPoints2.add(new Vector4f(2f, 5f, 3f, 0f));
+            controlPoints2.add(new Vector4f(3f, 2f, 3f, 1f));
+            controlPoints2.add(new Vector4f(0.5f, 1f, 0.6f, 5f));
+            List<Float> nurbKnots = new ArrayList<>(6);
+            nurbKnots.add(0.2f);
+            nurbKnots.add(0.3f);
+            nurbKnots.add(0.4f);
+            nurbKnots.add(0.43f);
+            nurbKnots.add(0.51f);
+            nurbKnots.add(0.52f);
+
+            Spline test2 = new Spline(controlPoints2, nurbKnots);
+            Spline copy2 = BinaryExporter.saveAndLoad(assetManager, test2);
+            assertSplineEquals(test2, copy2);
+        }
+
+        // Serialize and deserialize a Catmull-Rom spline:
+        {
+            List<Vector3f> controlPoints3 = new ArrayList<>(6);
+            controlPoints3.add(new Vector3f(0f, 1f, 2f));
+            controlPoints3.add(new Vector3f(3f, -1f, 4f));
+            controlPoints3.add(new Vector3f(2f, 5f, 3f));
+            controlPoints3.add(new Vector3f(3f, -2f, 3f));
+            controlPoints3.add(new Vector3f(0.5f, 1f, 0.6f));
+            controlPoints3.add(new Vector3f(-0.5f, 4f, 0.2f));
+
+            Spline test3 = new Spline(
+                    Spline.SplineType.CatmullRom, controlPoints3, 0.01f, false);
+            Spline copy3 = BinaryExporter.saveAndLoad(assetManager, test3);
+            assertSplineEquals(test3, copy3);
+        }
+
+        // Serialize and deserialize a linear spline:
+        {
+            List<Vector3f> controlPoints4 = new ArrayList<>(3);
+            controlPoints4.add(new Vector3f(3f, -1f, 4f));
+            controlPoints4.add(new Vector3f(2f, 0f, 3f));
+            controlPoints4.add(new Vector3f(3f, -2f, 3f));
+
+            Spline test4 = new Spline(
+                    Spline.SplineType.Linear, controlPoints4, 0f, true);
+            Spline copy4 = BinaryExporter.saveAndLoad(assetManager, test4);
+            assertSplineEquals(test4, copy4);
+        }
+
+        // Serialize and deserialize a default spline:
+        {
+            Spline test5 = new Spline();
+            Spline copy5 = BinaryExporter.saveAndLoad(assetManager, test5);
+            assertSplineEquals(test5, copy5);
+        }
+    }
+    // *************************************************************************
+    // private helper methods
+
+    /**
+     * Verify that the specified lists are equivalent.
+     *
+     * @param s1 the first list to compare (may be null, unaffected)
+     * @param s2 the 2nd list to compare (may be null, unaffected)
+     */
+    private static void assertListEquals(List<?> a1, List<?> a2) {
+        if (a1 != a2) {
+            Assert.assertEquals(a1.size(), a2.size());
+            for (int i = 0; i < a1.size(); ++i) {
+                Assert.assertEquals(a1.get(i), a2.get(i));
+            }
+        }
+    }
+
+    /**
+     * Verify that the specified splines are equivalent.
+     *
+     * @param s1 the first spline to compare (not null, unaffected)
+     * @param s2 the 2nd split to compare (not null, unaffected)
+     */
+    private static void assertSplineEquals(Spline s1, Spline s2) {
+        Assert.assertEquals(s1.getType(), s2.getType());
+        Assert.assertEquals(s1.isCycle(), s2.isCycle());
+
+        Assert.assertEquals(
+                s1.getBasisFunctionDegree(), s2.getBasisFunctionDegree());
+        assertListEquals(s1.getControlPoints(), s2.getControlPoints());
+        Assert.assertEquals(s1.getCurveTension(), s2.getCurveTension(), 0f);
+        assertListEquals(s1.getKnots(), s2.getKnots());
+
+        if (s1.getType() == Spline.SplineType.Nurb) {
+            // These methods throw NPEs on non-NURB splines.
+            Assert.assertEquals(s1.getMaxNurbKnot(), s2.getMaxNurbKnot(), 0f);
+            Assert.assertEquals(s1.getMinNurbKnot(), s2.getMinNurbKnot(), 0f);
+        }
+
+        assertListEquals(s1.getSegmentsLength(), s2.getSegmentsLength());
+        Assert.assertEquals(
+                s1.getTotalLength(), s2.getTotalLength(), 0f);
+        Assert.assertArrayEquals(s1.getWeights(), s2.getWeights(), 0f);
+    }
+}

+ 108 - 57
jme3-examples/src/main/java/jme3test/model/anim/TestSingleLayerInfluenceMask.java

@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2024 jMonkeyEngine
+ * Copyright (c) 2025 jMonkeyEngine
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
@@ -32,42 +32,47 @@
 package jme3test.model.anim;
 
 import com.jme3.anim.AnimComposer;
+import com.jme3.anim.AnimLayer;
+import com.jme3.anim.Armature;
 import com.jme3.anim.ArmatureMask;
 import com.jme3.anim.SingleLayerInfluenceMask;
 import com.jme3.anim.SkinningControl;
-import com.jme3.anim.tween.action.ClipAction;
+import com.jme3.anim.tween.action.BlendableAction;
 import com.jme3.anim.util.AnimMigrationUtils;
 import com.jme3.app.SimpleApplication;
+import com.jme3.export.binary.BinaryExporter;
 import com.jme3.font.BitmapFont;
 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.light.DirectionalLight;
-import com.jme3.math.ColorRGBA;
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Spatial;
 
 /**
  * Tests {@link SingleLayerInfluenceMask}.
- * 
- * The test runs two simultaneous looping actions on seperate layers.
+ *
+ * The test runs two simultaneous looping actions on separate layers.
  * <p>
  * The test <strong>fails</strong> if the visible dancing action does <em>not</em>
- * loop seamlessly when using SingleLayerInfluenceMasks. Note that the action is not
- * expected to loop seamlessly when <em>not</em> using SingleLayerArmatureMasks.
+ * loop seamlessly when using SingleLayerInfluenceMasks. Note that the action is
+ * not expected to loop seamlessly when <em>not</em> using
+ * SingleLayerArmatureMasks.
  * <p>
- * Press the spacebar to switch between using SingleLayerInfluenceMasks and masks
- * provided by {@link ArmatureMask}.
- * 
+ * Press the spacebar to switch between using SingleLayerInfluenceMasks and
+ * masks provided by {@link ArmatureMask}.
+ *
  * @author codex
  */
 public class TestSingleLayerInfluenceMask extends SimpleApplication implements ActionListener {
-    
+
     private Spatial model;
-    private AnimComposer anim;
-    private SkinningControl skin;
-    private boolean useSLIMask = true;
+    private AnimComposer animComposer;
+    private SkinningControl skinningControl;
+    private final String idleLayer = "idleLayer";
+    private final String danceLayer = "danceLayer";
+    private boolean useSingleLayerInfMask = true;
     private BitmapText display;
 
     public static void main(String[] args) {
@@ -77,74 +82,120 @@ public class TestSingleLayerInfluenceMask extends SimpleApplication implements A
 
     @Override
     public void simpleInitApp() {
-        
-        flyCam.setMoveSpeed(30f);
 
+        flyCam.setMoveSpeed(30f);
+        
         DirectionalLight dl = new DirectionalLight();
         dl.setDirection(new Vector3f(-0.1f, -0.7f, -1).normalizeLocal());
-        dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f));
         rootNode.addLight(dl);
-        
+
         BitmapFont font = assetManager.loadFont("Interface/Fonts/Default.fnt");
         display = new BitmapText(font);
         display.setSize(font.getCharSet().getRenderedSize());
         display.setText("");
-        display.setLocalTranslation(5, context.getSettings().getHeight()-5, 0);
+        display.setLocalTranslation(5, context.getSettings().getHeight() - 5, 0);
         guiNode.attachChild(display);
- 
-        inputManager.addMapping("reset", new KeyTrigger(KeyInput.KEY_SPACE));
-        inputManager.addListener(this, "reset");
-        
+
+        inputManager.addMapping("SWITCH_MASKS", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addListener(this, "SWITCH_MASKS");
+
         setupModel();
-        
+        createAnimMasks();
+        testSerialization();
+        playAnimations();
+        updateUI();
     }
+
     @Override
     public void simpleUpdate(float tpf) {
         cam.lookAt(model.getWorldTranslation(), Vector3f.UNIT_Y);
     }
+
     @Override
     public void onAction(String name, boolean isPressed, float tpf) {
-        if (name.equals("reset") && isPressed) {
-            model.removeFromParent();
-            setupModel();
+        if (name.equals("SWITCH_MASKS") && isPressed) {
+            useSingleLayerInfMask = !useSingleLayerInfMask;
+            animComposer.removeLayer(idleLayer);
+            animComposer.removeLayer(danceLayer);
+            
+            createAnimMasks();
+            playAnimations();
+            updateUI();
         }
     }
-    
+
+    /**
+     * Sets up the model by loading it, migrating animations, and attaching it to
+     * the root node.
+     */
     private void setupModel() {
-        
         model = assetManager.loadModel("Models/Sinbad/SinbadOldAnim.j3o");
+        // Migrate the model's animations to the new system
         AnimMigrationUtils.migrate(model);
-        anim = model.getControl(AnimComposer.class);
-        skin = model.getControl(SkinningControl.class);
-        
-        if (useSLIMask) {
-            SingleLayerInfluenceMask walkLayer = new SingleLayerInfluenceMask("idleLayer", anim, skin);
-            walkLayer.addAll();
-            walkLayer.makeLayer();
-            SingleLayerInfluenceMask danceLayer = new SingleLayerInfluenceMask("danceLayer", anim, skin);
-            danceLayer.addAll();
-            danceLayer.makeLayer();
+        rootNode.attachChild(model);
+
+        animComposer = model.getControl(AnimComposer.class);
+        skinningControl = model.getControl(SkinningControl.class);
+
+        ((BlendableAction) animComposer.action("Dance")).setMaxTransitionWeight(.9f);
+        ((BlendableAction) animComposer.action("IdleTop")).setMaxTransitionWeight(.8f);
+    }
+
+    /**
+     * Creates animation masks for the idle and dance layers.
+     */
+    private void createAnimMasks() {
+        Armature armature = skinningControl.getArmature();
+        ArmatureMask idleMask;
+        ArmatureMask danceMask;
+
+        if (useSingleLayerInfMask) {
+            // Create single layer influence masks for idle and dance layers
+            idleMask = new SingleLayerInfluenceMask(idleLayer, animComposer, armature);
+            danceMask = new SingleLayerInfluenceMask(danceLayer, animComposer, armature);
+
         } else {
-            anim.makeLayer("idleLayer", ArmatureMask.createMask(skin.getArmature(), "Root"));
-            anim.makeLayer("danceLayer", ArmatureMask.createMask(skin.getArmature(), "Root"));
+            // Create default armature masks for idle and dance layers
+            idleMask = new ArmatureMask(armature);
+            danceMask = new ArmatureMask(armature);
         }
-        
-        setSLIMaskInfo();
-        useSLIMask = !useSLIMask;
-        
-        ClipAction clip = (ClipAction)anim.action("Dance");
-        clip.setMaxTransitionWeight(.9f);
-        ClipAction clip2 = (ClipAction)anim.action("IdleTop");
-        clip2.setMaxTransitionWeight(.8f);
-        
-        anim.setCurrentAction("Dance", "danceLayer");
-        anim.setCurrentAction("IdleTop", "idleLayer");
 
-        rootNode.attachChild(model);
-        
+        // Assign the masks to the respective animation layers
+        animComposer.makeLayer(idleLayer, idleMask);
+        animComposer.makeLayer(danceLayer, danceMask);
     }
-    private void setSLIMaskInfo() {
-        display.setText("Using SingleLayerInfluenceMasks: "+useSLIMask+"\nPress Spacebar to switch masks");
+
+    /**
+     * Plays the "Dance" and "IdleTop" animations on their respective layers.
+     */
+    private void playAnimations() {
+        animComposer.setCurrentAction("Dance", danceLayer);
+        animComposer.setCurrentAction("IdleTop", idleLayer);
     }
-    
+
+    /**
+     * Tests the serialization of animation masks.
+     */
+    private void testSerialization() {
+        AnimComposer aComposer = model.getControl(AnimComposer.class);
+        for (String layerName : aComposer.getLayerNames()) {
+
+            System.out.println("layerName: " + layerName);
+            AnimLayer layer = aComposer.getLayer(layerName);
+
+            if (layer.getMask() instanceof SingleLayerInfluenceMask) {
+                SingleLayerInfluenceMask mask = (SingleLayerInfluenceMask) layer.getMask();
+                // Serialize and deserialize the mask
+                mask = BinaryExporter.saveAndLoad(assetManager, mask);
+                // Reassign the AnimComposer to the mask and remake the layer
+                mask.setAnimComposer(aComposer);
+                aComposer.makeLayer(layerName, mask);
+            }
+        }
+    }
+
+    private void updateUI() {
+        display.setText("Using SingleLayerInfluenceMasks: " + useSingleLayerInfMask + "\nPress Spacebar to switch masks");
+    }
+
 }

+ 5 - 5
jme3-vr/src/main/java/com/jme3/input/vr/osvr/OSVR.java

@@ -33,6 +33,7 @@ import com.ochafik.lang.jnaerator.runtime.NativeSizeByReference;
 import com.sun.jna.Pointer;
 import com.sun.jna.ptr.PointerByReference;
 import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
 import java.util.logging.Logger;
 
 /**
@@ -78,6 +79,7 @@ public class OSVR implements VRAPI {
      */
     public static byte[] OpenGLString = { 'O', 'p', 'e', 'n', 'G', 'L', (byte)0 };
 
+    private final IntBuffer lastError = IntBuffer.allocate(2);
     private final Matrix4f[] eyeMatrix = new Matrix4f[2];
 
     private PointerByReference grabRM;
@@ -180,7 +182,7 @@ public class OSVR implements VRAPI {
      */
     public void grabGLFWContext() {
         // get current context
-        wglGLFW = org.lwjgl.opengl.WGL.wglGetCurrentContext();
+        wglGLFW = org.lwjgl.opengl.WGL.wglGetCurrentContext(lastError);
         glfwContext = org.lwjgl.glfw.GLFW.glfwGetCurrentContext();
     }
 
@@ -189,7 +191,7 @@ public class OSVR implements VRAPI {
      * @return <code>true</code> if the context is successfully shared and <code>false</code> otherwise.
      */
     public boolean shareContext() {
-        if( org.lwjgl.opengl.WGL.wglShareLists(wglRM, wglGLFW)) {
+        if (org.lwjgl.opengl.WGL.wglShareLists(lastError, wglRM, wglGLFW)) {
             System.out.println("Context sharing success!");
             return true;
         } else {
@@ -216,7 +218,7 @@ public class OSVR implements VRAPI {
             openResults.setAutoSynch(false);
             retval = OsvrRenderManagerOpenGLLibrary.osvrRenderManagerOpenDisplayOpenGL(renderManager, openResults);
             if( retval == 0 ) {
-                wglRM = org.lwjgl.opengl.WGL.wglGetCurrentContext();
+                wglRM = org.lwjgl.opengl.WGL.wglGetCurrentContext(lastError);
                 renderManagerContext = org.lwjgl.glfw.GLFW.glfwGetCurrentContext();
                 shareContext();
                 OsvrClientKitLibrary.osvrClientUpdate(context);
@@ -298,7 +300,6 @@ public class OSVR implements VRAPI {
         // may need to take current position and negate it from future values
     }
 
-    @Override
     public void getRenderSize(Vector2f store) {
         if( eyeLeftInfo == null || eyeLeftInfo.viewport.width == 0.0 ) {
             store.x = 1280f; store.y = 720f;
@@ -345,7 +346,6 @@ public class OSVR implements VRAPI {
         return storePos;
     }
 
-    @Override
     public void getPositionAndOrientation(Vector3f storePos, Quaternion storeRot) {
         storePos.x = (float)-hmdPose.translation.data[0];
         storePos.y = (float)hmdPose.translation.data[1];