diff --git a/jme3-core/src/main/java/com/jme3/math/Transform.java b/jme3-core/src/main/java/com/jme3/math/Transform.java index 2d4e1fb7d..5d8899892 100644 --- a/jme3-core/src/main/java/com/jme3/math/Transform.java +++ b/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; } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java index 62edf2098..b4f21ec54 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonBone.java +++ b/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 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() { diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java index 4aa8f1bd1..43778f5ce 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/SkeletonDebugAppState.java +++ b/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()); diff --git a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java b/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java index ff0424fb7..f5016a741 100644 --- a/jme3-examples/src/main/java/jme3test/model/TestGltfLoading.java +++ b/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 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 findControl(Spatial s, Class 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); + } + } + } } 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 9ce7489b0..fff0a8b1d 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 @@ -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 defaultMaterialAdapters = new HashMap<>(); private boolean useNormalsFlag = false; + private Transform tmpTransforms = new Transform(); - Map> skinnedSpatials = new HashMap<>(); + Map> 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 spatials = skinnedSpatials.get(skeleton); + SkinData skinData = fetchFromCache("skins", skinIndex, SkinData.class); + List 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 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 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()); - System.err.println(skeleton); + SkinData skinData = new SkinData(); + skinData.skeletonControl = new SkeletonControl(skeleton); + addToCache("skins", index, skinData, nodes.size()); + skinnedSpatials.put(skinData, new ArrayList()); } } - 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 spatials = skinnedSpatials.get(skeleton); - Spatial spatial = null; + for (SkinData skinData : skinnedSpatials.keySet()) { + List 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 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 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; } diff --git a/jme3-testdata/src/main/resources/Scenes/defaultProbe.j3o b/jme3-testdata/src/main/resources/Scenes/defaultProbe.j3o new file mode 100644 index 000000000..89a086d44 Binary files /dev/null and b/jme3-testdata/src/main/resources/Scenes/defaultProbe.j3o differ