Explorar o código

New Armature system

Nehon %!s(int64=7) %!d(string=hai) anos
pai
achega
3eb890da38

+ 251 - 0
jme3-core/src/main/java/com/jme3/animation/Armature.java

@@ -0,0 +1,251 @@
+package com.jme3.animation;
+
+import com.jme3.export.*;
+import com.jme3.math.Matrix4f;
+import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Created by Nehon on 15/12/2017.
+ */
+public class Armature implements JmeCloneable, Savable {
+
+    private Joint[] rootJoints;
+    private Joint[] jointList;
+
+    /**
+     * Contains the skinning matrices, multiplying it by a vertex effected by a bone
+     * will cause it to go to the animated position.
+     */
+    private transient Matrix4f[] skinningMatrixes;
+
+
+    /**
+     * Serialization only
+     */
+    public Armature() {
+    }
+
+    /**
+     * Creates an armature from a joint list.
+     * The root joints are found automatically.
+     * <p>
+     * Note that using this constructor will cause the joints in the list
+     * to have their bind pose recomputed based on their local transforms.
+     *
+     * @param jointList The list of joints to manage by this Armature
+     */
+    public Armature(Joint[] jointList) {
+        this.jointList = jointList;
+
+        List<Joint> rootJointList = new ArrayList<>();
+        for (int i = jointList.length - 1; i >= 0; i--) {
+            Joint b = jointList[i];
+            if (b.getParent() == null) {
+                rootJointList.add(b);
+            }
+        }
+        rootJoints = rootJointList.toArray(new Joint[rootJointList.size()]);
+
+        createSkinningMatrices();
+
+        for (int i = rootJoints.length - 1; i >= 0; i--) {
+            Joint rootJoint = rootJoints[i];
+            rootJoint.update();
+        }
+    }
+
+//
+//    /**
+//     * Special-purpose copy constructor.
+//     * <p>
+//     * Shallow copies bind pose data from the source skeleton, does not
+//     * copy any other data.
+//     *
+//     * @param source The source Skeleton to copy from
+//     */
+//    public Armature(Armature source) {
+//        Joint[] sourceList = source.jointList;
+//        jointList = new Joint[sourceList.length];
+//        for (int i = 0; i < sourceList.length; i++) {
+//            jointList[i] = new Joint(sourceList[i]);
+//        }
+//
+//        rootJoints = new Bone[source.rootJoints.length];
+//        for (int i = 0; i < rootJoints.length; i++) {
+//            rootJoints[i] = recreateBoneStructure(source.rootJoints[i]);
+//        }
+//        createSkinningMatrices();
+//
+//        for (int i = rootJoints.length - 1; i >= 0; i--) {
+//            rootJoints[i].update();
+//        }
+//    }
+
+    /**
+     * Update all joints sin this Amature.
+     */
+    public void update() {
+        for (Joint rootJoint : rootJoints) {
+            rootJoint.update();
+        }
+    }
+
+    private void createSkinningMatrices() {
+        skinningMatrixes = new Matrix4f[jointList.length];
+        for (int i = 0; i < skinningMatrixes.length; i++) {
+            skinningMatrixes[i] = new Matrix4f();
+        }
+    }
+
+    /**
+     * returns the array of all root joints of this Armatire
+     *
+     * @return
+     */
+    public Joint[] getRoots() {
+        return rootJoints;
+    }
+
+    /**
+     * return a joint for the given index
+     *
+     * @param index
+     * @return
+     */
+    public Joint getJoint(int index) {
+        return jointList[index];
+    }
+
+    /**
+     * returns the joint with the given name
+     *
+     * @param name
+     * @return
+     */
+    public Joint getJoint(String name) {
+        for (int i = 0; i < jointList.length; i++) {
+            if (jointList[i].getName().equals(name)) {
+                return jointList[i];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * returns the bone index of the given bone
+     *
+     * @param joint
+     * @return
+     */
+    public int getJointIndex(Joint joint) {
+        for (int i = 0; i < jointList.length; i++) {
+            if (jointList[i] == joint) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * returns the joint index of the joint that has the given name
+     *
+     * @param name
+     * @return
+     */
+    public int getJointIndex(String name) {
+        for (int i = 0; i < jointList.length; i++) {
+            if (jointList[i].getName().equals(name)) {
+                return i;
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Saves the current Armature state as it's bind pose.
+     */
+    public void setBindPose() {
+        //make sure all bones are updated
+        update();
+        //Save the current pose as bind pose
+        for (Joint rootJoint : rootJoints) {
+            rootJoint.setBindPose();
+        }
+    }
+
+    /**
+     * Compute the skining matrices for each bone of the armature that would be used to transform vertices of associated meshes
+     *
+     * @return
+     */
+    public Matrix4f[] computeSkinningMatrices() {
+        TempVars vars = TempVars.get();
+        for (int i = 0; i < jointList.length; i++) {
+            jointList[i].getOffsetTransform(skinningMatrixes[i], vars.quat1, vars.vect1, vars.vect2, vars.tempMat3);
+        }
+        vars.release();
+        return skinningMatrixes;
+    }
+
+    /**
+     * returns the number of joints of this armature
+     *
+     * @return
+     */
+    public int getJointCount() {
+        return jointList.length;
+    }
+
+    @Override
+    public Object jmeClone() {
+        try {
+            Armature clone = (Armature) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        this.rootJoints = cloner.clone(rootJoints);
+        this.jointList = cloner.clone(jointList);
+        this.skinningMatrixes = cloner.clone(skinningMatrixes);
+    }
+
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule input = im.getCapsule(this);
+
+        Savable[] jointRootsAsSavable = input.readSavableArray("rootJoints", null);
+        rootJoints = new Joint[jointRootsAsSavable.length];
+        System.arraycopy(jointRootsAsSavable, 0, rootJoints, 0, jointRootsAsSavable.length);
+
+        Savable[] jointListAsSavable = input.readSavableArray("jointList", null);
+        jointList = new Joint[jointListAsSavable.length];
+        System.arraycopy(jointListAsSavable, 0, jointList, 0, jointListAsSavable.length);
+
+        createSkinningMatrices();
+
+        for (Joint rootJoint : rootJoints) {
+            rootJoint.update();
+        }
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule output = ex.getCapsule(this);
+        output.write(rootJoints, "rootJoints", null);
+        output.write(jointList, "jointList", null);
+    }
+
+}

+ 742 - 0
jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java

@@ -0,0 +1,742 @@
+/*
+ * 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 com.jme3.animation;
+
+import com.jme3.export.*;
+import com.jme3.material.MatParamOverride;
+import com.jme3.math.FastMath;
+import com.jme3.math.Matrix4f;
+import com.jme3.renderer.*;
+import com.jme3.scene.*;
+import com.jme3.scene.VertexBuffer.Type;
+import com.jme3.scene.control.AbstractControl;
+import com.jme3.scene.mesh.IndexBuffer;
+import com.jme3.shader.VarType;
+import com.jme3.util.SafeArrayList;
+import com.jme3.util.TempVars;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+import java.nio.Buffer;
+import java.nio.FloatBuffer;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * The Skeleton control deforms a model according to a armature, It handles the
+ * computation of the deformation matrices and performs the transformations on
+ * the mesh
+ *
+ * @author Rémy Bouquet Based on AnimControl by Kirill Vainer
+ */
+public class ArmatureControl extends AbstractControl implements Cloneable, JmeCloneable {
+
+    private static final Logger logger = Logger.getLogger(ArmatureControl.class.getName());
+
+    /**
+     * The armature of the model.
+     */
+    private Armature armature;
+
+    /**
+     * List of geometries affected by this control.
+     */
+    private SafeArrayList<Geometry> targets = new SafeArrayList<Geometry>(Geometry.class);
+
+    /**
+     * Used to track when a mesh was updated. Meshes are only updated if they
+     * are visible in at least one camera.
+     */
+    private boolean wasMeshUpdated = false;
+
+    /**
+     * User wishes to use hardware skinning if available.
+     */
+    private transient boolean hwSkinningDesired = true;
+
+    /**
+     * Hardware skinning is currently being used.
+     */
+    private transient boolean hwSkinningEnabled = false;
+
+    /**
+     * Hardware skinning was tested on this GPU, results
+     * are stored in {@link #hwSkinningSupported} variable.
+     */
+    private transient boolean hwSkinningTested = false;
+
+    /**
+     * If hardware skinning was {@link #hwSkinningTested tested}, then
+     * this variable will be set to true if supported, and false if otherwise.
+     */
+    private transient boolean hwSkinningSupported = false;
+
+    /**
+     * Bone offset matrices, recreated each frame
+     */
+    private transient Matrix4f[] offsetMatrices;
+
+
+    private MatParamOverride numberOfJointsParam;
+    private MatParamOverride jointMatricesParam;
+
+    /**
+     * Serialization only. Do not use.
+     */
+    public ArmatureControl() {
+    }
+
+    /**
+     * Creates a armature control. The list of targets will be acquired
+     * automatically when the control is attached to a node.
+     *
+     * @param skeleton the armature
+     */
+    public ArmatureControl(Armature armature) {
+        if (armature == null) {
+            throw new IllegalArgumentException("armature cannot be null");
+        }
+        this.armature = armature;
+        this.numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+        this.jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
+    }
+
+
+    private void switchToHardware() {
+        numberOfJointsParam.setEnabled(true);
+        jointMatricesParam.setEnabled(true);
+
+        // Next full 10 bones (e.g. 30 on 24 bones)
+        int numBones = ((armature.getJointCount() / 10) + 1) * 10;
+        numberOfJointsParam.setValue(numBones);
+
+        for (Geometry geometry : targets) {
+            Mesh mesh = geometry.getMesh();
+            if (mesh != null && mesh.isAnimated()) {
+                mesh.prepareForAnim(false);
+            }
+        }
+    }
+
+    private void switchToSoftware() {
+        numberOfJointsParam.setEnabled(false);
+        jointMatricesParam.setEnabled(false);
+
+        for (Geometry geometry : targets) {
+            Mesh mesh = geometry.getMesh();
+            if (mesh != null && mesh.isAnimated()) {
+                mesh.prepareForAnim(true);
+            }
+        }
+    }
+
+    private boolean testHardwareSupported(RenderManager rm) {
+
+        //Only 255 bones max supported with hardware skinning
+        if (armature.getJointCount() > 255) {
+            return false;
+        }
+
+        switchToHardware();
+
+        try {
+            rm.preloadScene(spatial);
+            return true;
+        } catch (RendererException e) {
+            logger.log(Level.WARNING, "Could not enable HW skinning due to shader compile error:", e);
+            return false;
+        }
+    }
+
+    /**
+     * Specifies if hardware skinning is preferred. If it is preferred and
+     * supported by GPU, it shall be enabled, if its not preferred, or not
+     * supported by GPU, then it shall be disabled.
+     *
+     * @param preferred
+     * @see #isHardwareSkinningUsed()
+     */
+    public void setHardwareSkinningPreferred(boolean preferred) {
+        hwSkinningDesired = preferred;
+    }
+
+    /**
+     * @return True if hardware skinning is preferable to software skinning.
+     * Set to false by default.
+     * @see #setHardwareSkinningPreferred(boolean)
+     */
+    public boolean isHardwareSkinningPreferred() {
+        return hwSkinningDesired;
+    }
+
+    /**
+     * @return True is hardware skinning is activated and is currently used, false otherwise.
+     */
+    public boolean isHardwareSkinningUsed() {
+        return hwSkinningEnabled;
+    }
+
+
+    /**
+     * 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);
+        }
+
+    }
+
+    private void findTargets(Node node) {
+        for (Spatial child : node.getChildren()) {
+            if (child instanceof Geometry) {
+                findTargets((Geometry) child);
+            } else if (child instanceof Node) {
+                findTargets((Node) child);
+            }
+        }
+    }
+
+    @Override
+    public void setSpatial(Spatial spatial) {
+        Spatial oldSpatial = this.spatial;
+        super.setSpatial(spatial);
+        updateTargetsAndMaterials(spatial);
+
+        if (oldSpatial != null) {
+            oldSpatial.removeMatParamOverride(numberOfJointsParam);
+            oldSpatial.removeMatParamOverride(jointMatricesParam);
+        }
+
+        if (spatial != null) {
+            spatial.removeMatParamOverride(numberOfJointsParam);
+            spatial.removeMatParamOverride(jointMatricesParam);
+            spatial.addMatParamOverride(numberOfJointsParam);
+            spatial.addMatParamOverride(jointMatricesParam);
+        }
+    }
+
+    private void controlRenderSoftware() {
+        resetToBind(); // reset morph meshes to bind pose
+
+        offsetMatrices = armature.computeSkinningMatrices();
+
+        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);
+        }
+    }
+
+    private void controlRenderHardware() {
+        offsetMatrices = armature.computeSkinningMatrices();
+        jointMatricesParam.setValue(offsetMatrices);
+    }
+
+    @Override
+    protected void controlRender(RenderManager rm, ViewPort vp) {
+        if (!wasMeshUpdated) {
+            updateTargetsAndMaterials(spatial);
+
+            // Prevent illegal cases. These should never happen.
+            assert hwSkinningTested || (!hwSkinningTested && !hwSkinningSupported && !hwSkinningEnabled);
+            assert !hwSkinningEnabled || (hwSkinningEnabled && hwSkinningTested && hwSkinningSupported);
+
+            if (hwSkinningDesired && !hwSkinningTested) {
+                hwSkinningTested = true;
+                hwSkinningSupported = testHardwareSupported(rm);
+
+                if (hwSkinningSupported) {
+                    hwSkinningEnabled = true;
+
+                    Logger.getLogger(ArmatureControl.class.getName()).log(Level.INFO, "Hardware skinning engaged for {0}", spatial);
+                } else {
+                    switchToSoftware();
+                }
+            } else if (hwSkinningDesired && hwSkinningSupported && !hwSkinningEnabled) {
+                switchToHardware();
+                hwSkinningEnabled = true;
+            } else if (!hwSkinningDesired && hwSkinningEnabled) {
+                switchToSoftware();
+                hwSkinningEnabled = false;
+            }
+
+            if (hwSkinningEnabled) {
+                controlRenderHardware();
+            } else {
+                controlRenderSoftware();
+            }
+
+            wasMeshUpdated = true;
+        }
+    }
+
+    @Override
+    protected void controlUpdate(float tpf) {
+        wasMeshUpdated = false;
+    }
+
+    //only do this for software updates
+    void resetToBind() {
+        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()) {
+                    mesh.prepareForAnim(true); // prepare for software animation
+                }
+                VertexBuffer bindPos = mesh.getBuffer(Type.BindPosePosition);
+                VertexBuffer bindNorm = mesh.getBuffer(Type.BindPoseNormal);
+                VertexBuffer pos = mesh.getBuffer(Type.Position);
+                VertexBuffer norm = mesh.getBuffer(Type.Normal);
+                FloatBuffer pb = (FloatBuffer) pos.getData();
+                FloatBuffer nb = (FloatBuffer) norm.getData();
+                FloatBuffer bpb = (FloatBuffer) bindPos.getData();
+                FloatBuffer bnb = (FloatBuffer) bindNorm.getData();
+                pb.clear();
+                nb.clear();
+                bpb.clear();
+                bnb.clear();
+
+                //reseting bind tangents if there is a bind tangent buffer
+                VertexBuffer bindTangents = mesh.getBuffer(Type.BindPoseTangent);
+                if (bindTangents != null) {
+                    VertexBuffer tangents = mesh.getBuffer(Type.Tangent);
+                    FloatBuffer tb = (FloatBuffer) tangents.getData();
+                    FloatBuffer btb = (FloatBuffer) bindTangents.getData();
+                    tb.clear();
+                    btb.clear();
+                    tb.put(btb).clear();
+                }
+
+
+                pb.put(bpb).clear();
+                nb.put(bnb).clear();
+            }
+        }
+    }
+
+    @Override
+    public Object jmeClone() {
+        return super.jmeClone();
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        super.cloneFields(cloner, original);
+
+        this.armature = cloner.clone(armature);
+
+        // If the targets were cloned then this will clone them.  If the targets
+        // were shared then this will share them.
+        this.targets = cloner.clone(targets);
+
+        this.numberOfJointsParam = cloner.clone(numberOfJointsParam);
+        this.jointMatricesParam = cloner.clone(jointMatricesParam);
+    }
+
+    /**
+     * 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 jointName the name of the joint
+     * @return the attachments node of the joint
+     */
+    public Node getAttachmentsNode(String jointName) {
+        Joint b = armature.getJoint(jointName);
+        if (b == null) {
+            throw new IllegalArgumentException("Given bone name does not exist "
+                    + "in the armature.");
+        }
+
+        updateTargetsAndMaterials(spatial);
+        int boneIndex = armature.getJointIndex(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;
+    }
+
+    /**
+     * returns the armature of this control
+     *
+     * @return
+     */
+    public Armature getArmature() {
+        return armature;
+    }
+
+    /**
+     * Enumerate the target meshes of this control.
+     *
+     * @return a new array
+     */
+    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;
+    }
+
+    /**
+     * Update the mesh according to the given transformation matrices
+     *
+     * @param mesh           then mesh
+     * @param offsetMatrices the transformation matrices to apply
+     */
+    private void softwareSkinUpdate(Mesh mesh, Matrix4f[] offsetMatrices) {
+
+        VertexBuffer tb = mesh.getBuffer(Type.Tangent);
+        if (tb == null) {
+            //if there are no tangents use the classic skinning
+            applySkinning(mesh, offsetMatrices);
+        } else {
+            //if there are tangents use the skinning with tangents
+            applySkinningTangents(mesh, offsetMatrices, tb);
+        }
+
+
+    }
+
+    /**
+     * Method to apply skinning transforms to a mesh's buffers
+     *
+     * @param mesh           the mesh
+     * @param offsetMatrices the offset matices to apply
+     */
+    private void applySkinning(Mesh mesh, Matrix4f[] offsetMatrices) {
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+        if (maxWeightsPerVert <= 0) {
+            throw new IllegalStateException("Max weights per vert is incorrectly set!");
+        }
+        int fourMinusMaxWeights = 4 - maxWeightsPerVert;
+
+        // NOTE: This code assumes the vertex buffer is in bind pose
+        // resetToBind() has been called this frame
+        VertexBuffer vb = mesh.getBuffer(Type.Position);
+        FloatBuffer fvb = (FloatBuffer) vb.getData();
+        fvb.rewind();
+
+        VertexBuffer nb = mesh.getBuffer(Type.Normal);
+        FloatBuffer fnb = (FloatBuffer) nb.getData();
+        fnb.rewind();
+
+        // get boneIndexes and weights for mesh
+        IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData());
+        FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
+
+        wb.rewind();
+
+        float[] weights = wb.array();
+        int idxWeights = 0;
+
+        TempVars vars = TempVars.get();
+
+        float[] posBuf = vars.skinPositions;
+        float[] normBuf = vars.skinNormals;
+
+        int iterations = (int) FastMath.ceil(fvb.limit() / ((float) posBuf.length));
+        int bufLength = posBuf.length;
+        for (int i = iterations - 1; i >= 0; i--) {
+            // read next set of positions and normals from native buffer
+            bufLength = Math.min(posBuf.length, fvb.remaining());
+            fvb.get(posBuf, 0, bufLength);
+            fnb.get(normBuf, 0, bufLength);
+            int verts = bufLength / 3;
+            int idxPositions = 0;
+
+            // iterate vertices and apply skinning transform for each effecting bone
+            for (int vert = verts - 1; vert >= 0; vert--) {
+                // Skip this vertex if the first weight is zero.
+                if (weights[idxWeights] == 0) {
+                    idxPositions += 3;
+                    idxWeights += 4;
+                    continue;
+                }
+
+                float nmx = normBuf[idxPositions];
+                float vtx = posBuf[idxPositions++];
+                float nmy = normBuf[idxPositions];
+                float vty = posBuf[idxPositions++];
+                float nmz = normBuf[idxPositions];
+                float vtz = posBuf[idxPositions++];
+
+                float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0;
+
+                for (int w = maxWeightsPerVert - 1; w >= 0; w--) {
+                    float weight = weights[idxWeights];
+                    Matrix4f mat = offsetMatrices[ib.get(idxWeights++)];
+
+                    rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight;
+                    ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight;
+                    rz += (mat.m20 * vtx + mat.m21 * vty + mat.m22 * vtz + mat.m23) * weight;
+
+                    rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight;
+                    rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight;
+                    rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight;
+                }
+
+                idxWeights += fourMinusMaxWeights;
+
+                idxPositions -= 3;
+                normBuf[idxPositions] = rnx;
+                posBuf[idxPositions++] = rx;
+                normBuf[idxPositions] = rny;
+                posBuf[idxPositions++] = ry;
+                normBuf[idxPositions] = rnz;
+                posBuf[idxPositions++] = rz;
+            }
+
+            fvb.position(fvb.position() - bufLength);
+            fvb.put(posBuf, 0, bufLength);
+            fnb.position(fnb.position() - bufLength);
+            fnb.put(normBuf, 0, bufLength);
+        }
+
+        vars.release();
+
+        vb.updateData(fvb);
+        nb.updateData(fnb);
+
+    }
+
+    /**
+     * Specific method for skinning with tangents to avoid cluttering the
+     * classic skinning calculation with null checks that would slow down the
+     * process even if tangents don't have to be computed. Also the iteration
+     * has additional indexes since tangent has 4 components instead of 3 for
+     * pos and norm
+     *
+     * @param maxWeightsPerVert maximum number of weights per vertex
+     * @param mesh              the mesh
+     * @param offsetMatrices    the offsetMaytrices to apply
+     * @param tb                the tangent vertexBuffer
+     */
+    private void applySkinningTangents(Mesh mesh, Matrix4f[] offsetMatrices, VertexBuffer tb) {
+        int maxWeightsPerVert = mesh.getMaxNumWeights();
+
+        if (maxWeightsPerVert <= 0) {
+            throw new IllegalStateException("Max weights per vert is incorrectly set!");
+        }
+
+        int fourMinusMaxWeights = 4 - maxWeightsPerVert;
+
+        // NOTE: This code assumes the vertex buffer is in bind pose
+        // resetToBind() has been called this frame
+        VertexBuffer vb = mesh.getBuffer(Type.Position);
+        FloatBuffer fvb = (FloatBuffer) vb.getData();
+        fvb.rewind();
+
+        VertexBuffer nb = mesh.getBuffer(Type.Normal);
+
+        FloatBuffer fnb = (FloatBuffer) nb.getData();
+        fnb.rewind();
+
+
+        FloatBuffer ftb = (FloatBuffer) tb.getData();
+        ftb.rewind();
+
+
+        // get boneIndexes and weights for mesh
+        IndexBuffer ib = IndexBuffer.wrapIndexBuffer(mesh.getBuffer(Type.BoneIndex).getData());
+        FloatBuffer wb = (FloatBuffer) mesh.getBuffer(Type.BoneWeight).getData();
+
+        wb.rewind();
+
+        float[] weights = wb.array();
+        int idxWeights = 0;
+
+        TempVars vars = TempVars.get();
+
+
+        float[] posBuf = vars.skinPositions;
+        float[] normBuf = vars.skinNormals;
+        float[] tanBuf = vars.skinTangents;
+
+        int iterations = (int) FastMath.ceil(fvb.limit() / ((float) posBuf.length));
+        int bufLength = 0;
+        int tanLength = 0;
+        for (int i = iterations - 1; i >= 0; i--) {
+            // read next set of positions and normals from native buffer
+            bufLength = Math.min(posBuf.length, fvb.remaining());
+            tanLength = Math.min(tanBuf.length, ftb.remaining());
+            fvb.get(posBuf, 0, bufLength);
+            fnb.get(normBuf, 0, bufLength);
+            ftb.get(tanBuf, 0, tanLength);
+            int verts = bufLength / 3;
+            int idxPositions = 0;
+            //tangents has their own index because of the 4 components
+            int idxTangents = 0;
+
+            // iterate vertices and apply skinning transform for each effecting bone
+            for (int vert = verts - 1; vert >= 0; vert--) {
+                // Skip this vertex if the first weight is zero.
+                if (weights[idxWeights] == 0) {
+                    idxTangents += 4;
+                    idxPositions += 3;
+                    idxWeights += 4;
+                    continue;
+                }
+
+                float nmx = normBuf[idxPositions];
+                float vtx = posBuf[idxPositions++];
+                float nmy = normBuf[idxPositions];
+                float vty = posBuf[idxPositions++];
+                float nmz = normBuf[idxPositions];
+                float vtz = posBuf[idxPositions++];
+
+                float tnx = tanBuf[idxTangents++];
+                float tny = tanBuf[idxTangents++];
+                float tnz = tanBuf[idxTangents++];
+
+                // skipping the 4th component of the tangent since it doesn't have to be transformed
+                idxTangents++;
+
+                float rx = 0, ry = 0, rz = 0, rnx = 0, rny = 0, rnz = 0, rtx = 0, rty = 0, rtz = 0;
+
+                for (int w = maxWeightsPerVert - 1; w >= 0; w--) {
+                    float weight = weights[idxWeights];
+                    Matrix4f mat = offsetMatrices[ib.get(idxWeights++)];
+
+                    rx += (mat.m00 * vtx + mat.m01 * vty + mat.m02 * vtz + mat.m03) * weight;
+                    ry += (mat.m10 * vtx + mat.m11 * vty + mat.m12 * vtz + mat.m13) * weight;
+                    rz += (mat.m20 * vtx + mat.m21 * vty + mat.m22 * vtz + mat.m23) * weight;
+
+                    rnx += (nmx * mat.m00 + nmy * mat.m01 + nmz * mat.m02) * weight;
+                    rny += (nmx * mat.m10 + nmy * mat.m11 + nmz * mat.m12) * weight;
+                    rnz += (nmx * mat.m20 + nmy * mat.m21 + nmz * mat.m22) * weight;
+
+                    rtx += (tnx * mat.m00 + tny * mat.m01 + tnz * mat.m02) * weight;
+                    rty += (tnx * mat.m10 + tny * mat.m11 + tnz * mat.m12) * weight;
+                    rtz += (tnx * mat.m20 + tny * mat.m21 + tnz * mat.m22) * weight;
+                }
+
+                idxWeights += fourMinusMaxWeights;
+
+                idxPositions -= 3;
+
+                normBuf[idxPositions] = rnx;
+                posBuf[idxPositions++] = rx;
+                normBuf[idxPositions] = rny;
+                posBuf[idxPositions++] = ry;
+                normBuf[idxPositions] = rnz;
+                posBuf[idxPositions++] = rz;
+
+                idxTangents -= 4;
+
+                tanBuf[idxTangents++] = rtx;
+                tanBuf[idxTangents++] = rty;
+                tanBuf[idxTangents++] = rtz;
+
+                //once again skipping the 4th component of the tangent
+                idxTangents++;
+            }
+
+            fvb.position(fvb.position() - bufLength);
+            fvb.put(posBuf, 0, bufLength);
+            fnb.position(fnb.position() - bufLength);
+            fnb.put(normBuf, 0, bufLength);
+            ftb.position(ftb.position() - tanLength);
+            ftb.put(tanBuf, 0, tanLength);
+        }
+
+        vars.release();
+
+        vb.updateData(fvb);
+        nb.updateData(fnb);
+        tb.updateData(ftb);
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        super.write(ex);
+        OutputCapsule oc = ex.getCapsule(this);
+        oc.write(armature, "armature", null);
+
+        oc.write(numberOfJointsParam, "numberOfBonesParam", null);
+        oc.write(jointMatricesParam, "boneMatricesParam", null);
+    }
+
+    @Override
+    public void read(JmeImporter im) throws IOException {
+        super.read(im);
+        InputCapsule in = im.getCapsule(this);
+        armature = (Armature) in.readSavable("armature", null);
+
+        numberOfJointsParam = (MatParamOverride) in.readSavable("numberOfBonesParam", null);
+        jointMatricesParam = (MatParamOverride) in.readSavable("boneMatricesParam", null);
+
+        if (numberOfJointsParam == null) {
+            numberOfJointsParam = new MatParamOverride(VarType.Int, "NumberOfBones", null);
+            jointMatricesParam = new MatParamOverride(VarType.Matrix4Array, "BoneMatrices", null);
+            getSpatial().addMatParamOverride(numberOfJointsParam);
+            getSpatial().addMatParamOverride(jointMatricesParam);
+        }
+    }
+
+    /**
+     * Update the lists of animation targets.
+     *
+     * @param spatial the controlled spatial
+     */
+    private void updateTargetsAndMaterials(Spatial spatial) {
+        targets.clear();
+
+        if (spatial instanceof Node) {
+            findTargets((Node) spatial);
+        } else if (spatial instanceof Geometry) {
+            findTargets((Geometry) spatial);
+        }
+    }
+}

+ 292 - 0
jme3-core/src/main/java/com/jme3/animation/Joint.java

@@ -0,0 +1,292 @@
+package com.jme3.animation;
+
+import com.jme3.export.*;
+import com.jme3.material.MatParamOverride;
+import com.jme3.math.*;
+import com.jme3.scene.*;
+import com.jme3.shader.VarType;
+import com.jme3.util.SafeArrayList;
+import com.jme3.util.clone.Cloner;
+import com.jme3.util.clone.JmeCloneable;
+
+import java.io.IOException;
+import java.util.ArrayList;
+
+/**
+ * A Joint is the basic component of an armature designed to perform skeletal animation
+ * Created by Nehon on 15/12/2017.
+ */
+public class Joint implements Savable, JmeCloneable {
+
+    private String name;
+    private Joint parent;
+    private ArrayList<Joint> children = new ArrayList<>();
+    private Geometry targetGeometry;
+
+    public Joint() {
+    }
+
+    public Joint(String name) {
+        this.name = name;
+    }
+
+    /**
+     * The attachment node.
+     */
+    private Node attachedNode;
+
+    /**
+     * The transform of the joint in local space. Relative to its parent.
+     * Or relative to the model's origin for the root joint.
+     */
+    private Transform localTransform = new Transform();
+
+    /**
+     * The base transform of the joint in local space.
+     * Those transform are the bone's initial value.
+     */
+    private Transform baseLocalTransform = new Transform();
+
+    /**
+     * The transform of the bone in model space. Relative to the origin of the model.
+     */
+    private Transform modelTransform = new Transform();
+
+    /**
+     * The matrix used to transform affected vertices position into the bone model space.
+     * Used for skinning.
+     */
+    private Matrix4f inverseModelBindMatrix = new Matrix4f();
+
+    /**
+     * Updates world transforms for this bone and it's children.
+     */
+    public final void update() {
+        this.updateModelTransforms();
+
+        for (int i = children.size() - 1; i >= 0; i--) {
+            children.get(i).update();
+        }
+    }
+
+    /**
+     * Updates the model transforms for this bone, and, possibly the attach node
+     * if not null.
+     * <p>
+     * The model transform of this bone is computed by combining the parent's
+     * model transform with this bones' local transform.
+     */
+    public final void updateModelTransforms() {
+        modelTransform.set(localTransform);
+        if (parent != null) {
+            modelTransform.combineWithParent(parent.getModelTransform());
+        }
+
+        updateAttachNode();
+    }
+
+    /**
+     * Update the local transform of the attachments node.
+     */
+    private void updateAttachNode() {
+        if (attachedNode == null) {
+            return;
+        }
+        Node attachParent = attachedNode.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.
+             */
+            attachedNode.setLocalTransform(modelTransform);
+
+        } else {
+            Spatial loopSpatial = targetGeometry;
+            Transform combined = modelTransform.clone();
+            /*
+             * 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();
+            }
+            attachedNode.setLocalTransform(combined);
+        }
+    }
+
+    /**
+     * Stores the skinning transform in the specified Matrix4f.
+     * The skinning transform applies the animation of the bone to a vertex.
+     * <p>
+     * This assumes that the world transforms for the entire bone hierarchy
+     * have already been computed, otherwise this method will return undefined
+     * results.
+     *
+     * @param outTransform
+     */
+    void getOffsetTransform(Matrix4f outTransform, Quaternion tmp1, Vector3f tmp2, Vector3f tmp3, Matrix3f tmp4) {
+        modelTransform.toTransformMatrix(outTransform).mult(inverseModelBindMatrix, outTransform);
+    }
+
+    protected void setBindPose() {
+        //Note that the whole Armature must be updated before calling this method.
+        modelTransform.toTransformMatrix(inverseModelBindMatrix);
+        inverseModelBindMatrix.invertLocal();
+    }
+
+    public Vector3f getLocalTranslation() {
+        return localTransform.getTranslation();
+    }
+
+    public Quaternion getLocalRotation() {
+        return localTransform.getRotation();
+    }
+
+    public Vector3f getLocalScale() {
+        return localTransform.getScale();
+    }
+
+    public void setLocalTranslation(Vector3f translation) {
+        localTransform.setTranslation(translation);
+    }
+
+    public void setLocalRotation(Quaternion rotation) {
+        localTransform.setRotation(rotation);
+    }
+
+    public void setLocalScale(Vector3f scale) {
+        localTransform.setScale(scale);
+    }
+
+    public void addChild(Joint child) {
+        children.add(child);
+        child.parent = this;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public void setLocalTransform(Transform localTransform) {
+        this.localTransform.set(localTransform);
+    }
+
+    public void setInverseModelBindMatrix(Matrix4f inverseModelBindMatrix) {
+        this.inverseModelBindMatrix = inverseModelBindMatrix;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Joint getParent() {
+        return parent;
+    }
+
+    public ArrayList<Joint> getChildren() {
+        return children;
+    }
+
+    /**
+     * Access the attachments node of this joint. If this joint doesn't already
+     * have an attachments node, create one. Models and effects attached to the
+     * attachments node will follow this bone's motions.
+     *
+     * @param jointIndex this bone's index in its armature (&ge;0)
+     * @param targets    a list of geometries animated by this bone's skeleton (not
+     *                   null, unaffected)
+     */
+    Node getAttachmentsNode(int jointIndex, SafeArrayList<Geometry> targets) {
+        targetGeometry = null;
+        /*
+         * Search for a geometry animated by this particular bone.
+         */
+        for (Geometry geometry : targets) {
+            Mesh mesh = geometry.getMesh();
+            if (mesh != null && mesh.isAnimatedByJoint(jointIndex)) {
+                targetGeometry = geometry;
+                break;
+            }
+        }
+
+        if (attachedNode == null) {
+            attachedNode = new Node(name + "_attachnode");
+            attachedNode.setUserData("AttachedBone", this);
+            //We don't want the node to have a numBone set by a parent node so we force it to null
+            attachedNode.addMatParamOverride(new MatParamOverride(VarType.Int, "NumberOfBones", null));
+        }
+
+        return attachedNode;
+    }
+
+
+    public Transform getLocalTransform() {
+        return localTransform;
+    }
+
+    public Transform getModelTransform() {
+        return modelTransform;
+    }
+
+    public Matrix4f getInverseModelBindMatrix() {
+        return inverseModelBindMatrix;
+    }
+
+    @Override
+    public Object jmeClone() {
+        try {
+            Joint clone = (Joint) super.clone();
+            return clone;
+        } catch (CloneNotSupportedException ex) {
+            throw new AssertionError();
+        }
+    }
+
+    @Override
+    public void cloneFields(Cloner cloner, Object original) {
+        this.children = cloner.clone(children);
+        this.attachedNode = cloner.clone(attachedNode);
+        this.targetGeometry = cloner.clone(targetGeometry);
+
+        this.baseLocalTransform = cloner.clone(baseLocalTransform);
+        this.localTransform = cloner.clone(baseLocalTransform);
+        this.modelTransform = cloner.clone(baseLocalTransform);
+        this.inverseModelBindMatrix = cloner.clone(inverseModelBindMatrix);
+    }
+
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public void read(JmeImporter im) throws IOException {
+        InputCapsule input = im.getCapsule(this);
+
+        name = input.readString("name", null);
+        attachedNode = (Node) input.readSavable("attachedNode", null);
+        targetGeometry = (Geometry) input.readSavable("targetGeometry", null);
+        baseLocalTransform = (Transform) input.readSavable("baseLocalTransforms", baseLocalTransform);
+        localTransform.set(baseLocalTransform);
+        inverseModelBindMatrix = (Matrix4f) input.readSavable("inverseModelBindMatrix", inverseModelBindMatrix);
+
+        ArrayList<Joint> childList = input.readSavableArrayList("children", null);
+        for (int i = childList.size() - 1; i >= 0; i--) {
+            this.addChild(childList.get(i));
+        }
+    }
+
+    @Override
+    public void write(JmeExporter ex) throws IOException {
+        OutputCapsule output = ex.getCapsule(this);
+
+        output.write(name, "name", null);
+        output.write(attachedNode, "attachedNode", null);
+        output.write(targetGeometry, "targetGeometry", null);
+        output.write(baseLocalTransform, "baseLocalTransform", new Transform());
+        output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f());
+        output.writeSavableArrayList(children, "children", null);
+    }
+
+}

+ 12 - 5
jme3-core/src/main/java/com/jme3/math/Transform.java

@@ -32,6 +32,7 @@
 package com.jme3.math;
 
 import com.jme3.export.*;
+
 import java.io.IOException;
 
 /**
@@ -257,11 +258,17 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
     }
 
     public Matrix4f toTransformMatrix() {
-        Matrix4f trans = new Matrix4f();
-        trans.setTranslation(translation);
-        trans.setRotationQuaternion(rot);
-        trans.setScale(scale);
-        return trans;
+        return toTransformMatrix(null);
+    }
+
+    public Matrix4f toTransformMatrix(Matrix4f store) {
+        if (store == null) {
+            store = new Matrix4f();
+        }
+        store.setTranslation(translation);
+        store.setRotationQuaternion(rot);
+        store.setScale(scale);
+        return store;
     }
     
     public void fromTransformMatrix(Matrix4f mat) {

+ 16 - 13
jme3-core/src/main/java/com/jme3/scene/Mesh.java

@@ -39,18 +39,11 @@ import com.jme3.collision.bih.BIHTree;
 import com.jme3.export.*;
 import com.jme3.material.Material;
 import com.jme3.material.RenderState;
-import com.jme3.math.Matrix4f;
-import com.jme3.math.Triangle;
-import com.jme3.math.Vector2f;
-import com.jme3.math.Vector3f;
-import com.jme3.scene.VertexBuffer.Format;
-import com.jme3.scene.VertexBuffer.Type;
-import com.jme3.scene.VertexBuffer.Usage;
+import com.jme3.math.*;
+import com.jme3.scene.VertexBuffer.*;
 import com.jme3.scene.mesh.*;
-import com.jme3.util.BufferUtils;
-import com.jme3.util.IntMap;
+import com.jme3.util.*;
 import com.jme3.util.IntMap.Entry;
-import com.jme3.util.SafeArrayList;
 import com.jme3.util.clone.Cloner;
 import com.jme3.util.clone.JmeCloneable;
 
@@ -1446,13 +1439,23 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
                getBuffer(Type.HWBoneIndex) != null;
     }
 
+    /**
+     * @deprecated use isAnimatedByJoint
+     * @param boneIndex
+     * @return
+     */
+    @Deprecated
+    public boolean isAnimatedByBone(int boneIndex) {
+        return isAnimatedByJoint(boneIndex);
+    }
+
     /**
      * Test whether the specified bone animates this mesh.
      *
-     * @param boneIndex the bone's index in its skeleton
+     * @param jointIndex the bone's index in its skeleton
      * @return true if the specified bone animates this mesh, otherwise false
      */
-    public boolean isAnimatedByBone(int boneIndex) {
+    public boolean isAnimatedByJoint(int jointIndex) {
         VertexBuffer biBuf = getBuffer(VertexBuffer.Type.BoneIndex);
         VertexBuffer wBuf = getBuffer(VertexBuffer.Type.BoneWeight);
         if (biBuf == null || wBuf == null) {
@@ -1472,7 +1475,7 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         /*
          * Test each vertex to determine whether the bone affects it.
          */
-        int biByte = boneIndex;
+        int biByte = jointIndex;
         for (int vIndex = 0; vIndex < numVertices; vIndex++) {
             for (int wIndex = 0; wIndex < 4; wIndex++) {
                 int bIndex = boneIndexBuffer.get();