GLTF: armature loading.

fix-456
Rémy Bouquet 8 years ago committed by Rémy Bouquet
parent b129d2954f
commit 6b3093aa3e
  1. 111
      jme3-core/src/main/java/com/jme3/scene/Mesh.java
  2. 196
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java
  3. 84
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfUtils.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);

@ -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;
}
}
}

@ -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());

Loading…
Cancel
Save