diff --git a/jme3-core/src/main/java/com/jme3/animation/Armature.java b/jme3-core/src/main/java/com/jme3/animation/Armature.java
new file mode 100644
index 000000000..9c09e63a0
--- /dev/null
+++ b/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.
+ *
+ * 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 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.
+// *
+// * 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);
+ }
+
+}
diff --git a/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java b/jme3-core/src/main/java/com/jme3/animation/ArmatureControl.java
new file mode 100644
index 000000000..00f017f32
--- /dev/null
+++ b/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 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.
+ */
+ 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);
+ }
+ }
+}
diff --git a/jme3-core/src/main/java/com/jme3/animation/Joint.java b/jme3-core/src/main/java/com/jme3/animation/Joint.java
new file mode 100644
index 000000000..a5a35af7f
--- /dev/null
+++ b/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 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.
+ *
+ * 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.
+ *
+ * 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 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 (≥0)
+ * @param targets a list of geometries animated by this bone's skeleton (not
+ * null, unaffected)
+ */
+ Node getAttachmentsNode(int jointIndex, 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.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 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);
+ }
+
+}
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 6fb29c99b..f0a18bf91 100644
--- a/jme3-core/src/main/java/com/jme3/math/Transform.java
+++ b/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) {
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 e07c2cfa2..e9fccb73e 100644
--- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java
+++ b/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();