Question about DynamicAnimControl Minie-4.0.2

hi @sgold, could you help me with a doubt that I can’t solve?
I’m experimenting with the Minie library. I created a model and added the DynamicAnimControl to it.
Here is the script (if I made some mistakes in the configuration please report them to me):

public class TestBoneRagdoll extends SimpleApplication extends ActionListener {

	private BulletAppState physics;
	private DynamicAnimControl ragdoll;
	
	...

	@Override
	public void simpleInitApp() {
        physics = new BulletAppState();
		stateManager.attach(physics);

		...

		setupCharacter();
	}
	
	private void setupCharacter() {
		Spatial model = assetManager.loadModel("Models/Dismember/zombie3.gltf");
		rootNode.attachChild(model);

		ragdoll = new DynamicAnimControl();
		setupRagdoll(ragdoll);
		model.addControl(ragdoll);
		physics.getPhysicsSpace().add(ragdoll);
		
		System.out.println(Arrays.toString(ragdoll.listLinkedBoneNames()));
	}
	
	private void setupRagdoll(DynamicAnimControl ragdoll) {
		final String prefix = "Armature_mixamorig:";
	    RangeOfMotion defaultRom = new RangeOfMotion(1f);
		
        ragdoll.link(prefix + "Spine1", 		1f, defaultRom);
		ragdoll.link(prefix + "Spine2", 		1f, defaultRom);
		ragdoll.link(prefix + "Neck", 			1f, defaultRom);
		ragdoll.link(prefix + "Head", 			1f, defaultRom);
		ragdoll.link(prefix + "RightShoulder", 	1f, defaultRom);
		ragdoll.link(prefix + "RightArm", 		1f, defaultRom);
		ragdoll.link(prefix + "RightForeArm", 	1f, defaultRom);
		ragdoll.link(prefix + "RightHand", 		1f, defaultRom);
		ragdoll.link(prefix + "LeftShoulder", 	1f, defaultRom);
		ragdoll.link(prefix + "LeftArm", 		1f, defaultRom);
		ragdoll.link(prefix + "LeftForeArm", 	1f, defaultRom);
		ragdoll.link(prefix + "LeftHand", 		1f, defaultRom);
		ragdoll.link(prefix + "RightUpLeg", 	1f, defaultRom);
		ragdoll.link(prefix + "RightLeg", 		1f, defaultRom);
		ragdoll.link(prefix + "RightFoot", 		1f, defaultRom);
		ragdoll.link(prefix + "LeftUpLeg", 		1f, defaultRom);
		ragdoll.link(prefix + "LeftLeg", 		1f, defaultRom);
		ragdoll.link(prefix + "LeftFoot",		1f, defaultRom);
	}
	
	@Override
	public void onAction(String name, boolean isPressed, float tpf) {
		if (name.equals("fire") && isPressed) {
			fire();
		}
	}
	
	private void fire() {
		float maxDistance = 200f;
		if (Physics.doRaycast(cam.getLocation(), cam.getDirection(), hitInfo, maxDistance)) {
			
			if (hitInfo.userObject instanceof BoneLink) {
				BoneLink bone = (BoneLink) hitInfo.userObject;
				System.out.println("BoneName=" + bone.boneName() + ", BoneLink=" + bone.name());
				
				//ragdoll.unlinkBone(bone.boneName()); //cause error
			}
		}
	}
	
	...
	
}

Question: Is it possible to remove a BoneLink and all its children during the game loop?

I tried with this instruction:

ragdoll.unlinkBone(bone.boneName());

but I get the following error message:

java.lang.IllegalStateException: Cannot unlink a bone while the Control is added to a Spatial.
	at com.jme3.bullet.animation.DacConfiguration.verifyNotAddedToSpatial(DacConfiguration.java:562)
	at com.jme3.bullet.animation.DacConfiguration.unlinkBone(DacConfiguration.java:348)
	at jme3test.bullet.TestBoneRagdoll.onAction(TestBoneRagdoll.java:300)
	at com.jme3.input.InputManager.invokeActions(InputManager.java:171)
	at com.jme3.input.InputManager.onKeyEventQueued(InputManager.java:471)
	at com.jme3.input.InputManager.processQueue(InputManager.java:865)
	at com.jme3.input.InputManager.update(InputManager.java:917)
	at com.jme3.app.LegacyApplication.update(LegacyApplication.java:724)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:246)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:153)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:193)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:234)
	at java.base/java.lang.Thread.run(Thread.java:834)

could you give me some suggestions please?

3 Likes

You could try removing the control, unlinking the bone(s), and then adding the control back. That should avoid the exception.

Or we could discuss your objective. What are you actually trying to do: simulate the sudden amputation of a limb? For that, I’d suggest a different approach.

1 Like

You immediately understood my goal. Could you explain me how to do it? :smiley:

1 Like

DynamicAnimControl has a method that causes a limb to shrivel up:
DynamicAnimControl.amputateSubtree(boneLink, blendSeconds)

You can see it in action if you run the TestDac demo application and press the DEL key.

It works by scaling the Joint transform, so the point of amputation has to be where a joint connects to another: shoulder, elbow, wrist, knee, ankle, etc. The next step would be “dropping” a copy of the amputated limb onto the ground.

2 Likes

thank you very much @sgold, I do some experiments and share the results. I’m taking this video as an example. I would like to expand the idea by adding limb amputation as well.

Hi @sgold, the new Minie library is really cool, has a lot of useful features and comes with great options for debugging.
Following your guide Project overview :: The Minie project I tried the DacWizard tool to generate the configuration code of my 3d model. The tool is great!

I modified the TestMode.java file by adding java.util.Locale.US to write the float variables to the file with a period instead of a comma.

Here is the change of methods describeAngle() and format():

    /**
     * Generate a textual description of a single-precision floating-point value
     * using at most 2 decimal places.
     *
     * @param fValue the value to describe
     * @return a description (not null, not empty)
     */
    private static String describeAngle(float fValue) {
        String raw = String.format(Locale.US, "%.2f", fValue);
        String result = MyString.trimFloat(raw);

        assert result != null;
        assert !result.isEmpty();
        return result;
    }

    /**
     * Format a LinkConfig as Java source code.
     *
     * @param config (not null, unaffected)
     */
    private static String format(LinkConfig config) {
        Vector3f scale = config.shapeScale(null);
        
        String scaleXString = MyString.trimFloat(String.format(Locale.US, "%.2f", scale.x));
        String scaleYString = MyString.trimFloat(String.format(Locale.US, "%.2f", scale.y));
        String scaleZString = MyString.trimFloat(String.format(Locale.US, "%.2f", scale.z));

        float massP = config.massParameter();
        String massPString = MyString.trimFloat(String.format(Locale.US, "%.2f", massP));

        String code = String.format(
                "new LinkConfig(%sf, MassHeuristic.%s,%n"
                + "                ShapeHeuristic.%s, "
                + "new Vector3f(%sf, %sf, %sf),%n"
                + "                CenterHeuristic.%s)",
                massPString, config.massHeuristic(),
                config.shapeHeuristic(),
                scaleXString, scaleYString, scaleZString,
                config.centerHeuristic());

        return code;
    }

Compilation errors before and after:

Here is the generated file:

package com.test.engine.ragdoll;

import com.jme3.bullet.animation.CenterHeuristic;
import com.jme3.bullet.animation.DynamicAnimControl;
import com.jme3.bullet.animation.LinkConfig;
import com.jme3.bullet.animation.MassHeuristic;
import com.jme3.bullet.animation.RangeOfMotion;
import com.jme3.bullet.animation.ShapeHeuristic;
import com.jme3.math.Vector3f;

public class YBotControl extends DynamicAnimControl {

    public YBotControl() {
        super();
        LinkConfig config1 = new LinkConfig(1f, MassHeuristic.Density,
                ShapeHeuristic.VertexHull, new Vector3f(1f, 1f, 1f),
                CenterHeuristic.Mean);
        
        super.setConfig("", config1);
        
        super.link("Armature_mixamorig:Spine1", config1,
                new RangeOfMotion(0.52f, -0.58f, 0.52f, -0.42f, 0.52f, -0.23f));
        super.link("Armature_mixamorig:RightHand", config1,
                new RangeOfMotion(0.26f, -0.66f, 0.52f, -1.06f, 0.79f, -0.66f));
        super.link("Armature_mixamorig:Spine2", config1,
                new RangeOfMotion(0.00f, -0.31f, 0.00f, -0.12f, 0.26f, -0.07f));
        super.link("Armature_mixamorig:LeftShoulder", config1,
                new RangeOfMotion(0.52f, -0.28f, 0.00f, -0.10f, 0.26f, -0.65f));
        super.link("Armature_mixamorig:RightFoot", config1,
                new RangeOfMotion(0.26f, -0.98f, 0.26f, -0.72f, 0.00f, -0.73f));
        super.link("Armature_mixamorig:LeftArm", config1,
                new RangeOfMotion(1.31f, -0.75f, 1.05f, -0.49f, 0.52f, -1.34f));
        super.link("Armature_mixamorig:RightArm", config1,
                new RangeOfMotion(0.79f, -1.45f, 0.52f, -1.45f, 0.00f, -1.17f));
        super.link("Armature_mixamorig:RightForeArm", config1,
                new RangeOfMotion(0.00f, -2.06f, 1.05f, -1.34f, 0.52f, -1.24f));
        super.link("Armature_mixamorig:LeftLeg", config1,
                new RangeOfMotion(0.00f, -2.43f, 0.52f, -0.38f, 1.05f, -0.44f));
        super.link("Armature_mixamorig:LeftForeArm", config1,
                new RangeOfMotion(2.62f, 0.00f, 0.79f, -1.48f, 2.36f, -0.99f));
        super.link("Armature_mixamorig:LeftUpLeg", config1,
                new RangeOfMotion(1.57f, -0.35f, 1.05f, -0.96f, 0.79f, -0.21f));
        super.link("Armature_mixamorig:RightShoulder", config1,
                new RangeOfMotion(0.79f, -0.49f, 0.26f, -0.52f, 0.52f, -0.54f));
        super.link("Armature_mixamorig:Neck", config1,
                new RangeOfMotion(0.26f, -0.17f, 0.00f, -0.21f, 0.26f, -0.17f));
        super.link("Armature_mixamorig:RightLeg", config1,
                new RangeOfMotion(0.00f, -2.09f, 1.05f, -0.72f, 0.52f, -1.17f));
        super.link("Armature_mixamorig:Head", config1,
                new RangeOfMotion(1.05f, -0.59f, 0.79f, -0.87f, 0.79f, -0.61f));
        super.link("Armature_mixamorig:LeftFoot", config1,
                new RangeOfMotion(0.79f, -0.84f, 0.52f, -0.42f, 0.26f, -0.59f));
        super.link("Armature_mixamorig:LeftHand", config1,
                new RangeOfMotion(0.79f, -0.49f, 1.31f, -0.31f, 1.05f, -0.65f));
        super.link("Armature_mixamorig:RightUpLeg", config1,
                new RangeOfMotion(1.57f, -0.70f, 0.26f, -0.66f, 0.26f, -0.51f));
    }
}

Now the model configuration has become the following:

	private Spatial model;
	private AnimComposer composer;
	private DynamicAnimControl dac;
	private final Vector3f defaultGravity = new Vector3f(0, -10, 0);
    private BulletAppState physics;

	private void setupRagdoll() {
		model = assetManager.loadModel("Models/Dismember/zombie3.j3o");
		model.scale(5.5f);
		rootNode.attachChild(model);
		
		composer = AnimUtils.findAnimComposer(model);
		Spatial animRoot = animComposer.getSpatial();

		dac = new YBotControl();
		animRoot.addControl(dac);
		physics.getPhysicsSpace().add(dac);
        dac.setGravity(defaultGravity);
	}

I did some experiments in ragdoll and kinematic modes. Here are the results.

  1. Test Kinematic mode:
	private RaycastHit hitInfo = new RaycastHit();
	private float maxDistance = 200f;
	
	private void fire() {
		if (Physics.doRaycast(cam.getLocation(), cam.getDirection(), hitInfo, maxDistance)) {
			
			if (hitInfo.userObject instanceof BoneLink) {
            	BoneLink bone = (BoneLink) hitInfo.userObject;
            	System.out.println("BoneName=" + bone.boneName() + ", BoneLink=" + bone.name());
				
				if (bone.boneName().endsWith("Arm") 
						|| bone.boneName().endsWith("Leg")
						|| bone.boneName().endsWith("Head")) {

						Vector3f position 	= bone.getRigidBody().getPhysicsLocation();
						Quaternion rotation = bone.getRigidBody().getPhysicsRotation();
						Vector3f impulse 	= hitInfo.normal.negate().multLocal(2);
						
						for (int i = 0; i < 15; i++) {
							Spatial limb = dropLimb(position, rotation, rootNode);
							limb.getControl(RigidBodyControl.class).applyImpulse(impulse, hitInfo.point);
						}

					dac.amputateSubtree(bone, 0.1f);

					if (bone.boneName().endsWith("Head") || bone.boneName().endsWith("Leg")) {
						composer.setCurrentAction(AnimDefs.Dying);
					}
				}
			}
		}
	}
	
	private Spatial dropLimb(Vector3f position, Quaternion rotation, Node parent) {
		Node bodyPart = new Node("BodyPart");
		Box mesh = new Box(0.1f, 0.1f, 0.1f);
		Geometry geo = new Geometry("Limb", mesh);
		
		Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
		mat.setColor("Color", ColorRGBA.Red.clone());
		geo.setMaterial(mat);
		bodyPart.attachChild(geo);
		bodyPart.setLocalTranslation(position);
		bodyPart.setLocalRotation(rotation);

		float mass = 5f;
		CollisionShape shape = CollisionShapeFactory.createBoxShape(bodyPart);
		RigidBodyControl rbc = new RigidBodyControl(shape, mass);
		rbc.setKinematic(false);
		rbc.setFriction(10);
		bodyPart.addControl(rbc);
		parent.attachChild(bodyPart);
		physicsSpace.add(bodyPart);
		rbc.setGravity(defaultGravity);

		return bodyPart;
	}
  1. Test Ragdoll Mode part 1:
	private RaycastHit hitInfo = new RaycastHit();
	private float maxDistance = 200f;
	
	private void fire() {
		if (Physics.doRaycast(cam.getLocation(), cam.getDirection(), hitInfo, maxDistance)) {
			
			if (hitInfo.userObject instanceof BoneLink) {
            	BoneLink bone = (BoneLink) hitInfo.userObject;
            	System.out.println("BoneName=" + bone.boneName() + ", BoneLink=" + bone.name());

				if (bone.boneName().endsWith("Arm") 
						|| bone.boneName().endsWith("Leg")
						|| bone.boneName().endsWith("Head")) {
					
						Vector3f position 	= bone.getRigidBody().getPhysicsLocation();
						Quaternion rotation = bone.getRigidBody().getPhysicsRotation();
						Vector3f impulse 	= hitInfo.normal.negate().multLocal(2);
						
						for (int i = 0; i < 15; i++) {
							Spatial limb = dropLimb(position, rotation, rootNode);
							limb.getControl(RigidBodyControl.class).applyImpulse(impulse, hitInfo.point);
						}
					
					if (bone.boneName().endsWith("Head") || bone.boneName().endsWith("Leg")) {
						dac.setRagdollMode();
					}
					
					dac.amputateSubtree(bone, 0.1f);
				}
			}
		}
	}
  1. Test Ragdoll Mode part 2: In the test where I remove the legs and then switch to Ragdoll mode, the 3d model keeps moving in circle. Is there any way to avoid it?

if you have any advice to give me let me know.

I’d be curious to know if anyone else has tried using DynamicAnimControl in a fun way to share ideas about it. Greetings to all users of the community :wink:

7 Likes

Sweet! It’s nice to see someone having fun with Minie.

Thanks for reporting the issues with unspecified locales in String.format(). Based on your report, I’ve committed fixes to MInie’s “master” branch, which will appear in the 4.0.0 release. Any day now…

The circular motion of the ragdoll might be caused by insufficient friction with the floor. You can increase the friction of a RigidBody using the setFriction() method. You can increase the friction of a DynamicAnimControl by looping over its rigid bodies using the listRigidBodies() method.

Edit: another possibility would be to increase damping. The key difference is that damping applies to all moving bodies, while friction only applies to bodies in contact with other bodies.

2 Likes

I am happy to contribute a little :grin:
Okay, I’ll do some tests. thank you

1 Like

hi @sgold, I am continuing with the experiments but I cannot solve a seemingly simple problem.
The scenario is the following: I have an AbstractControl that adds an NPC to the game arena every 6 seconds.
Since adding the code for configuring the DynamicAnimControl, the program takes longer to process, and the game freezes for a few seconds.
Could you give me some ideas to speed up this operation?

public class Spawner extends AbstractControl {

	int maxObjects  = 6;
    float spawnTime = 6f;
    float timer = 0;
	
	...

	@Override
    protected void controlUpdate(float tpf) {
        timer += tpf;
        if (timer > spawnTime) {
            timer = 0;
            
            int nChild = ((Node) spatial).getQuantity();
            
            if (nChild < maxObjects) {
                Vector3f spawnPoint = getRandomSpawnPoint();
                instantiate(spawnPoint, Quaternion.IDENTITY, (Node) spatial);
            }
        }
    }
	
	private void instantiate(Vector3f position, Quaternion rotation, Node parent) {

		Node enemy = assetManager.loadModel("Models/Dismember/zombie.j3o");
		enemy.setLocalTranslation(position);

		// setup Dac
		DynamicAnimControl dac = new YBotControl();
		enemy.addControl(dac);
        dac.setGravity(gravityVector);
        dac.setDamping(0.8f);
		
		for (PhysicsRigidBody body : dac.listRigidBodies()) {
			body.setFriction(10);
			body.setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_03);
		}
		
		PhysicsSpace.getPhysicsSpace().add(dac);
		
		// setup Physics Character
		float radius = 0.8f;
		float height = 4f;
		float mass = 5;
		BetterCharacterControl bcc = new BetterCharacterControl(radius, height, mass);
		bcc.getRigidBody().setCollisionGroup(PhysicsCollisionObject.COLLISION_GROUP_02);
		for (PhysicsRigidBody rb : dac.listRigidBodies()) {
			bcc.getRigidBody().addToIgnoreList(rb);
			rb.addToIgnoreList(bcc.getRigidBody());
		}
		enemy.addControl(bcc);
		PhysicsSpace.getPhysicsSpace().add(bcc);
		
		parent.attachChild(enemy);
	}
	
}
1 Like

If I recall correctly, the expensive part of initializing DAC is generating the collision shapes for meshes. If you’re spawning NPC models whose meshes are stored in J3O assets, I suggest storing the shapes in J3O assets as well. In other words, add DAC to the model before the game starts.

You can write your own asset processor to do this. Or you can use the “Save J3O” button in the Test screen of DacWizard.

2 Likes

I saved the model in j3o format with the DacWizard tool, but when I try to reload it via assetManager.loadModel(“zombie3-002648.j3o”) the program crashes without errors and I have to kill the task.

crash

I tried to convert the model to j3o format by writing the following code, but the program crashes.

private float cgmHeight = 4f;
private Node model;
private DynamicAnimControl dac;

@Override
public void simpleInitApp() {

	...
    setupCharacter();
}

private void setupCharacter() {
    model = (Node) assetManager.loadModel("Models/Dismember/zombie-t1.gltf");
		
	/**
	 * Scale the copy uniformly to the desired height, assuming Y-up
	 * orientation.
	 */
	Vector3f[] minMax = MySpatial.findMinMaxCoords(model);
	Vector3f min = minMax[0];
	Vector3f max = minMax[1];
	float oldHeight = max.y - min.y;
	if (oldHeight > 0f) {
		model.scale(cgmHeight / oldHeight);
	}
	
	rootNode.attachChild(model);
	
	Spatial animRoot = AnimUtils.getAnimControl(model).getSpatial();
	dac = new YBotControl();
	animRoot.addControl(dac);
	physicsSpace.add(dac);
}

private boolean save = true;

@Override
public void simpleUpdate(float tpf) {
	if (ragdoll.isReady() && save) {
		save = false;
		
		Spatial sp = (Spatial) Heart.deepCopy(model);
		Spatial animRoot = AnimUtils.getAnimControl(sp).getSpatial();
		DynamicAnimControl clone = (DynamicAnimControl) Heart.deepCopy(dac);
		animRoot.addControl(clone);
		
		convertModelToJ3o(sp, "zombie-t1");
	}
}

private void convertModelToJ3o(Spatial model, String assetName) {
	String dirName = System.getProperty("user.dir") + "/assets/Models/Dismember";
	String fileName = assetName + ".j3o";
	
	File file = new File(dirName, fileName);
	System.out.println("Converting file: " + file.getAbsolutePath());
	
	try {
		BinaryExporter.getInstance().save(model, file);
	} catch (IOException e) {
		e.printStackTrace();
	}
}

Could you tell me where I’m wrong or write me a sample code snippet?

1 Like

I’m not sure what went wrong. There’s no stack trace and no JVM crash log? Even with assertions enabled and a debug Minie library?

One slight problem I see with the code snippet you posted is that you add the DAC to a PhysicsSpace before writing to J3O. It would be better to write it without adding it to any space.

1 Like

If you run your application from the command line then you should be to hit Ctrl-Break to get it to dump the stack trace of all threads. (At least on Windows… note: Break not C)

1 Like

I wrote a test class. The conversion of the 3d model in j3o format with the addition of the DynamicAnimControl works correctly, but the loading fails. Here is the stack:

public class TestSaveLoadDac extends SimpleApplication {

	/**
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		TestSaveLoadDac app = new TestSaveLoadDac();
        app.setShowSettings(false);
		app.setPauseOnLostFocus(false);
		app.start();
	}

	@Override
	public void simpleInitApp() {
		saveModel();
		
		System.out.println("Loading model...");
		
		Spatial model = assetManager.loadModel("Models/Dismember/Sinbad.j3o");
		rootNode.attachChild(model);
		
		System.out.println("Done");
	}
	
	private void saveModel() {
		Spatial model = assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml");

		Spatial animRoot = model.getControl(AnimComposer.class).getSpatial();
		DynamicAnimControl dac = new DynamicAnimControl();
		setupSinbad(dac);
		animRoot.addControl(dac);
		
		convertModelToJ3o(model, "Sinbad");
	}
	
	private void setupSinbad(DynamicAnimControl dac) {
		dac.link("Waist", 		1f, new RangeOfMotion(1f, -0.4f, 0.8f, -0.8f, 0.4f, -0.4f));
		dac.link("Chest", 		1f, new RangeOfMotion(0.4f, 0f, 0.4f));
		dac.link("Neck", 		1f, new RangeOfMotion(0.5f, 1f, 0.7f));

		dac.link("Clavicle.R", 	1f, new RangeOfMotion(0.3f, -0.6f, 0f, 0f, 0.4f, -0.4f));
		dac.link("Humerus.R", 	1f, new RangeOfMotion(1.6f, -0.8f, 1f, -1f, 1.6f, -1f));
		dac.link("Ulna.R", 		1f, new RangeOfMotion(0f, 0f, 1f, -1f, 0f, -2f));
		dac.link("Hand.R", 		1f, new RangeOfMotion(0.8f, 0f, 0.2f));

		dac.link("Clavicle.L", 	1f, new RangeOfMotion(0.6f, -0.3f, 0f, 0f, 0.4f, -0.4f));
		dac.link("Humerus.L", 	1f, new RangeOfMotion(0.8f, -1.6f, 1f, -1f, 1f, -1.6f));
		dac.link("Ulna.L", 		1f, new RangeOfMotion(0f, 0f, 1f, -1f, 2f, 0f));
		dac.link("Hand.L", 		1f, new RangeOfMotion(0.8f, 0f, 0.2f));

		dac.link("Thigh.R", 	1f, new RangeOfMotion(0.4f, -1f, 0.4f, -0.4f, 1f, -0.5f));
		dac.link("Calf.R", 		1f, new RangeOfMotion(2f, 0f, 0f, 0f, 0f, 0f));
		dac.link("Foot.R", 		1f, new RangeOfMotion(0.3f, 0.5f, 0f));

		dac.link("Thigh.L", 	1f, new RangeOfMotion(0.4f, -1f, 0.4f, -0.4f, 0.5f, -1f));
		dac.link("Calf.L", 		1f, new RangeOfMotion(2f, 0f, 0f, 0f, 0f, 0f));
		dac.link("Foot.L", 		1f, new RangeOfMotion(0.3f, 0.5f, 0f));
	}
	
	private void convertModelToJ3o(Spatial model, String assetName) {
		String dirName = System.getProperty("user.dir") + "/assets/Models/Dismember";
		String fileName = assetName + ".j3o";
		File file = new File(dirName, fileName);
		
		System.out.println("Converting file: " + file.getAbsolutePath());
		
		try {
			BinaryExporter.getInstance().save(model, file);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		System.out.println("Done");
	}
}
Converting file: <my_local_path>
Done
Loading model...
SEVERE: Uncaught exception thrown in Thread[jME3 Main,5,main]
java.lang.StackOverflowError
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at com.jme3.util.clone.Cloner.javaClone(Cloner.java:375)
	at com.jme3.util.clone.ListCloneFunction.cloneObject(ListCloneFunction.java:47)
	at com.jme3.util.clone.ListCloneFunction.cloneObject(ListCloneFunction.java:43)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:221)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.anim.Joint.cloneFields(Joint.java:307)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:66)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:43)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:228)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.anim.Joint.cloneFields(Joint.java:307)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:66)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:43)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:228)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.anim.Joint.cloneFields(Joint.java:307)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:66)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:43)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:228)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.anim.Joint.cloneFields(Joint.java:307)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:66)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:43)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:228)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.anim.Joint.cloneFields(Joint.java:307)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:66)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:43)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:228)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.anim.Joint.cloneFields(Joint.java:307)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:66)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:43)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:228)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.anim.Joint.cloneFields(Joint.java:307)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:66)
	at com.jme3.util.clone.ListCloneFunction.cloneFields(ListCloneFunction.java:43)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:228)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.anim.Joint.cloneFields(Joint.java:307)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.bullet.animation.PhysicsLink.cloneFields(PhysicsLink.java:664)
	at com.jme3.bullet.animation.BoneLink.cloneFields(BoneLink.java:472)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.bullet.collision.PhysicsCollisionObject.cloneFields(PhysicsCollisionObject.java:1375)
	at com.jme3.bullet.objects.PhysicsRigidBody.cloneFields(PhysicsRigidBody.java:1029)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.bullet.collision.PhysicsCollisionObject.cloneIgnoreList(PhysicsCollisionObject.java:1216)
	at com.jme3.bullet.objects.PhysicsRigidBody.cloneFields(PhysicsRigidBody.java:1033)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.bullet.collision.PhysicsCollisionObject.cloneIgnoreList(PhysicsCollisionObject.java:1216)
	at com.jme3.bullet.objects.PhysicsRigidBody.cloneFields(PhysicsRigidBody.java:1033)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.bullet.collision.PhysicsCollisionObject.cloneIgnoreList(PhysicsCollisionObject.java:1216)
	at com.jme3.bullet.objects.PhysicsRigidBody.cloneFields(PhysicsRigidBody.java:1033)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	at com.jme3.bullet.collision.PhysicsCollisionObject.cloneIgnoreList(PhysicsCollisionObject.java:1216)
	at com.jme3.bullet.objects.PhysicsRigidBody.cloneFields(PhysicsRigidBody.java:1033)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:255)
	at com.jme3.util.clone.Cloner.clone(Cloner.java:160)
	...
1 Like

I’ll look into this ASAP.

Edit: load/save of rigid bodies with ignore lists is broken. Apparently has been for a long time. Still investigating…

Edit^2: I think I’ve found the issue and a fix for it. Expect a new release of MInie (with the fix) in a day or 2.

Edit^3: Minie v4.0.1 has been released with the fix.

3 Likes

Thank you very much for the quick resolution of the problem. I go on with the experiments :wink:

1 Like

And thank you very much for providing a test class.

1 Like

hi @sgold, i did some tests and i don’t understand why the collision shapes are not positioned correctly after loading a model previously saved in j3o format with the DynamicAnimControl. Am I doing something wrong or maybe there is still some problem?

Using Sinbad as a model the situation is even stranger. The model is crumpled on itself and also in this case the collision shapes seem centered in the origin of the model.

Here is the test case:

public class TestSaveLoadDac extends SimpleApplication {

	/**
	 * 
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TestSaveLoadDac app = new TestSaveLoadDac();
		AppSettings settings = new AppSettings(true);
        settings.setResolution(800, 600);
        settings.setFrameRate(60);
        settings.setSamples(4); // anti-aliasing
        settings.setVSync(true);
        settings.setGammaCorrection(true);
        app.setSettings(settings);
        app.setShowSettings(false);
		app.setPauseOnLostFocus(false);
		app.start();
	}
	
//	private String MODEL = "Models/Dismember/zombie-t1.gltf";
//	private String assetPath = "Models/Dismember";
//	private String modelName = "zombie-t1.j3o";
	
	private String MODEL = "Models/Sinbad/Sinbad.mesh.xml";
	private String assetPath = "Models/Dismember";
	private String modelName = "Sinbad.j3o";

	private BulletAppState physics;

	@Override
	public void simpleInitApp() {
		// TODO Auto-generated method stub
		flyCam.setMoveSpeed(25f);
		MyCamera.setNearFar(cam, 0.1f, 250f);
		
		ColorRGBA bgColor = new ColorRGBA(0.2f, 0.2f, 0.2f, 1f);
        viewPort.setBackgroundColor(bgColor);
        
        addLighting();
        initPhysics();
		saveModel();
		reloadModel();
	}
	
    /**
     * Add lighting and background color to the scene.
     */
    private void addLighting() {
        ColorRGBA ambientColor = new ColorRGBA(1f, 1f, 1f, 1f);
        AmbientLight ambient = new AmbientLight(ambientColor);
        rootNode.addLight(ambient);
        ambient.setName("ambient");

        Vector3f direction = new Vector3f(1f, -2f, -1f).normalizeLocal();
        DirectionalLight sun = new DirectionalLight(direction);
        rootNode.addLight(sun);
        sun.setName("sun");
    }
    
    private void initPhysics() {
    	physics = new BulletAppState();
		stateManager.attach(physics);
		physics.setDebugAxisLength(1);
		physics.setDebugEnabled(true);
    }
    
    /**
     * Edit Code
     */
    private void reloadModel() {
		Spatial model = assetManager.loadModel(assetPath + "/" + modelName);
		dumpScene(model);
		rootNode.attachChild(model);
		
		SkinningControl skControl = findControl(model, SkinningControl.class);
		skControl.getArmature().applyBindPose();
		
		AnimComposer animComposer = findControl(model, AnimComposer.class);
		System.out.println("--Animations: " + animComposer.getAnimClipsNames());
		animComposer.setCurrentAction("Dance");
		
		DynamicAnimControl dac = findControl(model, DynamicAnimControl.class);
		physics.getPhysicsSpace().add(dac);
    }
	
    /**
     * Edit Code
     */
	private void saveModel() {
		Spatial sp = assetManager.loadModel(MODEL);
		dumpScene(sp);
		
		SkinningControl skControl = findControl(sp, SkinningControl.class);
		skControl.getArmature().saveInitialPose();
		
		Spatial animRoot = skControl.getSpatial();
		DynamicAnimControl dac = new SinbadControl();
		animRoot.addControl(dac);
		
		convertModelToJ3o(sp, modelName);
	}
	
	private void convertModelToJ3o(Spatial model, String assetName) {
		String dirName = System.getProperty("user.dir") + "/assets/" + assetPath;
		String fileName = assetName;
		File file = new File(dirName, fileName);
		System.out.println("Converting file: " + file.getAbsolutePath());
		
		try {
			BinaryExporter.getInstance().save(model, file);
		} catch (IOException e) {
			e.printStackTrace();
		}
		
		System.out.println("Done");
	}
	
	private <T extends Control> T findControl(Spatial spatial, final Class<T> clazz) {
        T control = spatial.getControl(clazz);
        if (control != null) {
            return control;
        }

        if (spatial instanceof Node) {
            for (Spatial child : ((Node) spatial).getChildren()) {
                control = findControl(child, clazz);
                if (control != null) {
                    return control;
                }
            }
        }

        return null;
    }
	
	private void dumpScene(Spatial sp) {
		dumpScene(sp, 0);
	}

	private void dumpScene(Spatial sp, int level) {
		String tabs = "";
		for (int i = 0; i < level; i++) {
			tabs += '\t';
		}
		
		System.out.println(tabs + sp);
		System.out.println(tabs + " - Position: " + sp.getWorldTranslation());
		System.out.println(tabs + " - Rotation: " + sp.getWorldRotation());
		System.out.println(tabs + " - Scale: " + sp.getWorldScale());
		
		if (sp instanceof Geometry)
			System.out.println(tabs + " - " + ((Geometry) sp).getMaterial());
		
		for (Light light : sp.getLocalLightList()) {
			System.out.println(tabs + light.getName() + " - " + light);
		}
		for (int i = 0; i < sp.getNumControls(); i++) {
			System.out.println(tabs + sp.getControl(i));
		}
		if (sp instanceof Node) {
			for (Spatial child : ((Node) sp).getChildren()) {
				dumpScene(child, level + 1);
			}
		}
	}

}

DumpScene for Sinbad after reload:

Sinbad-ogremesh (Node)
 - Position: (0.0, 0.0, 0.0)
 - Rotation: (0.0, 0.0, 0.0, 1.0)
 - Scale: (1.0, 1.0, 1.0)
[email protected]
[email protected]
[email protected]
	Sinbad-geom-1 (Geometry)
	 - Position: (0.0, 0.0, 0.0)
	 - Rotation: (0.0, 0.0, 0.0, 1.0)
	 - Scale: (1.0, 1.0, 1.0)
	 - Material[name=Sinbad/Eyes, def=Phong Lighting, tech=null]
	Sinbad-geom-2 (Geometry)
	 - Position: (0.0, 0.0, 0.0)
	 - Rotation: (0.0, 0.0, 0.0, 1.0)
	 - Scale: (1.0, 1.0, 1.0)
	 - Material[name=Sinbad/Body, def=Phong Lighting, tech=null]
	Sinbad-geom-3 (Geometry)
	 - Position: (0.0, 0.0, 0.0)
	 - Rotation: (0.0, 0.0, 0.0, 1.0)
	 - Scale: (1.0, 1.0, 1.0)
	 - Material[name=Sinbad/Gold, def=Phong Lighting, tech=null]
	Sinbad-geom-4 (Geometry)
	 - Position: (0.0, 0.0, 0.0)
	 - Rotation: (0.0, 0.0, 0.0, 1.0)
	 - Scale: (1.0, 1.0, 1.0)
	 - Material[name=Sinbad/Teeth, def=Phong Lighting, tech=null]
	Sinbad-geom-5 (Geometry)
	 - Position: (0.0, 0.0, 0.0)
	 - Rotation: (0.0, 0.0, 0.0, 1.0)
	 - Scale: (1.0, 1.0, 1.0)
	 - Material[name=Sinbad/Sheaths, def=Phong Lighting, tech=null]
	Sinbad-geom-6 (Geometry)
	 - Position: (0.0, 0.0, 0.0)
	 - Rotation: (0.0, 0.0, 0.0, 1.0)
	 - Scale: (1.0, 1.0, 1.0)
	 - Material[name=Sinbad/Spikes, def=Phong Lighting, tech=null]
	Sinbad-geom-7 (Geometry)
	 - Position: (0.0, 0.0, 0.0)
	 - Rotation: (0.0, 0.0, 0.0, 1.0)
	 - Scale: (1.0, 1.0, 1.0)
	 - Material[name=Sinbad/Clothes, def=Phong Lighting, tech=null]

I am using the following versions of the libraries:

  • Minie-4.0.1
  • Heart-6.4.2

Edit: I changed the saveModel() and reloadModel() methods as suggested.

1 Like

Sinbad is an OGRE XML model, so you may be hitting issue 1395 there. If so, there are various ways to work around it:

  1. invoke saveInitialPose() on the Armature before exporting to J3O
  2. invoke applyBindPose() on the Armature after loading the J3O
  3. load an animation clip after loading the J3O
1 Like

I modified the code as you suggested. Sinbad is no longer crumpled as before, but the problem of the collisions shapes not correctly positioned on the bones of the 3d model remains.

In my previous post there is the modified code.

Here is the result.

2 Likes