Przeglądaj źródła

Implemented bone animation. This is still in WIP state, currently working for the most simple cases.

Nehon 8 lat temu
rodzic
commit
ea6c406979

+ 7 - 10
jme3-core/src/main/java/com/jme3/math/Transform.java

@@ -186,27 +186,24 @@ public final class Transform implements Savable, Cloneable, java.io.Serializable
     }
 
     /**
-     * Changes the values of this matrix acording to it's parent.  Very similar to the concept of Node/Spatial transforms.
+     * Changes the values of this matrix according to it's parent.  Very similar to the concept of Node/Spatial transforms.
      * @param parent The parent matrix.
      * @return This matrix, after combining.
      */
     public Transform combineWithParent(Transform parent) {
+        //applying parent scale to local scale
         scale.multLocal(parent.scale);
-//        rot.multLocal(parent.rot);
+        //applying parent rotation to local rotation.
         parent.rot.mult(rot, rot);
-
-        // This here, is evil code
-//        parent
-//            .rot
-//            .multLocal(translation)
-//            .multLocal(parent.scale)
-//            .addLocal(parent.translation);
-
+        //applying parent scale to local translation.
         translation.multLocal(parent.scale);
+        //applying parent rotation to local translation, then applying parent translation to local translation.
+        //Note that parent.rot.multLocal(translation) doesn't modify "parent.rot" but "translation"
         parent
             .rot
             .multLocal(translation)
             .addLocal(parent.translation);
+
         return this;
     }
 

+ 29 - 7
jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java

@@ -36,6 +36,7 @@ import java.util.Map;
 
 import com.jme3.animation.Bone;
 import com.jme3.animation.Skeleton;
+import com.jme3.bounding.*;
 import com.jme3.math.FastMath;
 import com.jme3.math.Quaternion;
 import com.jme3.math.Vector3f;
@@ -94,6 +95,32 @@ public class SkeletonBone extends Node {
         for (Bone bone : skeleton.getRoots()) {
             createSkeletonGeoms(bone, boneShape, jointShape, boneLengths, skeleton, this, guessBonesOrientation);
         }
+        this.updateModelBound();
+
+
+        Sphere originShape = new Sphere(10, 10, 0.02f);
+        originShape.setBuffer(VertexBuffer.Type.Color, 4, createFloatBuffer(originShape.getVertexCount() * 4));
+        cb = originShape.getFloatBuffer(VertexBuffer.Type.Color);
+        cb.rewind();
+        for (int i = 0; i < jointShape.getVertexCount(); i++) {
+            cb.put(0.4f).put(0.4f).put(0.05f).put(1f);
+        }
+
+        Geometry origin = new Geometry("origin", originShape);
+        BoundingVolume bv = this.getWorldBound();
+        float scale = 1;
+        if (bv.getType() == BoundingVolume.Type.AABB) {
+            BoundingBox bb = (BoundingBox) bv;
+            scale = (bb.getXExtent() + bb.getYExtent() + bb.getZExtent()) / 3f;
+        } else if (bv.getType() == BoundingVolume.Type.Sphere) {
+            BoundingSphere bs = (BoundingSphere) bv;
+            scale = bs.getRadius();
+        }
+        origin.scale(scale);
+        attachChild(origin);
+
+
+
     }
 
     protected final void createSkeletonGeoms(Bone bone, Mesh boneShape, Mesh jointShape, Map<Integer, Float> boneLengths, Skeleton skeleton, Node parent, boolean guessBonesOrientation) {
@@ -120,6 +147,7 @@ public class SkeletonBone extends Node {
                 Quaternion q = new Quaternion();
                 q.lookAt(v, Vector3f.UNIT_Z);
                 bGeom.setLocalRotation(q);
+                boneLength = v.length();
             }
             //no child, the bone has the same direction as the parent bone.
             if (bone.getChildren().isEmpty()) {
@@ -184,12 +212,6 @@ public class SkeletonBone extends Node {
     }
 
 
-//    private Quaternion getRotationBetweenVect(Vector3f v1, Vector3f v2){       
-//        Vector3f a =v1.cross(v2);
-//        float w = FastMath.sqrt((v1.length() * v1.length()) * (v2.length() * v2.length())) + v1.dot(v2);       
-//        return new Quaternion(a.x, a.y, a.z, w).normalizeLocal() ;
-//    }
-
     protected final void updateSkeletonGeoms(Bone bone) {
         if (guessBonesOrientation && bone.getName().equalsIgnoreCase("Site")) {
             return;
@@ -205,7 +227,7 @@ public class SkeletonBone extends Node {
     }
 
     /**
-     * The method updates the geometry according to the poitions of the bones.
+     * The method updates the geometry according to the positions of the bones.
      */
     public void updateGeometry() {
 

+ 6 - 0
jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java

@@ -60,6 +60,11 @@ public class SkeletonDebugAppState extends AbstractAppState {
     public SkeletonDebugger addSkeleton(SkeletonControl skeletonControl, boolean guessBonesOrientation) {
         Skeleton skeleton = skeletonControl.getSkeleton();
         Spatial forSpatial = skeletonControl.getSpatial();
+        return addSkeleton(skeleton, forSpatial, guessBonesOrientation);
+    }
+
+    public SkeletonDebugger addSkeleton(Skeleton skeleton, Spatial forSpatial, boolean guessBonesOrientation) {
+
         SkeletonDebugger sd = new SkeletonDebugger(forSpatial.getName() + "_Skeleton", skeleton, guessBonesOrientation);
         sd.setLocalTransform(forSpatial.getWorldTransform());
         if (forSpatial instanceof Node) {
@@ -113,6 +118,7 @@ public class SkeletonDebugAppState extends AbstractAppState {
                             selectedBones.put(skeleton.getSkeleton(), selectedBone);
                             System.err.println("-----------------------");
                             System.err.println("Selected Bone : " + selectedBone.getName() + " in skeleton " + skeleton.getName());
+                            System.err.println("Root Bone : " + (selectedBone.getParent() == null));
                             System.err.println("-----------------------");
                             System.err.println("Bind translation: " + selectedBone.getBindPosition());
                             System.err.println("Bind rotation: " + selectedBone.getBindRotation());

+ 225 - 16
jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java

@@ -31,48 +31,257 @@
  */
 package jme3test.model;
 
+import com.jme3.animation.*;
+import com.jme3.app.ChaseCameraAppState;
 import com.jme3.app.SimpleApplication;
-import com.jme3.light.DirectionalLight;
-import com.jme3.light.PointLight;
+import com.jme3.input.KeyInput;
+import com.jme3.input.controls.ActionListener;
+import com.jme3.input.controls.KeyTrigger;
 import com.jme3.math.*;
-import com.jme3.scene.Geometry;
+import com.jme3.renderer.Limits;
+import com.jme3.scene.Node;
 import com.jme3.scene.Spatial;
-import com.jme3.scene.plugins.gltf.GltfModelKey;
-import com.jme3.scene.shape.Sphere;
+import com.jme3.scene.control.Control;
+import com.jme3.scene.debug.custom.SkeletonDebugAppState;
+
+import java.util.ArrayList;
+import java.util.List;
 
 public class TestGltfLoading extends SimpleApplication {
 
+    Node autoRotate = new Node("autoRotate");
+    List<Spatial> assets = new ArrayList<>();
+    Node probeNode;
+    float time = 0;
+    int assetIndex = 0;
+    boolean useAutoRotate = true;
+    private final static String indentString = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
+    int duration = 2;
+    boolean playAnim = true;
 
     public static void main(String[] args) {
         TestGltfLoading app = new TestGltfLoading();
         app.start();
     }
 
+    /*
+    WARNING this test case can't wok without the assets, and considering their size, they are not pushed into the repo
+    you can find them here :
+    https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0
+    https://sketchfab.com/features/gltf
+    You have to copy them in Model/gltf folder in the test-data project.
+     */
     public void simpleInitApp() {
-        flyCam.setMoveSpeed(10f);
-        viewPort.setBackgroundColor(ColorRGBA.DarkGray);
 
-        // sunset light
+        SkeletonDebugAppState skeletonDebugAppState = new SkeletonDebugAppState();
+        getStateManager().attach(skeletonDebugAppState);
+
+        // cam.setLocation(new Vector3f(4.0339394f, 2.645184f, 6.4627485f));
+        // cam.setRotation(new Quaternion(-0.013950467f, 0.98604023f, -0.119502485f, -0.11510504f));
+        cam.setFrustumPerspective(45f, (float) cam.getWidth() / cam.getHeight(), 0.1f, 100f);
+        renderer.setDefaultAnisotropicFilter(Math.min(renderer.getLimits().get(Limits.TextureAnisotropy), 8));
+        setPauseOnLostFocus(false);
+
+        flyCam.setMoveSpeed(5);
+        flyCam.setDragToRotate(true);
+        flyCam.setEnabled(false);
+        viewPort.setBackgroundColor(new ColorRGBA().setAsSrgb(0.2f, 0.2f, 0.2f, 1.0f));
+        rootNode.attachChild(autoRotate);
+        probeNode = (Node) assetManager.loadModel("Scenes/defaultProbe.j3o");
+        autoRotate.attachChild(probeNode);
+
 //        DirectionalLight dl = new DirectionalLight();
 //        dl.setDirection(new Vector3f(-1f, -1.0f, -1f).normalizeLocal());
 //        dl.setColor(new ColorRGBA(1f, 1f, 1f, 1.0f));
 //        rootNode.addLight(dl);
-//
+
 //        DirectionalLight dl2 = new DirectionalLight();
 //        dl2.setDirection(new Vector3f(1f, 1.0f, 1f).normalizeLocal());
 //        dl2.setColor(new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f));
 //        rootNode.addLight(dl2);
 
-        PointLight pl = new PointLight(new Vector3f(5.0f, 5.0f, 5.0f), ColorRGBA.White, 30);
-        rootNode.addLight(pl);
-        PointLight pl1 = new PointLight(new Vector3f(-5.0f, -5.0f, -5.0f), ColorRGBA.White.mult(0.5f), 50);
-        rootNode.addLight(pl1);
+//        PointLight pl = new PointLight(new Vector3f(5.0f, 5.0f, 5.0f), ColorRGBA.White, 30);
+//        rootNode.addLight(pl);
+//        PointLight pl1 = new PointLight(new Vector3f(-5.0f, -5.0f, -5.0f), ColorRGBA.White.mult(0.5f), 50);
+//        rootNode.addLight(pl1);
+
+//        loadModel("Models/gltf/box/box.gltf", Vector3f.ZERO, 1);
+//        loadModel("Models/gltf/duck/Duck.gltf", new Vector3f(0, -1, 0), 1);
+        //loadModel("Models/gltf/damagedHelmet/damagedHelmet.gltf", Vector3f.ZERO, 1);
+        //loadModel("Models/gltf/hornet/scene.gltf", new Vector3f(0, -0.5f, 0), 0.4f);
+//        loadModel("Models/gltf/adamHead/adamHead.gltf", Vector3f.ZERO, 0.6f);
+        //loadModel("Models/gltf/busterDrone/busterDrone.gltf", new Vector3f(0, 0f, 0), 0.8f);
+        //loadModel("Models/gltf/animatedCube/AnimatedCube.gltf", Vector3f.ZERO, 0.5f);
+
+        //TODO need to pad tracks that doesn't have the same length than the animation.
+        //loadModel("Models/gltf/BoxAnimated/BoxAnimated.gltf", new Vector3f(0, 0f, 0), 0.8f);
+
+        //loadModel("Models/gltf/RiggedFigure/RiggedSimple.gltf", new Vector3f(0, -0.3f, 0), 0.2f);
+        //loadModel("Models/gltf/RiggedFigure/RiggedFigure.gltf", new Vector3f(0, -1f, 0), 1f);
+        //loadModel("Models/gltf/CesiumMan/CesiumMan.gltf", new Vector3f(0, -1, 0), 1f);
+        //loadModel("Models/gltf/BrainStem/BrainStem.gltf", new Vector3f(0, -1, 0), 1f);
+        //loadModel("Models/gltf/Jaime/Jaime.gltf", new Vector3f(0, -1, 0), 1f);
+        //loadModel("Models/gltf/GiantWorm/GiantWorm.gltf", new Vector3f(0, -1, 0), 1f);
+        //loadModel("Models/gltf/RiggedFigure/WalkingLady.gltf", new Vector3f(0, -0.f, 0), 1f);
+//        loadModel("Models/gltf/Monster/Monster.gltf", Vector3f.ZERO, 0.03f);
+
+
+        probeNode.attachChild(assets.get(0));
+
+        ChaseCameraAppState chaseCam = new ChaseCameraAppState();
+        chaseCam.setTarget(probeNode);
+        getStateManager().attach(chaseCam);
+        chaseCam.setInvertHorizontalAxis(true);
+        chaseCam.setInvertVerticalAxis(true);
+        chaseCam.setZoomSpeed(0.5f);
+        chaseCam.setMinVerticalRotation(-FastMath.HALF_PI);
+        chaseCam.setRotationSpeed(3);
+        chaseCam.setDefaultDistance(3);
+        chaseCam.setDefaultVerticalRotation(0.3f);
+
+        inputManager.addMapping("autorotate", new KeyTrigger(KeyInput.KEY_SPACE));
+        inputManager.addListener(new ActionListener() {
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if (isPressed) {
+                    useAutoRotate = !useAutoRotate;
+
+                }
+            }
+        }, "autorotate");
+
+        inputManager.addMapping("toggleAnim", new KeyTrigger(KeyInput.KEY_RETURN));
+        inputManager.addListener(new ActionListener() {
+            @Override
+            public void onAction(String name, boolean isPressed, float tpf) {
+                if (isPressed) {
+                    playAnim = !playAnim;
+                    if (playAnim) {
+                        playFirstAnim(rootNode);
+                    } else {
+                        stopAnim(rootNode);
+                    }
+                }
+            }
+        }, "toggleAnim");
+
+        dumpScene(rootNode, 0);
+    }
+
+    private <T extends Control> T findControl(Spatial s, Class<T> controlClass) {
+        T ctrl = s.getControl(controlClass);
+        if (ctrl != null) {
+            return ctrl;
+        }
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial spatial : n.getChildren()) {
+                ctrl = findControl(spatial, controlClass);
+                if (ctrl != null) {
+                    return ctrl;
+                }
+            }
+        }
+        return null;
+    }
+
+    private void loadModel(String path, Vector3f offset, float scale) {
+        Spatial s = assetManager.loadModel(path);
+        s.scale(scale);
+        s.move(offset);
+        assets.add(s);
+        if (playAnim) {
+            playFirstAnim(s);
+        }
+
+//        SkeletonControl ctrl = findControl(s, SkeletonControl.class);
+//        //ctrl.getSpatial().removeControl(ctrl);
+//        if (ctrl == null) {
+//            return;
+//        }
+//        getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(ctrl, true);
+//        AnimControl aCtrl = findControl(s, AnimControl.class);
+//        //ctrl.getSpatial().removeControl(ctrl);
+//        if (aCtrl == null) {
+//            return;
+//        }
+//        if (aCtrl.getSkeleton() != null) {
+//            getStateManager().getState(SkeletonDebugAppState.class).addSkeleton(aCtrl.getSkeleton(), aCtrl.getSpatial(), true);
+//        }
+
+    }
+
+    private void playFirstAnim(Spatial s) {
 
-        rootNode.attachChild(assetManager.loadModel("Models/gltf/box/box.gltf"));
-        //rootNode.attachChild(assetManager.loadModel(new GltfModelKey("Models/gltf/duck/Duck.gltf")));
+        AnimControl control = s.getControl(AnimControl.class);
+        if (control != null) {
+//            if (control.getAnimationNames().size() > 0) {
+//                control.createChannel().setAnim(control.getAnimationNames().iterator().next());
+//            }
+            for (String name : control.getAnimationNames()) {
+                control.createChannel().setAnim(name);
+            }
 
-        //rootNode.attachChild(assetManager.loadModel("Models/gltf/hornet/scene.gltf"));
+        }
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial spatial : n.getChildren()) {
+                playFirstAnim(spatial);
+            }
+        }
     }
 
+    private void stopAnim(Spatial s) {
 
+        AnimControl control = s.getControl(AnimControl.class);
+        if (control != null) {
+            for (int i = 0; i < control.getNumChannels(); i++) {
+                AnimChannel ch = control.getChannel(i);
+                ch.reset(true);
+            }
+            control.clearChannels();
+            if (control.getSkeleton() != null) {
+                control.getSkeleton().reset();
+            }
+
+        }
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial spatial : n.getChildren()) {
+                stopAnim(spatial);
+            }
+        }
+    }
+
+    @Override
+    public void simpleUpdate(float tpf) {
+
+        if (!useAutoRotate) {
+            return;
+        }
+        time += tpf;
+        autoRotate.rotate(0, tpf * 0.5f, 0);
+        if (time > duration) {
+            assets.get(assetIndex).removeFromParent();
+            assetIndex = (assetIndex + 1) % assets.size();
+            if (assetIndex == 0) {
+                duration = 10;
+            }
+            probeNode.attachChild(assets.get(assetIndex));
+            time = 0;
+        }
+    }
+
+    private void dumpScene(Spatial s, int indent) {
+        System.err.println(indentString.substring(0, indent) + s.getName() + " (" + s.getClass().getSimpleName() + ") / " +
+                s.getLocalTransform().getTranslation().toString() + ", " +
+                s.getLocalTransform().getRotation().toString() + ", " +
+                s.getLocalTransform().getScale().toString());
+        if (s instanceof Node) {
+            Node n = (Node) s;
+            for (Spatial spatial : n.getChildren()) {
+                dumpScene(spatial, indent + 1);
+            }
+        }
+    }
 }

+ 188 - 56
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

@@ -15,10 +15,7 @@ import com.jme3.util.mikktspace.MikktspaceTangentGenerator;
 
 import java.io.*;
 import java.nio.Buffer;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
@@ -56,8 +53,9 @@ public class GltfLoader implements AssetLoader {
     private Matrix4fArrayPopulator matrix4fArrayPopulator = new Matrix4fArrayPopulator();
     private static Map<String, MaterialAdapter> defaultMaterialAdapters = new HashMap<>();
     private boolean useNormalsFlag = false;
+    private Transform tmpTransforms = new Transform();
 
-    Map<Skeleton, List<Spatial>> skinnedSpatials = new HashMap<>();
+    Map<SkinData, List<Spatial>> skinnedSpatials = new HashMap<>();
 
     static {
         defaultMaterialAdapters.put("pbrMetallicRoughness", new PBRMaterialAdapter());
@@ -146,6 +144,13 @@ public class GltfLoader implements AssetLoader {
             root.attachChild(sceneNode);
         }
 
+        //update skeletons
+        for (int i = 0; i < skins.size(); i++) {
+            SkinData sd = fetchFromCache("skins", i, SkinData.class);
+            sd.skeletonControl.getSkeleton().resetAndUpdate();
+            sd.skeletonControl.getSkeleton().setBindingPose();
+        }
+
         //Loading animations
         if (animations != null) {
             for (int i = 0; i < animations.size(); i++) {
@@ -165,7 +170,7 @@ public class GltfLoader implements AssetLoader {
     private Object readNode(int nodeIndex) throws IOException {
         Object obj = fetchFromCache("nodes", nodeIndex, Object.class);
         if (obj != null) {
-            if (obj instanceof Bone) {
+            if (obj instanceof BoneWrapper) {
                 //the node can be a previously loaded bone let's return it
                 return obj;
             } else {
@@ -207,11 +212,13 @@ public class GltfLoader implements AssetLoader {
 
         Integer skinIndex = getAsInteger(nodeData, "skin");
         if (skinIndex != null) {
-            Skeleton skeleton = fetchFromCache("skins", skinIndex, Skeleton.class);
-            List<Spatial> spatials = skinnedSpatials.get(skeleton);
+            SkinData skinData = fetchFromCache("skins", skinIndex, SkinData.class);
+            List<Spatial> spatials = skinnedSpatials.get(skinData);
             spatials.add(spatial);
         }
 
+        spatial.setLocalTransform(readTransforms(nodeData));
+
         if (children != null) {
             for (JsonElement child : children) {
                 readChild(spatial, child);
@@ -221,7 +228,6 @@ public class GltfLoader implements AssetLoader {
         if (spatial.getName() == null) {
             spatial.setName(getAsString(nodeData.getAsJsonObject(), "name"));
         }
-        spatial.setLocalTransform(readTransforms(nodeData));
 
         addToCache("nodes", nodeIndex, spatial, nodes.size());
         return spatial;
@@ -232,14 +238,34 @@ public class GltfLoader implements AssetLoader {
         Object loaded = readNode(child.getAsInt());
         if (loaded instanceof Spatial) {
             ((Node) parent).attachChild((Spatial) loaded);
-        } else if (loaded instanceof Bone) {
-            //fetch the skeleton and add a skeletonControl to the node.
-            // Skeleton skeleton = fetchFromCache("skeletons", index, Skeleton.class);
-            // SkeletonControl control = new SkeletonControl(skeleton);
-            // parent.addControl(control);
+        } else if (loaded instanceof BoneWrapper) {
+            //parent is the Armature Node, we have to apply its transforms to all the Bones' bind pose
+            BoneWrapper bw = (BoneWrapper) loaded;
+            //TODO this part is still not woking properly.
+            applyTransformsToArmature(bw, parent.getLocalTransform());
+
+            //now we can remove the parent node as it's not meant as a real node.
+            parent.removeFromParent();
         }
     }
 
+    private void applyTransformsToArmature(BoneWrapper boneWrapper, Transform transforms) {
+        //Transforms are mean in model space, so we need some tricky transformation
+        //We have this inverseBindMatrix provided in the gltf for each bone that transforms a vector from mesh's model space to bone's local space.
+        //So it's inverse, transforms from bone's local space to mesh model space.
+        // we need to transform the bone's bind transforms in this mesh model space, transform them with the transform given in this method,
+        // then recompute their local space value according to parents model space
+        Bone bone = boneWrapper.bone;
+        tmpTransforms.setTranslation(bone.getBindPosition());
+        tmpTransforms.setRotation(bone.getBindRotation());
+        tmpTransforms.setScale(bone.getBindScale());
+
+        tmpTransforms.combineWithParent(transforms);
+
+        bone.setBindTransforms(tmpTransforms.getTranslation(), tmpTransforms.getRotation(), tmpTransforms.getScale());
+
+    }
+
     private Transform readTransforms(JsonObject nodeData) {
         Transform transform = new Transform();
         JsonArray matrix = nodeData.getAsJsonArray("matrix");
@@ -592,7 +618,7 @@ public class GltfLoader implements AssetLoader {
             } else {
                 //check if we are loading the same time array
                 if (animData.times != times) {
-                    throw new AssetLoadException("Channel has different input accessors for samplers");
+                    //            throw new AssetLoadException("Channel has different input accessors for samplers");
                 }
             }
             if (animData.length == null) {
@@ -619,6 +645,7 @@ public class GltfLoader implements AssetLoader {
         List<Spatial> spatials = new ArrayList<>();
         Animation anim = new Animation();
         anim.setName(name);
+        int skinIndex = -1;
 
         for (int i = 0; i < animatedNodes.length; i++) {
             AnimData animData = animatedNodes[i];
@@ -635,28 +662,58 @@ public class GltfLoader implements AssetLoader {
                 SpatialTrack track = new SpatialTrack(animData.times, animData.translations, animData.rotations, animData.scales);
                 track.setTrackSpatial(s);
                 anim.addTrack(track);
-            } else {
-                //At some point we'll have bone animation
-                //TODO support for bone animation.
-                System.err.println("animated");
-                System.err.println(node);
+            } else if (node instanceof BoneWrapper) {
+                BoneWrapper b = (BoneWrapper) node;
+                //apply the inverseBindMatrix to animation data.
+                b.update(animData);
+                BoneTrack track = new BoneTrack(b.boneIndex, animData.times, animData.translations, animData.rotations, animData.scales);
+                anim.addTrack(track);
+                if (skinIndex == -1) {
+                    skinIndex = b.skinIndex;
+                } else {
+                    //Check if all bones affected by this animation are from the same skin, otherwise raise an error.
+                    if (skinIndex != b.skinIndex) {
+                        throw new AssetLoadException("Animation " + animationIndex + " (" + name + ") applies to bones that are not from the same skin: skin " + skinIndex + ", bone " + b.bone.getName() + " from skin " + b.skinIndex);
+                    }
+                    //else everything is fine.
+                }
+            }
+        }
+
+        if (skinIndex != -1) {
+            //we have a bone animation.
+            SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
+            if (skin.animControl == null) {
+                skin.animControl = new AnimControl(skin.skeletonControl.getSkeleton());
             }
+            skin.animControl.addAnim(anim);
+            //the controls will be added to the right spatial in setupControls()
         }
 
+
         if (!spatials.isEmpty()) {
-            Spatial spatial = null;
-            if (spatials.size() == 1) {
-                spatial = spatials.get(0);
+            //Note that it's pretty unlikely to have an animation that is both a spatial animation and a bone animation...But you never know. The specs doesn't forbids it
+            if (skinIndex != -1) {
+                //there are some spatial tracks in this bone animation... or the other way around. Let's add the spatials in the skinnedSpatials.
+                SkinData skin = fetchFromCache("skins", skinIndex, SkinData.class);
+                List<Spatial> spat = skinnedSpatials.get(skin);
+                spat.addAll(spatials);
+                //the animControl will be added in the setupControls();
             } else {
-                spatial = findCommonAncestor(spatials);
-            }
+                Spatial spatial = null;
+                if (spatials.size() == 1) {
+                    spatial = spatials.get(0);
+                } else {
+                    spatial = findCommonAncestor(spatials);
+                }
 
-            AnimControl control = spatial.getControl(AnimControl.class);
-            if (control == null) {
-                control = new AnimControl();
-                spatial.addControl(control);
+                AnimControl control = spatial.getControl(AnimControl.class);
+                if (control == null) {
+                    control = new AnimControl();
+                    spatial.addControl(control);
+                }
+                control.addAnim(anim);
             }
-            control.addAnim(anim);
         }
     }
 
@@ -703,74 +760,113 @@ public class GltfLoader implements AssetLoader {
                 }
             }
 
-            System.err.println(inverseBindMatrices);
-
-            rootIndex = joints.get(0).getAsInt();
-
+            boolean addRootIndex = true;
             Bone[] bones = new Bone[joints.size()];
             for (int i = 0; i < joints.size(); i++) {
-                bones[i] = readNodeAsBone(joints.get(i).getAsInt(), inverseBindMatrices[i]);
+                int boneIndex = joints.get(i).getAsInt();
+                if (boneIndex == rootIndex) {
+                    addRootIndex = false;
+                }
+                bones[i] = readNodeAsBone(boneIndex, inverseBindMatrices[i], i, index);
             }
+
+            if (addRootIndex) {
+                //sometimes the root bone is not part of the joint array. in that case we add it at the end of the bone list.
+                //The bone won't deform the mesh, but that's pretty common with the root bone.
+                Bone[] newBones = new Bone[bones.length + 1];
+                System.arraycopy(bones, 0, newBones, 0, bones.length);
+                //TODO actually a regular node or a geometry can be attached to a bone, we have to handle this and attach it ti the AttachementNode.
+                newBones[bones.length] = readNodeAsBone(rootIndex, new Matrix4f(), bones.length, index);
+                findChildren(rootIndex);
+                bones = newBones;
+            }
+
             for (int i = 0; i < joints.size(); i++) {
                 findChildren(joints.get(i).getAsInt());
             }
 
             Skeleton skeleton = new Skeleton(bones);
-            addToCache("skins", index, skeleton, nodes.size());
-            skinnedSpatials.put(skeleton, new ArrayList<Spatial>());
 
-            System.err.println(skeleton);
+            SkinData skinData = new SkinData();
+            skinData.skeletonControl = new SkeletonControl(skeleton);
+            addToCache("skins", index, skinData, nodes.size());
+            skinnedSpatials.put(skinData, new ArrayList<Spatial>());
 
         }
 
     }
 
-    private Bone readNodeAsBone(int nodeIndex, Matrix4f inverseBindMatrix) throws IOException {
+    private Bone readNodeAsBone(int nodeIndex, Matrix4f inverseBindMatrix, int boneIndex, int skinIndex) throws IOException {
 
-        Bone bone = fetchFromCache("nodes", nodeIndex, Bone.class);
-        if (bone != null) {
-            return bone;
+        BoneWrapper boneWrapper = fetchFromCache("nodes", nodeIndex, BoneWrapper.class);
+        if (boneWrapper != null) {
+            return boneWrapper.bone;
         }
         JsonObject nodeData = nodes.get(nodeIndex).getAsJsonObject();
-        JsonArray children = nodeData.getAsJsonArray("children");
         String name = getAsString(nodeData, "name");
         if (name == null) {
             name = "Bone_" + nodeIndex;
         }
-        bone = new Bone(name);
+        Bone bone = new Bone(name);
         Transform boneTransforms = readTransforms(nodeData);
-        Transform inverseBind = new Transform();
-        inverseBind.fromTransformMatrix(inverseBindMatrix);
-        //  boneTransforms.combineWithParent(inverseBind);
         bone.setBindTransforms(boneTransforms.getTranslation(), boneTransforms.getRotation(), boneTransforms.getScale());
 
+        addToCache("nodes", nodeIndex, new BoneWrapper(bone, boneIndex, skinIndex, inverseBindMatrix), nodes.size());
+//
+//        System.err.println(bone.getName() + " " + inverseBindMatrix);
+//        tmpTransforms.fromTransformMatrix(inverseBindMatrix);
+//        System.err.println("t: " + tmpTransforms.getTranslation());
+//        System.err.println("r: " + tmpTransforms.getRotation());
+//        Quaternion q = tmpTransforms.getRotation();
+//        float[] axis = new float[3];
+//        q.toAngles(axis);
+//        for (int i = 0; i < axis.length; i++) {
+//            System.err.print(axis[i] + ", ");
+//        }
+//        System.err.println("");
+//        System.err.println("s: " + tmpTransforms.getScale());
 
-        addToCache("nodes", nodeIndex, bone, nodes.size());
         return bone;
     }
 
     private void findChildren(int nodeIndex) {
-        Bone bone = fetchFromCache("nodes", nodeIndex, Bone.class);
+        BoneWrapper bw = fetchFromCache("nodes", nodeIndex, BoneWrapper.class);
         JsonObject nodeData = nodes.get(nodeIndex).getAsJsonObject();
         JsonArray children = nodeData.getAsJsonArray("children");
         if (children != null) {
             for (JsonElement child : children) {
-                bone.addChild(fetchFromCache("nodes", child.getAsInt(), Bone.class));
+                int childIndex = child.getAsInt();
+                BoneWrapper cbw = fetchFromCache("nodes", childIndex, BoneWrapper.class);
+                if (cbw != null) {
+                    bw.bone.addChild(cbw.bone);
+                    bw.children.add(childIndex);
+                }
             }
         }
     }
 
     private void setupControls() {
-        for (Skeleton skeleton : skinnedSpatials.keySet()) {
-            List<Spatial> spatials = skinnedSpatials.get(skeleton);
-            Spatial spatial = null;
+        for (SkinData skinData : skinnedSpatials.keySet()) {
+            List<Spatial> spatials = skinnedSpatials.get(skinData);
+            Spatial spatial;
             if (spatials.size() >= 1) {
                 spatial = findCommonAncestor(spatials);
             } else {
                 spatial = spatials.get(0);
             }
-            SkeletonControl control = new SkeletonControl(skeleton);
-            spatial.addControl(control);
+
+            AnimControl animControl = spatial.getControl(AnimControl.class);
+            if (animControl != null) {
+                //The spatial already has an anim control, we need to merge it with the one in skinData. Then remove it.
+                for (String name : animControl.getAnimationNames()) {
+                    Animation anim = animControl.getAnim(name);
+                    skinData.animControl.addAnim(anim);
+                }
+                spatial.removeControl(animControl);
+            }
+
+            spatial.addControl(skinData.animControl);
+            spatial.addControl(skinData.skeletonControl);
         }
     }
 
@@ -805,6 +901,43 @@ public class GltfLoader implements AssetLoader {
         float[] weights;
     }
 
+    private class BoneWrapper {
+        Bone bone;
+        int boneIndex;
+        int skinIndex;
+        Matrix4f bindMatrix = new Matrix4f();
+        List<Integer> children = new ArrayList<>();
+
+        public BoneWrapper(Bone bone, int boneIndex, int skinIndex, Matrix4f inverseBindMatrix) {
+            this.bone = bone;
+            this.boneIndex = boneIndex;
+            this.skinIndex = skinIndex;
+            this.bindMatrix.set(inverseBindMatrix).invertLocal();
+        }
+
+        /**
+         * Applies the inverseBindMatrix to anim data.
+         */
+        public void update(AnimData data) {
+            Transform invBind = new Transform(bone.getBindPosition(), bone.getBindRotation(), bone.getBindScale());
+            invBind = invBind.invert();
+            //invBind.fromTransformMatrix(bindMatrix);
+            for (int i = 0; i < data.translations.length; i++) {
+                Transform t = new Transform(data.translations[i], data.rotations[i], data.scales[i]);
+                t.combineWithParent(invBind);
+                data.translations[i] = t.getTranslation();
+                data.rotations[i] = t.getRotation();
+                data.scales[i] = t.getScale();
+
+            }
+        }
+    }
+
+    private class SkinData {
+        SkeletonControl skeletonControl;
+        AnimControl animControl;
+    }
+
     private interface Populator<T> {
         T populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset) throws IOException;
     }
@@ -837,7 +970,6 @@ public class GltfLoader implements AssetLoader {
             }
             vb.setupData(VertexBuffer.Usage.Dynamic, numComponents, format, buff);
 
-
             return vb;
         }
 

BIN
jme3-testdata/src/main/resources/Scenes/defaultProbe.j3o