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 f115f8121..aa703393c 100644 --- a/jme3-core/src/main/java/com/jme3/scene/Mesh.java +++ b/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); diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java index db4204295..86d458ac3 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java +++ b/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 defaultMaterialAdapters = new HashMap<>(); private boolean useNormalsFlag = false; + Map> 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 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 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()); + + 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 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 { + + @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; + } + } } diff --git a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java b/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java index eb7032328..7b03f333b 100644 --- a/jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.java +++ b/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 spatials) { + Map> flatParents = new HashMap<>(); + + for (Spatial spatial : spatials) { + List 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 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());