diff --git a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java index 6a74a0c2c..e70a4c3ea 100644 --- a/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java +++ b/jme3-core/src/main/java/com/jme3/anim/AnimComposer.java @@ -4,8 +4,7 @@ import com.jme3.renderer.RenderManager; import com.jme3.renderer.ViewPort; import com.jme3.scene.control.AbstractControl; -import java.util.HashMap; -import java.util.Map; +import java.util.*; /** * Created by Nehon on 20/12/2017. @@ -65,6 +64,14 @@ public class AnimComposer extends AbstractControl { time = 0; } + public Collection getAnimClips() { + return Collections.unmodifiableCollection(animClipMap.values()); + } + + public Collection getAnimClipsNames() { + return Collections.unmodifiableCollection(animClipMap.keySet()); + } + @Override protected void controlUpdate(float tpf) { if (currentAnimClip != null) { diff --git a/jme3-core/src/main/java/com/jme3/anim/Armature.java b/jme3-core/src/main/java/com/jme3/anim/Armature.java index 4e86b8fee..0d37c35f0 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Armature.java +++ b/jme3-core/src/main/java/com/jme3/anim/Armature.java @@ -169,7 +169,7 @@ public class Armature implements JmeCloneable, Savable { } /** - * Saves the current Armature state as it's bind pose. + * Saves the current Armature state as its bind pose. */ public void setBindPose() { //make sure all bones are updated diff --git a/jme3-core/src/main/java/com/jme3/anim/Joint.java b/jme3-core/src/main/java/com/jme3/anim/Joint.java index c060ae86d..f85b95c38 100644 --- a/jme3-core/src/main/java/com/jme3/anim/Joint.java +++ b/jme3-core/src/main/java/com/jme3/anim/Joint.java @@ -34,11 +34,6 @@ public class Joint implements Savable, JmeCloneable { * Or relative to the model's origin for the root joint. */ private Transform localTransform = new Transform(); - /** - * The base transform of the joint in local space. - * Those transform are the joint's initial value. - */ - private Transform baseLocalTransform = new Transform(); /** * The transform of the joint in model space. Relative to the origin of the model. @@ -134,7 +129,6 @@ public class Joint implements Savable, JmeCloneable { //Note that the whole Armature must be updated before calling this method. getModelTransform().toTransformMatrix(inverseModelBindMatrix); inverseModelBindMatrix.invertLocal(); - baseLocalTransform.set(localTransform); } protected void resetToBindPose() { @@ -267,8 +261,6 @@ public class Joint implements Savable, JmeCloneable { this.children = cloner.clone(children); this.attachedNode = cloner.clone(attachedNode); this.targetGeometry = cloner.clone(targetGeometry); - - this.baseLocalTransform = cloner.clone(baseLocalTransform); this.localTransform = cloner.clone(localTransform); this.jointModelTransform = cloner.clone(jointModelTransform); this.inverseModelBindMatrix = cloner.clone(inverseModelBindMatrix); @@ -283,8 +275,6 @@ public class Joint implements Savable, JmeCloneable { name = input.readString("name", null); attachedNode = (Node) input.readSavable("attachedNode", null); targetGeometry = (Geometry) input.readSavable("targetGeometry", null); - baseLocalTransform = (Transform) input.readSavable("baseLocalTransforms", baseLocalTransform); - localTransform.set(baseLocalTransform); inverseModelBindMatrix = (Matrix4f) input.readSavable("inverseModelBindMatrix", inverseModelBindMatrix); jointModelTransform = (JointModelTransform) input.readSavable("jointModelTransform", null); @@ -301,7 +291,6 @@ public class Joint implements Savable, JmeCloneable { output.write(name, "name", null); output.write(attachedNode, "attachedNode", null); output.write(targetGeometry, "targetGeometry", null); - output.write(baseLocalTransform, "baseLocalTransform", new Transform()); output.write(inverseModelBindMatrix, "inverseModelBindMatrix", new Matrix4f()); output.writeSavableArrayList(children, "children", null); output.write(jointModelTransform, "jointModelTransform", null); diff --git a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java index f11a0c43c..ba049c3ae 100644 --- a/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java +++ b/jme3-core/src/main/java/com/jme3/anim/util/AnimMigrationUtils.java @@ -1,24 +1,189 @@ package com.jme3.anim.util; -import com.jme3.animation.AnimControl; -import com.jme3.scene.SceneGraphVisitor; -import com.jme3.scene.Spatial; +import com.jme3.anim.*; +import com.jme3.animation.*; +import com.jme3.math.Quaternion; +import com.jme3.math.Vector3f; +import com.jme3.scene.*; + +import java.util.*; public class AnimMigrationUtils { + private static AnimControlVisitor animControlVisitor = new AnimControlVisitor(); + private static SkeletonControlVisitor skeletonControlVisitor = new SkeletonControlVisitor(); + + public static Spatial migrate(Spatial source) { - //source.depthFirstTraversal(); + Map skeletonArmatureMap = new HashMap<>(); + animControlVisitor.setMappings(skeletonArmatureMap); + source.depthFirstTraversal(animControlVisitor); + skeletonControlVisitor.setMappings(skeletonArmatureMap); + source.depthFirstTraversal(skeletonControlVisitor); return source; } - private class AnimControlVisitor implements SceneGraphVisitor { + private static class AnimControlVisitor implements SceneGraphVisitor { + + Map skeletonArmatureMap; @Override public void visit(Spatial spatial) { AnimControl control = spatial.getControl(AnimControl.class); if (control != null) { + AnimComposer composer = new AnimComposer(); + Skeleton skeleton = control.getSkeleton(); + if (skeleton == null) { + //only bone anim for now + return; + } + + Joint[] joints = new Joint[skeleton.getBoneCount()]; + for (int i = 0; i < skeleton.getBoneCount(); i++) { + Bone b = skeleton.getBone(i); + Joint j = joints[i]; + if (j == null) { + j = fromBone(b); + joints[i] = j; + } + for (Bone bone : b.getChildren()) { + int index = skeleton.getBoneIndex(bone); + Joint joint = joints[index]; + if (joint == null) { + joint = fromBone(bone); + } + j.addChild(joint); + joints[index] = joint; + } + } + + Armature armature = new Armature(joints); + armature.setBindPose(); + skeletonArmatureMap.put(skeleton, armature); + + for (String animName : control.getAnimationNames()) { + Animation anim = control.getAnim(animName); + AnimClip clip = new AnimClip(animName); + Joint[] staticJoints = new Joint[joints.length]; + System.arraycopy(joints, 0, staticJoints, 0, joints.length); + for (Track track : anim.getTracks()) { + if (track instanceof BoneTrack) { + BoneTrack boneTrack = (BoneTrack) track; + int index = boneTrack.getTargetBoneIndex(); + Bone bone = skeleton.getBone(index); + Joint joint = joints[index]; + JointTrack jointTrack = fromBoneTrack(boneTrack, bone, joint); + clip.addTrack(jointTrack); + //this joint is animated let's remove it from the static joints + staticJoints[index] = null; + } + } + + for (int i = 0; i < staticJoints.length; i++) { + Joint j = staticJoints[i]; + if (j != null) { + // joint has no track , we create one with the default pose + float[] times = new float[]{0}; + Vector3f[] translations = new Vector3f[]{j.getLocalTranslation()}; + Quaternion[] rotations = new Quaternion[]{j.getLocalRotation()}; + Vector3f[] scales = new Vector3f[]{j.getLocalScale()}; + JointTrack track = new JointTrack(j, times, translations, rotations, scales); + clip.addTrack(track); + } + } + + composer.addAnimClip(clip); + } + spatial.removeControl(control); + spatial.addControl(composer); + } + } + + public void setMappings(Map skeletonArmatureMap) { + this.skeletonArmatureMap = skeletonArmatureMap; + } + } + + private static class SkeletonControlVisitor implements SceneGraphVisitor { + + Map skeletonArmatureMap; + + @Override + public void visit(Spatial spatial) { + SkeletonControl control = spatial.getControl(SkeletonControl.class); + if (control != null) { + Armature armature = skeletonArmatureMap.get(control.getSkeleton()); + SkinningControl skinningControl = new SkinningControl(armature); + Map> attachedSpatials = new HashMap<>(); + for (int i = 0; i < control.getSkeleton().getBoneCount(); i++) { + Bone b = control.getSkeleton().getBone(i); + Node n = control.getAttachmentsNode(b.getName()); + n.removeFromParent(); + if (!n.getChildren().isEmpty()) { + attachedSpatials.put(b.getName(), n.getChildren()); + } + } + spatial.removeControl(control); + spatial.addControl(skinningControl); + for (String name : attachedSpatials.keySet()) { + List spatials = attachedSpatials.get(name); + for (Spatial child : spatials) { + skinningControl.getAttachmentsNode(name).attachChild(child); + } + } } } + + public void setMappings(Map skeletonArmatureMap) { + this.skeletonArmatureMap = skeletonArmatureMap; + } } + + public static JointTrack fromBoneTrack(BoneTrack boneTrack, Bone bone, Joint joint) { + float[] times = new float[boneTrack.getTimes().length]; + int length = times.length; + System.arraycopy(boneTrack.getTimes(), 0, times, 0, length); + //translation + Vector3f[] translations = new Vector3f[length]; + if (boneTrack.getTranslations() != null) { + for (int i = 0; i < boneTrack.getTranslations().length; i++) { + Vector3f oldTrans = boneTrack.getTranslations()[i]; + Vector3f newTrans = new Vector3f(); + newTrans.set(bone.getBindPosition()).addLocal(oldTrans); + translations[i] = newTrans; + } + } + //rotation + Quaternion[] rotations = new Quaternion[length]; + if (boneTrack.getRotations() != null) { + for (int i = 0; i < boneTrack.getRotations().length; i++) { + Quaternion oldRot = boneTrack.getRotations()[i]; + Quaternion newRot = new Quaternion(); + newRot.set(bone.getBindRotation()).multLocal(oldRot); + rotations[i] = newRot; + } + } + //scale + Vector3f[] scales = new Vector3f[length]; + if (boneTrack.getScales() != null) { + for (int i = 0; i < boneTrack.getScales().length; i++) { + Vector3f oldScale = boneTrack.getScales()[i]; + Vector3f newScale = new Vector3f(); + newScale.set(bone.getBindScale()).multLocal(oldScale); + scales[i] = newScale; + } + } + + return new JointTrack(joint, times, translations, rotations, scales); + } + + private static Joint fromBone(Bone b) { + Joint j = new Joint(b.getName()); + j.setLocalTranslation(b.getBindPosition()); + j.setLocalRotation(b.getBindRotation()); + j.setLocalScale(b.getBindScale()); + return j; + } + } diff --git a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java index 439316a95..47b78c1c1 100644 --- a/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java +++ b/jme3-core/src/main/java/com/jme3/scene/debug/custom/ArmatureDebugAppState.java @@ -6,8 +6,7 @@ package com.jme3.scene.debug.custom; import com.jme3.anim.*; import com.jme3.app.Application; -import com.jme3.app.state.AbstractAppState; -import com.jme3.app.state.AppStateManager; +import com.jme3.app.state.BaseAppState; import com.jme3.collision.CollisionResults; import com.jme3.input.MouseInput; import com.jme3.input.controls.ActionListener; @@ -22,15 +21,17 @@ import java.util.*; /** * @author Nehon */ -public class ArmatureDebugAppState extends AbstractAppState { +public class ArmatureDebugAppState extends BaseAppState { private Node debugNode = new Node("debugNode"); private Map armatures = new HashMap<>(); private Map selectedBones = new HashMap<>(); private Application app; + ViewPort vp; + @Override - public void initialize(AppStateManager stateManager, Application app) { - ViewPort vp = app.getRenderManager().createMainView("debug", app.getCamera()); + protected void initialize(Application app) { + vp = app.getRenderManager().createMainView("debug", app.getCamera()); vp.attachScene(debugNode); vp.setClearDepth(true); this.app = app; @@ -39,12 +40,26 @@ public class ArmatureDebugAppState extends AbstractAppState { } app.getInputManager().addListener(actionListener, "shoot"); app.getInputManager().addMapping("shoot", new MouseButtonTrigger(MouseInput.BUTTON_LEFT), new MouseButtonTrigger(MouseInput.BUTTON_RIGHT)); - super.initialize(stateManager, app); - debugNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal())); debugNode.addLight(new DirectionalLight(new Vector3f(1f, 1f, 1f).normalizeLocal(), new ColorRGBA(0.5f, 0.5f, 0.5f, 1.0f))); + vp.setEnabled(false); + } + + @Override + protected void cleanup(Application app) { + + } + + @Override + protected void onEnable() { + vp.setEnabled(true); + } + + @Override + protected void onDisable() { + vp.setEnabled(false); } @Override @@ -53,13 +68,13 @@ public class ArmatureDebugAppState extends AbstractAppState { debugNode.updateGeometricState(); } - public ArmatureDebugger addArmature(SkinningControl skinningControl) { + public ArmatureDebugger addArmatureFrom(SkinningControl skinningControl) { Armature armature = skinningControl.getArmature(); Spatial forSpatial = skinningControl.getSpatial(); - return addArmature(armature, forSpatial); + return addArmatureFrom(armature, forSpatial); } - public ArmatureDebugger addArmature(Armature armature, Spatial forSpatial) { + public ArmatureDebugger addArmatureFrom(Armature armature, Spatial forSpatial) { ArmatureDebugger ad = new ArmatureDebugger(forSpatial.getName() + "_Armature", armature); ad.setLocalTransform(forSpatial.getWorldTransform()); diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java new file mode 100644 index 000000000..333f09cc2 --- /dev/null +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestAnimMigration.java @@ -0,0 +1,147 @@ +package jme3test.model.anim; + +import com.jme3.anim.AnimComposer; +import com.jme3.anim.SkinningControl; +import com.jme3.anim.util.AnimMigrationUtils; +import com.jme3.app.ChaseCameraAppState; +import com.jme3.app.SimpleApplication; +import com.jme3.input.KeyInput; +import com.jme3.input.controls.ActionListener; +import com.jme3.input.controls.KeyTrigger; +import com.jme3.light.AmbientLight; +import com.jme3.light.DirectionalLight; +import com.jme3.math.*; +import com.jme3.scene.Node; +import com.jme3.scene.Spatial; +import com.jme3.scene.debug.custom.ArmatureDebugAppState; + +import java.util.LinkedList; +import java.util.Queue; + +/** + * Created by Nehon on 18/12/2017. + */ +public class TestAnimMigration extends SimpleApplication { + + ArmatureDebugAppState debugAppState; + AnimComposer composer; + Queue anims = new LinkedList<>(); + boolean playAnim = true; + + public static void main(String... argv) { + TestAnimMigration app = new TestAnimMigration(); + app.start(); + } + + @Override + public void simpleInitApp() { + setTimer(new EraseTimer()); + //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); + viewPort.setBackgroundColor(ColorRGBA.DarkGray); + rootNode.addLight(new DirectionalLight(new Vector3f(-1, -1, -1).normalizeLocal())); + rootNode.addLight(new AmbientLight(ColorRGBA.DarkGray)); + + Spatial model = assetManager.loadModel("Models/Jaime/Jaime.j3o"); + //Spatial model = assetManager.loadModel("Models/Oto/Oto.mesh.xml"); + //Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); + //Spatial model = assetManager.loadModel("Models/Elephant/Elephant.mesh.xml"); + + AnimMigrationUtils.migrate(model); + + rootNode.attachChild(model); + + + debugAppState = new ArmatureDebugAppState(); + stateManager.attach(debugAppState); + + setupModel(model); + + flyCam.setEnabled(false); + + Node target = new Node("CamTarget"); + //target.setLocalTransform(model.getLocalTransform()); + target.move(0, 1, 0); + ChaseCameraAppState chaseCam = new ChaseCameraAppState(); + chaseCam.setTarget(target); + 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.setMinDistance(0.01f); + chaseCam.setZoomSpeed(0.01f); + chaseCam.setDefaultVerticalRotation(0.3f); + + initInputs(); + } + + public void initInputs() { + 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) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + System.err.println(anim); + } else { + composer.reset(); + } + } + } + }, "toggleAnim"); + inputManager.addMapping("nextAnim", new KeyTrigger(KeyInput.KEY_RIGHT)); + inputManager.addListener(new ActionListener() { + @Override + public void onAction(String name, boolean isPressed, float tpf) { + if (isPressed && composer != null) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + System.err.println(anim); + } + } + }, "nextAnim"); + } + + private void setupModel(Spatial model) { + if (composer != null) { + return; + } + composer = model.getControl(AnimComposer.class); + if (composer != null) { + + SkinningControl sc = model.getControl(SkinningControl.class); + debugAppState.addArmatureFrom(sc); + + anims.clear(); + for (String name : composer.getAnimClipsNames()) { + anims.add(name); + } + if (anims.isEmpty()) { + return; + } + if (playAnim) { + String anim = anims.poll(); + anims.add(anim); + composer.setCurrentAnimClip(anim); + System.err.println(anim); + } + + } else { + if (model instanceof Node) { + Node n = (Node) model; + for (Spatial child : n.getChildren()) { + setupModel(child); + } + } + } + + } +} diff --git a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java index ba0d96510..80ecb0132 100644 --- a/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java +++ b/jme3-examples/src/main/java/jme3test/model/anim/TestArmature.java @@ -6,7 +6,6 @@ import com.jme3.app.SimpleApplication; import com.jme3.input.KeyInput; import com.jme3.input.controls.ActionListener; import com.jme3.input.controls.KeyTrigger; -import com.jme3.light.DirectionalLight; import com.jme3.material.Material; import com.jme3.math.*; import com.jme3.scene.*; @@ -37,6 +36,7 @@ public class TestArmature extends SimpleApplication { //cam.setFrustumPerspective(90f, (float) cam.getWidth() / cam.getHeight(), 0.01f, 10f); viewPort.setBackgroundColor(ColorRGBA.DarkGray); + //create armature Joint root = new Joint("Root_Joint"); j1 = new Joint("Joint_1"); j2 = new Joint("Joint_2"); @@ -52,9 +52,9 @@ public class TestArmature extends SimpleApplication { final Armature armature = new Armature(joints); // armature.setModelTransformClass(SeparateJointModelTransform.class); - armature.setBindPose(); + //create animations AnimClip clip = new AnimClip("anim"); float[] times = new float[]{0, 2, 4}; Quaternion[] rotations = new Quaternion[]{ @@ -83,15 +83,18 @@ public class TestArmature extends SimpleApplication { clip.addTrack(track1); clip.addTrack(track2); + //create the animComposer control final AnimComposer composer = new AnimComposer(); composer.addAnimClip(clip); + //create the SkinningControl SkinningControl ac = new SkinningControl(armature); ac.setHardwareSkinningPreferred(false); Node node = new Node("Test Armature"); rootNode.attachChild(node); + //Create the mesh to deform. Geometry cylinder = new Geometry("cylinder", createMesh()); Material m = new Material(assetManager, "Common/MatDefs/Misc/fakeLighting.j3md"); m.setColor("Color", ColorRGBA.randomColor()); @@ -103,14 +106,9 @@ public class TestArmature extends SimpleApplication { composer.setCurrentAnimClip("anim"); ArmatureDebugAppState debugAppState = new ArmatureDebugAppState(); - debugAppState.addArmature(ac); + debugAppState.addArmatureFrom(ac); stateManager.attach(debugAppState); - rootNode.addLight(new DirectionalLight(new Vector3f(-1f, -1f, -1f).normalizeLocal())); - - rootNode.addLight(new DirectionalLight(new Vector3f(1f, 1f, 1f).normalizeLocal(), new ColorRGBA(0.7f, 0.7f, 0.7f, 1.0f))); - - flyCam.setEnabled(false); ChaseCameraAppState chaseCam = new ChaseCameraAppState(); @@ -132,12 +130,10 @@ public class TestArmature extends SimpleApplication { @Override public void onAction(String name, boolean isPressed, float tpf) { if (isPressed) { - play = false; composer.reset(); armature.resetToBindPose(); } else { - play = true; composer.setCurrentAnimClip("anim"); } } @@ -202,12 +198,10 @@ public class TestArmature extends SimpleApplication { c.updateCounts(); c.updateBound(); - //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); c.setBuffer(weightsHW); @@ -220,20 +214,4 @@ public class TestArmature extends SimpleApplication { } - float time = 0; - boolean play = true; - - @Override - public void simpleUpdate(float tpf) { - - -// if (play == false) { -// return; -// } -// time += tpf; -// float rot = FastMath.sin(time); -// j1.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); -// j2.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI * rot, Vector3f.UNIT_Z)); - - } }