diff --git a/jme3-core/src/main/java/com/jme3/animation/Bone.java b/jme3-core/src/main/java/com/jme3/animation/Bone.java index 7480eded8..2e2ff55a4 100644 --- a/jme3-core/src/main/java/com/jme3/animation/Bone.java +++ b/jme3-core/src/main/java/com/jme3/animation/Bone.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2012 jMonkeyEngine + * Copyright (c) 2009-2017 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -33,7 +33,9 @@ package com.jme3.animation; import com.jme3.export.*; import com.jme3.math.*; +import com.jme3.scene.Geometry; import com.jme3.scene.Node; +import com.jme3.scene.Spatial; import com.jme3.util.TempVars; import com.jme3.util.clone.JmeCloneable; import com.jme3.util.clone.Cloner; @@ -80,7 +82,10 @@ public final class Bone implements Savable, JmeCloneable { * The attachment node. */ private Node attachNode; - + /** + * A geometry animated by this node, used when updating the attachments node. + */ + private Geometry targetGeometry = null; /** * Bind transform is the local bind transform of this bone. (local space) */ @@ -187,7 +192,8 @@ public final class Bone implements Savable, JmeCloneable { this.children = cloner.clone(children); this.attachNode = cloner.clone(attachNode); - + this.targetGeometry = cloner.clone(targetGeometry); + this.bindPos = cloner.clone(bindPos); this.bindRot = cloner.clone(bindRot); this.bindScale = cloner.clone(bindScale); @@ -505,9 +511,39 @@ public final class Bone implements Savable, JmeCloneable { } if (attachNode != null) { + updateAttachNode(); + } + } + + /** + * Update the local transform of the attachments node. + */ + private void updateAttachNode() { + Node attachParent = attachNode.getParent(); + if (attachParent == null || targetGeometry == null + || targetGeometry.getParent() == attachParent + && targetGeometry.getLocalTransform().isIdentity()) { + /* + * The animated meshes are in the same coordinate system as the + * attachments node: no further transforms are needed. + */ attachNode.setLocalTranslation(modelPos); attachNode.setLocalRotation(modelRot); attachNode.setLocalScale(modelScale); + + } else { + Spatial loopSpatial = targetGeometry; + Transform combined = new Transform(modelPos, modelRot, modelScale); + /* + * Climb the scene graph applying local transforms until the + * attachments node's parent is reached. + */ + while (loopSpatial != attachParent && loopSpatial != null) { + Transform localTransform = loopSpatial.getLocalTransform(); + combined.combineWithParent(localTransform); + loopSpatial = loopSpatial.getParent(); + } + attachNode.setLocalTransform(combined); } } @@ -661,15 +697,21 @@ public final class Bone implements Savable, JmeCloneable { } /** - * Returns the attachment node. - * Attach models and effects to this node to make - * them follow this bone's motions. + * Access the attachments node of this bone. If this bone doesn't already + * have an attachments node, create one. Models and effects attached to the + * attachments node will follow this bone's motions. + * + * @param target a geometry animated by this bone, or null to indicate that + * all geometries affected by this bone have the same global transform as + * the attachment node's parent */ - Node getAttachmentsNode() { + Node getAttachmentsNode(Geometry target) { if (attachNode == null) { attachNode = new Node(name + "_attachnode"); attachNode.setUserData("AttachedBone", this); } + targetGeometry = target; + return attachNode; } @@ -823,6 +865,7 @@ public final class Bone implements Savable, JmeCloneable { } attachNode = (Node) input.readSavable("attachNode", null); + targetGeometry = (Geometry) input.readSavable("targetGeometry", null); localPos.set(bindPos); localRot.set(bindRot); @@ -845,6 +888,7 @@ public final class Bone implements Savable, JmeCloneable { output.write(name, "name", null); output.write(attachNode, "attachNode", null); + output.write(targetGeometry, "targetGeometry", null); output.write(bindPos, "bindPos", null); output.write(bindRot, "bindRot", null); output.write(bindScale, "bindScale", new Vector3f(1.0f, 1.0f, 1.0f)); diff --git a/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java b/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java index 1d14fa61e..94948808f 100644 --- a/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java +++ b/jme3-core/src/main/java/com/jme3/animation/SkeletonControl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2012 jMonkeyEngine + * Copyright (c) 2009-2017 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -52,7 +52,6 @@ import java.io.IOException; import java.nio.Buffer; import java.nio.ByteBuffer; import java.nio.FloatBuffer; -import java.util.Arrays; import java.util.HashSet; import java.util.Set; import java.util.logging.Level; @@ -75,6 +74,11 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl * List of targets which this controller effects. */ private SafeArrayList targets = new SafeArrayList(Mesh.class); + /** + * Geometry with an animated mesh, for calculating attachments node + * transforms. A null means no geometry is subject to this control. + */ + private Geometry targetGeometry = null; /** * Used to track when a mesh was updated. Meshes are only updated if they * are visible in at least one camera. @@ -210,15 +214,23 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl this.skeleton = skeleton; } + /** + * If specified the geometry has an animated mesh, add its mesh and material + * to the lists of animation targets. + */ + private void findTargets(Geometry geometry) { + Mesh mesh = geometry.getMesh(); + if (mesh.isAnimated()) { + targets.add(mesh); + targetGeometry = geometry; + materials.add(geometry.getMaterial()); + } + } + private void findTargets(Node node) { for (Spatial child : node.getChildren()) { if (child instanceof Geometry) { - Geometry geom = (Geometry) child; - Mesh mesh = geom.getMesh(); - if (mesh.isAnimated()) { - targets.add(mesh); - materials.add(geom.getMaterial()); - } + findTargets((Geometry) child); } else if (child instanceof Node) { findTargets((Node) child); } @@ -432,9 +444,13 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl } /** + * Access the attachments node of the named bone. If the bone doesn't + * already have an attachments node, create one and attach it to the scene + * graph. Models and effects attached to the attachments node will follow + * the bone's motions. * * @param boneName the name of the bone - * @return the node attached to this bone + * @return the attachments node of the bone */ public Node getAttachmentsNode(String boneName) { Bone b = skeleton.getBone(boneName); @@ -443,9 +459,19 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl + "in the skeleton."); } - Node n = b.getAttachmentsNode(); - Node model = (Node) spatial; - model.attachChild(n); + updateTargetsAndMaterials(spatial); + Node n = b.getAttachmentsNode(targetGeometry); + /* + * Select a node to parent the attachments node. + */ + Node parent; + if (spatial instanceof Node) { + parent = (Node) spatial; // the usual case + } else { + parent = spatial.getParent(); + } + parent.attachChild(n); + return n; } @@ -758,12 +784,20 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl skeleton = (Skeleton) in.readSavable("skeleton", null); } + /** + * Update the lists of animation targets. + * + * @param spatial the controlled spatial + */ private void updateTargetsAndMaterials(Spatial spatial) { targets.clear(); - materials.clear(); - if (spatial != null && spatial instanceof Node) { - Node node = (Node) spatial; - findTargets(node); + targetGeometry = null; + materials.clear(); + + if (spatial instanceof Node) { + findTargets((Node) spatial); + } else if (spatial instanceof Geometry) { + findTargets((Geometry) spatial); } } } diff --git a/jme3-core/src/main/java/com/jme3/math/Transform.java b/jme3-core/src/main/java/com/jme3/math/Transform.java index 9d8a72a1e..2d4e1fb7d 100644 --- a/jme3-core/src/main/java/com/jme3/math/Transform.java +++ b/jme3-core/src/main/java/com/jme3/math/Transform.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2009-2012 jMonkeyEngine + * Copyright (c) 2009-2017 jMonkeyEngine * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -288,6 +288,17 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable rot.set(0, 0, 0, 1); } + /** + * Test for exact identity. + * + * @return true if exactly equal to {@link #IDENTITY}, otherwise false + */ + public boolean isIdentity() { + return translation.x == 0f && translation.y == 0f && translation.z == 0f + && scale.x == 1f && scale.y == 1f && scale.z == 1f + && rot.w == 1f && rot.x == 0f && rot.y == 0f && rot.z == 0f; + } + @Override public int hashCode() { int hash = 7; diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAttachmentsNode.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAttachmentsNode.java new file mode 100644 index 000000000..ab592d05e --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAttachmentsNode.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2009-2017 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 jme3test.model.anim; + +import com.jme3.animation.AnimChannel; +import com.jme3.animation.AnimControl; +import com.jme3.animation.AnimEventListener; +import com.jme3.animation.LoopMode; +import com.jme3.animation.SkeletonControl; +import com.jme3.app.SimpleApplication; +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.material.Material; +import com.jme3.math.ColorRGBA; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.Geometry; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.shape.Box; + +/** + * Simple application to an test attachments node on the Jaime model. + * + * Derived from {@link jme3test.model.anim.TestOgreAnim}. + */ +public class TestAttachmentsNode extends SimpleApplication + implements AnimEventListener, ActionListener { + + public static void main(String[] args) { + TestAttachmentsNode app = new TestAttachmentsNode(); + app.start(); + } + + private AnimChannel channel; + private AnimControl control; + + @Override + public void simpleInitApp() { + flyCam.setMoveSpeed(10f); + cam.setLocation(new Vector3f(6.4f, 7.5f, 12.8f)); + cam.setRotation(new Quaternion(-0.060740203f, 0.93925786f, -0.2398315f, -0.2378785f)); + + DirectionalLight dl = new DirectionalLight(); + dl.setDirection(new Vector3f(-0.1f, -0.7f, -1).normalizeLocal()); + dl.setColor(ColorRGBA.White); + rootNode.addLight(dl); + + Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + control = model.getControl(AnimControl.class); + SkeletonControl skeletonControl = model.getControl(SkeletonControl.class); + + model.center(); + model.setLocalScale(5f); + + control.addListener(this); + channel = control.createChannel(); + channel.setAnim("Idle"); + + Box box = new Box(0.3f, 0.02f, 0.02f); + Geometry saber = new Geometry("saber", box); + saber.move(0.4f, 0.05f, 0.01f); + Material red = assetManager.loadMaterial("Common/Materials/RedColor.j3m"); + saber.setMaterial(red); + Node n = skeletonControl.getAttachmentsNode("hand.R"); + n.attachChild(saber); + rootNode.attachChild(model); + + inputManager.addListener(this, "Attack"); + inputManager.addMapping("Attack", new KeyTrigger(KeyInput.KEY_SPACE)); + } + + @Override + public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) { + if (animName.equals("Punches")) { + channel.setAnim("Idle", 0.5f); + channel.setLoopMode(LoopMode.DontLoop); + channel.setSpeed(1f); + } + } + + @Override + public void onAnimChange(AnimControl control, AnimChannel channel, String animName) { + } + + @Override + public void onAction(String binding, boolean value, float tpf) { + if (binding.equals("Attack") && value) { + if (!channel.getAnimationName().equals("Punches")) { + channel.setAnim("Punches", 0.5f); + channel.setLoopMode(LoopMode.Cycle); + channel.setSpeed(0.5f); + } + } + } +}