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

fix-456
Nehon 7 years ago committed by Rémy Bouquet
parent d7b2e08d95
commit ea6c406979
  1. 17
      jme3-core/src/main/java/com/jme3/math/Transform.java
  2. 36
      jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java
  3. 6
      jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java
  4. 241
      jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java
  5. 244
      jme3-plugins/src/gltf/java/com/jme3/scene/plugins/gltf/GltfLoader.java
  6. BIN
      jme3-testdata/src/main/resources/Scenes/defaultProbe.j3o

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

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

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

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

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

Loading…
Cancel
Save