diff --git a/jme3-core/src/main/java/com/jme3/anim/SingleLayerInfluenceMask.java b/jme3-core/src/main/java/com/jme3/anim/SingleLayerInfluenceMask.java index 935cb99abd..0050b9ba2c 100644 --- a/jme3-core/src/main/java/com/jme3/anim/SingleLayerInfluenceMask.java +++ b/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. - *

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. - *

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); } - -} +} \ No newline at end of file diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestSingleLayerInfluenceMask.java b/jme3-examples/src/main/java/jme3test/model/anim/TestSingleLayerInfluenceMask.java index 6ab0902f52..18e38ea10f 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestSingleLayerInfluenceMask.java +++ b/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. *

* The test fails if the visible dancing action does not - * loop seamlessly when using SingleLayerInfluenceMasks. Note that the action is not - * expected to loop seamlessly when not using SingleLayerArmatureMasks. + * loop seamlessly when using SingleLayerInfluenceMasks. Note that the action is + * not expected to loop seamlessly when not using + * SingleLayerArmatureMasks. *

- * 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 static void main(String[] args) { @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"); + } + }