Browse Source

Merge pull request #2372 from capdevon/capdevon-SingleLayerInfluenceMask

SingleLayerInfluenceMask:  fix serialization issues
Ryan McDonough 6 months ago
parent
commit
d4f57c08af

+ 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);
     }
-    
-}
 
+}

+ 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");
+    }
+
 }