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..15ac6cc6f 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,11 @@ package com.jme3.animation; import com.jme3.export.*; import com.jme3.math.*; +import com.jme3.scene.Geometry; +import com.jme3.scene.Mesh; import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.util.SafeArrayList; import com.jme3.util.TempVars; import com.jme3.util.clone.JmeCloneable; import com.jme3.util.clone.Cloner; @@ -80,7 +84,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 +194,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 +513,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 +699,32 @@ 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. - */ - Node getAttachmentsNode() { + * 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 boneIndex this bone's index in its skeleton (≥0) + * @param targets a list of geometries animated by this bone's skeleton (not + * null, unaffected) + */ + Node getAttachmentsNode(int boneIndex, SafeArrayList targets) { + targetGeometry = null; + /* + * Search for a geometry animated by this particular bone. + */ + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimatedByBone(boneIndex)) { + targetGeometry = geometry; + break; + } + } + if (attachNode == null) { attachNode = new Node(name + "_attachnode"); attachNode.setUserData("AttachedBone", this); } + return attachNode; } @@ -823,6 +878,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 +901,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..82a4c6e22 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; @@ -71,10 +70,12 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl * The skeleton of the model. */ private Skeleton skeleton; + /** - * List of targets which this controller effects. + * List of geometries affected by this control. */ - private SafeArrayList targets = new SafeArrayList(Mesh.class); + private SafeArrayList targets = new SafeArrayList(Geometry.class); + /** * Used to track when a mesh was updated. Meshes are only updated if they * are visible in at least one camera. @@ -124,8 +125,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl for (Material m : materials) { m.setInt("NumberOfBones", numBones); } - for (Mesh mesh : targets) { - if (mesh.isAnimated()) { + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { mesh.prepareForAnim(false); } } @@ -137,8 +139,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl m.clearParam("NumberOfBones"); } } - for (Mesh mesh : targets) { - if (mesh.isAnimated()) { + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { mesh.prepareForAnim(true); } } @@ -210,15 +213,22 @@ 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 != null && mesh.isAnimated()) { + targets.add(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); } @@ -236,10 +246,11 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl offsetMatrices = skeleton.computeSkinningMatrices(); - for (Mesh mesh : targets) { - // NOTE: This assumes that code higher up - // Already ensured those targets are animated - // otherwise a crash will happen in skin update + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + // NOTE: This assumes code higher up has + // already ensured this mesh is animated. + // Otherwise a crash will happen in skin update. softwareSkinUpdate(mesh, offsetMatrices); } } @@ -313,8 +324,9 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl //only do this for software updates void resetToBind() { - for (Mesh mesh : targets) { - if (mesh.isAnimated()) { + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + if (mesh != null && mesh.isAnimated()) { Buffer bwBuff = mesh.getBuffer(Type.BoneWeight).getData(); Buffer biBuff = mesh.getBuffer(Type.BoneIndex).getData(); if (!biBuff.hasArray() || !bwBuff.hasArray()) { @@ -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,20 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl + "in the skeleton."); } - Node n = b.getAttachmentsNode(); - Node model = (Node) spatial; - model.attachChild(n); + updateTargetsAndMaterials(spatial); + int boneIndex = skeleton.getBoneIndex(b); + Node n = b.getAttachmentsNode(boneIndex, targets); + /* + * 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; } @@ -459,12 +486,20 @@ public class SkeletonControl extends AbstractControl implements Cloneable, JmeCl } /** - * returns a copy of array of the targets meshes of this control + * Enumerate the target meshes of this control. * - * @return + * @return a new array */ - public Mesh[] getTargets() { - return targets.toArray(new Mesh[targets.size()]); + public Mesh[] getTargets() { + Mesh[] result = new Mesh[targets.size()]; + int i = 0; + for (Geometry geometry : targets) { + Mesh mesh = geometry.getMesh(); + result[i] = mesh; + i++; + } + + return result; } /** @@ -758,12 +793,19 @@ 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); + 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-core/src/main/java/com/jme3/scene/Mesh.java b/jme3-core/src/main/java/com/jme3/scene/Mesh.java index 84c279536..68b153633 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java +++ b/jme3-core/src/main/java/com/jme3/scene/Mesh.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 @@ -1408,6 +1408,45 @@ public class Mesh implements Savable, Cloneable, JmeCloneable { getBuffer(Type.HWBoneIndex) != null; } + /** + * Test whether the specified bone animates this mesh. + * + * @param boneIndex the bone's index in its skeleton + * @return true if the specified bone animates this mesh, otherwise false + */ + public boolean isAnimatedByBone(int boneIndex) { + VertexBuffer biBuf = getBuffer(VertexBuffer.Type.BoneIndex); + VertexBuffer wBuf = getBuffer(VertexBuffer.Type.BoneWeight); + if (biBuf == null || wBuf == null) { + return false; // no bone animation data + } + + ByteBuffer boneIndexBuffer = (ByteBuffer) biBuf.getData(); + boneIndexBuffer.rewind(); + int numBoneIndices = boneIndexBuffer.remaining(); + assert numBoneIndices % 4 == 0 : numBoneIndices; + int numVertices = boneIndexBuffer.remaining() / 4; + + FloatBuffer weightBuffer = (FloatBuffer) wBuf.getData(); + weightBuffer.rewind(); + int numWeights = weightBuffer.remaining(); + assert numWeights == numVertices * 4 : numWeights; + /* + * Test each vertex to determine whether the bone affects it. + */ + byte biByte = (byte) boneIndex; // bone indices wrap after 127 + for (int vIndex = 0; vIndex < numVertices; vIndex++) { + for (int wIndex = 0; wIndex < 4; wIndex++) { + byte bIndex = boneIndexBuffer.get(); + float weight = weightBuffer.get(); + if (wIndex < maxNumWeights && bIndex == biByte && weight != 0f) { + return true; + } + } + } + return false; + } + /** * Sets the count of vertices used for each tessellation patch * @param patchVertexCount 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); + } + } + } +}