Browse Source

GLTF: armature loading.

Rémy Bouquet 8 years ago
parent
commit
6b3093aa3e

+ 64 - 47
jme3-core/src/main/java/com/jme3/scene/Mesh.java

@@ -331,6 +331,15 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
         this.modeStart = cloner.clone(modeStart);
     }
 
+    /**
+     * @param forSoftwareAnim
+     * @deprecated use generateBindPose();
+     */
+    @Deprecated
+    public void generateBindPose(boolean forSoftwareAnim) {
+        generateBindPose();
+    }
+
     /**
      * Generates the {@link Type#BindPosePosition}, {@link Type#BindPoseNormal},
      * and {@link Type#BindPoseTangent}
@@ -338,51 +347,48 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
      * buffers already set on the mesh.
      * This method does nothing if the mesh has no bone weight or index
      * buffers.
-     *
-     * @param forSoftwareAnim Should be true if the bind pose is to be generated.
      */
-    public void generateBindPose(boolean forSoftwareAnim){
-        if (forSoftwareAnim){
-            VertexBuffer pos = getBuffer(Type.Position);
-            if (pos == null || getBuffer(Type.BoneIndex) == null) {
-                // ignore, this mesh doesn't have positional data
-                // or it doesn't have bone-vertex assignments, so its not animated
-                return;
-            }
-
-            VertexBuffer bindPos = new VertexBuffer(Type.BindPosePosition);
-            bindPos.setupData(Usage.CpuOnly,
-                    pos.getNumComponents(),
-                    pos.getFormat(),
-                    BufferUtils.clone(pos.getData()));
-            setBuffer(bindPos);
-
-            // XXX: note that this method also sets stream mode
-            // so that animation is faster. this is not needed for hardware skinning
-            pos.setUsage(Usage.Stream);
-
-            VertexBuffer norm = getBuffer(Type.Normal);
-            if (norm != null) {
-                VertexBuffer bindNorm = new VertexBuffer(Type.BindPoseNormal);
-                bindNorm.setupData(Usage.CpuOnly,
-                        norm.getNumComponents(),
-                        norm.getFormat(),
-                        BufferUtils.clone(norm.getData()));
-                setBuffer(bindNorm);
-                norm.setUsage(Usage.Stream);
-            }
+    public void generateBindPose() {
+        VertexBuffer pos = getBuffer(Type.Position);
+        if (pos == null || getBuffer(Type.BoneIndex) == null) {
+            // ignore, this mesh doesn't have positional data
+            // or it doesn't have bone-vertex assignments, so its not animated
+            return;
+        }
 
-            VertexBuffer tangents = getBuffer(Type.Tangent);
-            if (tangents != null) {
-                VertexBuffer bindTangents = new VertexBuffer(Type.BindPoseTangent);
-                bindTangents.setupData(Usage.CpuOnly,
-                        tangents.getNumComponents(),
-                        tangents.getFormat(),
-                        BufferUtils.clone(tangents.getData()));
-                setBuffer(bindTangents);
-                tangents.setUsage(Usage.Stream);
-            }// else hardware setup does nothing, mesh already in bind pose
+        VertexBuffer bindPos = new VertexBuffer(Type.BindPosePosition);
+        bindPos.setupData(Usage.CpuOnly,
+                pos.getNumComponents(),
+                pos.getFormat(),
+                BufferUtils.clone(pos.getData()));
+        setBuffer(bindPos);
+
+        // XXX: note that this method also sets stream mode
+        // so that animation is faster. this is not needed for hardware skinning
+        pos.setUsage(Usage.Stream);
+
+        VertexBuffer norm = getBuffer(Type.Normal);
+        if (norm != null) {
+            VertexBuffer bindNorm = new VertexBuffer(Type.BindPoseNormal);
+            bindNorm.setupData(Usage.CpuOnly,
+                    norm.getNumComponents(),
+                    norm.getFormat(),
+                    BufferUtils.clone(norm.getData()));
+            setBuffer(bindNorm);
+            norm.setUsage(Usage.Stream);
         }
+
+        VertexBuffer tangents = getBuffer(Type.Tangent);
+        if (tangents != null) {
+            VertexBuffer bindTangents = new VertexBuffer(Type.BindPoseTangent);
+            bindTangents.setupData(Usage.CpuOnly,
+                    tangents.getNumComponents(),
+                    tangents.getFormat(),
+                    BufferUtils.clone(tangents.getData()));
+            setBuffer(bindTangents);
+            tangents.setUsage(Usage.Stream);
+        }// else hardware setup does nothing, mesh already in bind pose
+
     }
 
     /**
@@ -429,13 +435,24 @@ public class Mesh implements Savable, Cloneable, JmeCloneable {
             //if HWBoneIndex and HWBoneWeight are empty, we setup them as direct
             //buffers with software anim buffers data
             VertexBuffer indicesHW = getBuffer(Type.HWBoneIndex);
+            Buffer result;
             if (indicesHW.getData() == null) {
                 VertexBuffer indices = getBuffer(Type.BoneIndex);
-                ByteBuffer originalIndex = (ByteBuffer) indices.getData();
-                ByteBuffer directIndex = BufferUtils.createByteBuffer(originalIndex.capacity());
-                originalIndex.clear();
-                directIndex.put(originalIndex);
-                indicesHW.setupData(Usage.Static, indices.getNumComponents(), indices.getFormat(), directIndex);
+                if (indices.getFormat() == Format.UnsignedByte) {
+                    ByteBuffer originalIndex = (ByteBuffer) indices.getData();
+                    ByteBuffer directIndex = BufferUtils.createByteBuffer(originalIndex.capacity());
+                    originalIndex.clear();
+                    directIndex.put(originalIndex);
+                    result = directIndex;
+                } else {
+                    //bone indices can be stored in an UnsignedShort buffer
+                    ShortBuffer originalIndex = (ShortBuffer) indices.getData();
+                    ShortBuffer directIndex = BufferUtils.createShortBuffer(originalIndex.capacity());
+                    originalIndex.clear();
+                    directIndex.put(originalIndex);
+                    result = directIndex;
+                }
+                indicesHW.setupData(Usage.Static, indices.getNumComponents(), indices.getFormat(), result);
             }
 
             VertexBuffer weightsHW = getBuffer(Type.HWBoneWeight);

+ 183 - 13
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java

@@ -2,9 +2,7 @@ package com.jme3.scene.plugins.gltf;
 
 import com.google.gson.*;
 import com.google.gson.stream.JsonReader;
-import com.jme3.animation.AnimControl;
-import com.jme3.animation.Animation;
-import com.jme3.animation.SpatialTrack;
+import com.jme3.animation.*;
 import com.jme3.asset.*;
 import com.jme3.material.Material;
 import com.jme3.material.RenderState;
@@ -17,7 +15,9 @@ 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.logging.Level;
 import java.util.logging.Logger;
@@ -45,6 +45,7 @@ public class GltfLoader implements AssetLoader {
     private JsonArray images;
     private JsonArray samplers;
     private JsonArray animations;
+    private JsonArray skins;
 
     private Material defaultMat;
     private AssetInfo info;
@@ -52,9 +53,12 @@ public class GltfLoader implements AssetLoader {
     private FloatArrayPopulator floatArrayPopulator = new FloatArrayPopulator();
     private Vector3fArrayPopulator vector3fArrayPopulator = new Vector3fArrayPopulator();
     private QuaternionArrayPopulator quaternionArrayPopulator = new QuaternionArrayPopulator();
+    private Matrix4fArrayPopulator matrix4fArrayPopulator = new Matrix4fArrayPopulator();
     private static Map<String, MaterialAdapter> defaultMaterialAdapters = new HashMap<>();
     private boolean useNormalsFlag = false;
 
+    Map<Skeleton, List<Spatial>> skinnedSpatials = new HashMap<>();
+
     static {
         defaultMaterialAdapters.put("pbrMetallicRoughness", new PBRMaterialAdapter());
     }
@@ -94,10 +98,16 @@ public class GltfLoader implements AssetLoader {
             images = root.getAsJsonArray("images");
             samplers = root.getAsJsonArray("samplers");
             animations = root.getAsJsonArray("animations");
+            skins = root.getAsJsonArray("skins");
+
+            readSkins();
 
             JsonPrimitive defaultScene = root.getAsJsonPrimitive("scene");
 
             Node n = loadScenes(defaultScene);
+
+            setupControls();
+
             //only one scene let's not return the root.
             if (n.getChildren().size() == 1) {
                 n = (Node) n.getChild(0);
@@ -131,7 +141,7 @@ public class GltfLoader implements AssetLoader {
             sceneNode.setName(getAsString(scene.getAsJsonObject(), "name"));
             JsonArray sceneNodes = scene.getAsJsonObject().getAsJsonArray("nodes");
             for (JsonElement node : sceneNodes) {
-                sceneNode.attachChild(loadNode(node.getAsInt()));
+                loadChild(sceneNode, node);
             }
             root.attachChild(sceneNode);
         }
@@ -152,13 +162,19 @@ public class GltfLoader implements AssetLoader {
         return root;
     }
 
-    private Spatial loadNode(int nodeIndex) throws IOException {
-        Spatial spatial = fetchFromCache("nodes", nodeIndex, Spatial.class);
-        if (spatial != null) {
-            //If a spatial is referenced several times, it may be attached to different parents,
-            // and it's not possible in JME, so we have to clone it.
-            return spatial.clone();
+    private Object loadNode(int nodeIndex) throws IOException {
+        Object obj = fetchFromCache("nodes", nodeIndex, Object.class);
+        if (obj != null) {
+            if (obj instanceof Bone) {
+                //the node can be a previously loaded bone let's return it
+                return obj;
+            } else {
+                //If a spatial is referenced several times, it may be attached to different parents,
+                // and it's not possible in JME, so we have to clone it.
+                return ((Spatial) obj).clone();
+            }
         }
+        Spatial spatial;
         JsonObject nodeData = nodes.get(nodeIndex).getAsJsonObject();
         JsonArray children = nodeData.getAsJsonArray("children");
         Integer meshIndex = getAsInteger(nodeData, "mesh");
@@ -189,9 +205,16 @@ public class GltfLoader implements AssetLoader {
             spatial = node;
         }
 
+        Integer skinIndex = getAsInteger(nodeData, "skin");
+        if (skinIndex != null) {
+            Skeleton skeleton = fetchFromCache("skins", skinIndex, Skeleton.class);
+            List<Spatial> spatials = skinnedSpatials.get(skeleton);
+            spatials.add(spatial);
+        }
+
         if (children != null) {
             for (JsonElement child : children) {
-                ((Node) spatial).attachChild(loadNode(child.getAsInt()));
+                loadChild(spatial, child);
             }
         }
 
@@ -204,6 +227,19 @@ public class GltfLoader implements AssetLoader {
         return spatial;
     }
 
+    private void loadChild(Spatial parent, JsonElement child) throws IOException {
+        int index = child.getAsInt();
+        Object loaded = loadNode(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);
+        }
+    }
+
     private Transform loadTransforms(JsonObject nodeData) {
         Transform transform = new Transform();
         JsonArray matrix = nodeData.getAsJsonArray("matrix");
@@ -276,6 +312,21 @@ public class GltfLoader implements AssetLoader {
             for (Map.Entry<String, JsonElement> entry : attributes.entrySet()) {
                 mesh.setBuffer(loadAccessorData(entry.getValue().getAsInt(), new VertexBufferPopulator(getVertexBufferType(entry.getKey()))));
             }
+
+            if (mesh.getBuffer(VertexBuffer.Type.BoneIndex) != null) {
+                //the mesh has some skinning let's create needed buffers for HW skinning
+                //creating empty buffers for HW skinning
+                //the buffers will be setup if ever used.
+                VertexBuffer weightsHW = new VertexBuffer(VertexBuffer.Type.HWBoneWeight);
+                VertexBuffer indicesHW = new VertexBuffer(VertexBuffer.Type.HWBoneIndex);
+                //setting usage to cpuOnly so that the buffer is not send empty to the GPU
+                indicesHW.setUsage(VertexBuffer.Usage.CpuOnly);
+                weightsHW.setUsage(VertexBuffer.Usage.CpuOnly);
+                mesh.setBuffer(weightsHW);
+                mesh.setBuffer(indicesHW);
+                mesh.generateBindPose();
+            }
+
             Geometry geom = new Geometry(null, mesh);
 
             Integer materialIndex = getAsInteger(meshObject, "material");
@@ -302,6 +353,7 @@ public class GltfLoader implements AssetLoader {
             geomArray[index] = geom;
             index++;
 
+            //TODO skins
             //TODO targets(morph anim...)
         }
 
@@ -318,12 +370,14 @@ public class GltfLoader implements AssetLoader {
         int byteOffset = getAsInteger(accessor, "byteOffset", 0);
         Integer componentType = getAsInteger(accessor, "componentType");
         assertNotNull(componentType, "No component type defined for accessor " + accessorIndex);
-        boolean normalized = getAsBoolean(accessor, "normalized", false);
         Integer count = getAsInteger(accessor, "count");
         assertNotNull(count, "No count attribute defined for accessor " + accessorIndex);
         String type = getAsString(accessor, "type");
         assertNotNull(type, "No type attribute defined for accessor " + accessorIndex);
 
+        boolean normalized = getAsBoolean(accessor, "normalized", false);
+        //Some float data can be packed into short buffers, "normalized" means they have to be unpacked.
+        //TODO support packed data
         //TODO min / max
         //TODO sparse
         //TODO extensions?
@@ -578,8 +632,10 @@ public class GltfLoader implements AssetLoader {
                 control.addAnim(anim);
 
             } else {
-                //At some pont we'll have bone animation
+                //At some point we'll have bone animation
                 //TODO support for bone animation.
+                System.err.println("animated");
+                System.err.println(node);
             }
         }
 
@@ -608,6 +664,100 @@ public class GltfLoader implements AssetLoader {
         texture.setWrap(Texture.WrapAxis.T, wrapT);
     }
 
+    private void readSkins() throws IOException {
+        if (skins == null) {
+            //no skins, no bone animation.
+            return;
+        }
+        for (int index = 0; index < skins.size(); index++) {
+            JsonObject skin = skins.get(index).getAsJsonObject();
+
+            //each skin is a skeleton.
+            Integer rootIndex = getAsInteger(skin, "skeleton");
+            JsonArray joints = skin.getAsJsonArray("joints");
+            assertNotNull(joints, "No joints defined for skin");
+            Integer matricesIndex = getAsInteger(skin, "inverseBindMatrices");
+            Matrix4f[] inverseBindMatrices = null;
+            if (matricesIndex != null) {
+                inverseBindMatrices = loadAccessorData(matricesIndex, matrix4fArrayPopulator);
+            } else {
+                inverseBindMatrices = new Matrix4f[joints.size()];
+                for (int i = 0; i < inverseBindMatrices.length; i++) {
+                    inverseBindMatrices[i] = new Matrix4f();
+                }
+            }
+
+            System.err.println(inverseBindMatrices);
+
+            rootIndex = joints.get(0).getAsInt();
+
+            Bone[] bones = new Bone[joints.size()];
+            for (int i = 0; i < joints.size(); i++) {
+                bones[i] = loadNodeAsBone(joints.get(i).getAsInt(), inverseBindMatrices[i]);
+            }
+            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);
+
+        }
+
+    }
+
+    private Bone loadNodeAsBone(int nodeIndex, Matrix4f inverseBindMatrix) throws IOException {
+
+        Bone bone = fetchFromCache("nodes", nodeIndex, Bone.class);
+        if (bone != null) {
+            return 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);
+        Transform boneTransforms = loadTransforms(nodeData);
+        Transform inverseBind = new Transform();
+        inverseBind.fromTransformMatrix(inverseBindMatrix);
+        //  boneTransforms.combineWithParent(inverseBind);
+        bone.setBindTransforms(boneTransforms.getTranslation(), boneTransforms.getRotation(), boneTransforms.getScale());
+
+
+        addToCache("nodes", nodeIndex, bone, nodes.size());
+        return bone;
+    }
+
+    private void findChildren(int nodeIndex) {
+        Bone bone = fetchFromCache("nodes", nodeIndex, Bone.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));
+            }
+        }
+    }
+
+    private void setupControls() {
+        for (Skeleton skeleton : skinnedSpatials.keySet()) {
+            List<Spatial> spatials = skinnedSpatials.get(skeleton);
+            Spatial spatial = null;
+            if (spatials.size() >= 1) {
+                spatial = findCommonAncestor(spatials);
+            } else {
+                spatial = spatials.get(0);
+            }
+            SkeletonControl control = new SkeletonControl(skeleton);
+            spatial.addControl(control);
+        }
+    }
+
     private String loadMeshName(int meshIndex) {
         JsonObject meshData = meshes.get(meshIndex).getAsJsonObject();
         return getAsString(meshData, "name");
@@ -737,5 +887,25 @@ public class GltfLoader implements AssetLoader {
             return data;
         }
     }
+
+    private class Matrix4fArrayPopulator implements Populator<Matrix4f[]> {
+
+        @Override
+        public Matrix4f[] populate(Integer bufferViewIndex, int componentType, String type, int count, int byteOffset) throws IOException {
+
+            int numComponents = getNumberOfComponents(type);
+            int dataSize = numComponents * count;
+            Matrix4f[] data = new Matrix4f[count];
+
+            if (bufferViewIndex == null) {
+                //no referenced buffer, specs says to pad the data with zeros.
+                padBuffer(data, dataSize);
+            } else {
+                readBuffer(bufferViewIndex, byteOffset, dataSize, data, numComponents);
+            }
+
+            return data;
+        }
+    }
 }
 

+ 84 - 0
jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java

@@ -3,15 +3,21 @@ package com.jme3.scene.plugins.gltf;
 import com.google.gson.*;
 import com.jme3.asset.AssetLoadException;
 import com.jme3.math.ColorRGBA;
+import com.jme3.math.Matrix4f;
 import com.jme3.math.Quaternion;
 import com.jme3.math.Vector3f;
 import com.jme3.scene.Mesh;
+import com.jme3.scene.Spatial;
 import com.jme3.scene.VertexBuffer;
 import com.jme3.texture.Texture;
 import com.jme3.util.LittleEndien;
 
 import java.io.*;
 import java.nio.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 /**
  * Created by Nehon on 07/08/2017.
@@ -210,6 +216,11 @@ public class GltfUtils {
             for (int i = 0; i < array.length; i++) {
                 array[i] = new Quaternion();
             }
+        } else if (store instanceof Matrix4f[]) {
+            Matrix4f[] array = (Matrix4f[]) store;
+            for (int i = 0; i < array.length; i++) {
+                array[i] = new Matrix4f();
+            }
         }
     }
 
@@ -240,6 +251,8 @@ public class GltfUtils {
             populateVector3fArray((Vector3f[]) store, stream, length, byteOffset, byteStride, numComponents);
         } else if (store instanceof Quaternion[]) {
             populateQuaternionArray((Quaternion[]) store, stream, length, byteOffset, byteStride, numComponents);
+        } else if (store instanceof Matrix4f[]) {
+            populateMatrix4fArray((Matrix4f[]) store, stream, length, byteOffset, byteStride, numComponents);
         }
     }
 
@@ -348,6 +361,38 @@ public class GltfUtils {
         }
     }
 
+    private static void populateMatrix4fArray(Matrix4f[] array, LittleEndien stream, int length, int byteOffset, int byteStride, int numComponents) throws IOException {
+        int index = byteOffset;
+        int componentSize = 4;
+        int end = length * componentSize + byteOffset;
+        stream.skipBytes(byteOffset);
+        int arrayIndex = 0;
+        while (index < end) {
+            array[arrayIndex] = new Matrix4f(
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat(),
+                    stream.readFloat()
+            );
+
+            arrayIndex++;
+
+            index += Math.max(componentSize * numComponents, byteStride);
+        }
+    }
+
     private static LittleEndien getStream(byte[] buffer) {
         return new LittleEndien(new DataInputStream(new ByteArrayInputStream(buffer)));
     }
@@ -408,6 +453,45 @@ public class GltfUtils {
         }
     }
 
+    public static Spatial findCommonAncestor(List<Spatial> spatials) {
+        Map<Spatial, List<Spatial>> flatParents = new HashMap<>();
+
+        for (Spatial spatial : spatials) {
+            List<Spatial> parents = new ArrayList<>();
+            Spatial parent = spatial.getParent();
+            while (parent != null) {
+                parents.add(0, parent);
+                parent = parent.getParent();
+            }
+            flatParents.put(spatial, parents);
+        }
+
+        int index = 0;
+        Spatial lastCommonParent = null;
+        Spatial parent = null;
+        while (true) {
+            for (Spatial spatial : flatParents.keySet()) {
+                List<Spatial> parents = flatParents.get(spatial);
+                if (index == parents.size()) {
+                    //we reached the end of a spatial hierarchy let's return;
+                    return lastCommonParent;
+                }
+                Spatial p = parents.get(index);
+                if (parent == null) {
+                    parent = p;
+                } else if (p != parent) {
+                    return lastCommonParent;
+                }
+            }
+            lastCommonParent = parent;
+            parent = null;
+            index++;
+        }
+
+
+
+    }
+
     public static void dumpMesh(Mesh m) {
         for (VertexBuffer vertexBuffer : m.getBufferList().getArray()) {
             System.err.println(vertexBuffer.getBufferType());