I literally spent hours on this and it didn’t work. So I decided to strip it down to absolute basics, create a barebones solution and figure out exactly what is going wrong.
The kicker is, the answer is nothing, it works exactly as expected. Want to manipulate a bone in a Model in LibGDX and see the results propagated? Well, this is how.
First I modelled the following in Blender:
Its a simple mesh with a single animation attached. If you read my prior tutorials, the how of it will be no problem.
Then I ran it with this code:
package com.gamefromscratch;
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Files.FileType;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.loader.G3dModelLoader;
import com.badlogic.gdx.graphics.g3d.model.Node;
import com.badlogic.gdx.utils.JsonReader;
import com.badlogic.gdx.graphics.g3d.utils.AnimationController;
public class Boned implements ApplicationListener, InputProcessor {
private PerspectiveCamera camera;
private ModelBatch modelBatch;
private Model blobModel;
private ModelInstance blobModelInstance;
private Node rootBone;
private Environment environment;
private AnimationController animationController;
@Override
public void create() {
camera = new PerspectiveCamera(
75,
Gdx.graphics.getWidth(),
Gdx.graphics.getHeight());
camera.position.set(0f,3f,5f);
camera.lookAt(0f,3f,0f);
camera.near = 0.1f;
camera.far = 300.0f;
modelBatch = new ModelBatch();
JsonReader jsonReader = new JsonReader();
G3dModelLoader modelLoader = new G3dModelLoader(jsonReader);
blobModel = modelLoader.loadModel(Gdx.files.getFileHandle(“data/blob.g3dj”, FileType.Internal));
blobModelInstance = new ModelInstance(blobModel);
animationController = new AnimationController(blobModelInstance);
animationController.animate(“Bend”,-1,1f,null,0f);
rootBone = blobModelInstance.getNode(“Bone”);
environment = new Environment();
environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.8f, 0.8f, 0.8f, 1.0f));
Gdx.input.setInputProcessor(this);
}
@Override
public void dispose() {
modelBatch.dispose();
blobModel.dispose();
}
@Override
public void render() {
// You’ve seen all this before, just be sure to clear the GL_DEPTH_BUFFER_BIT when working in 3D
Gdx.gl.glViewport(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
Gdx.gl.glClearColor(1, 1, 1, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
camera.update();
animationController.update(Gdx.graphics.getDeltaTime());
modelBatch.begin(camera);
modelBatch.render(blobModelInstance, environment);
modelBatch.end();
}
@Override
public void resize(int width, int height) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public boolean keyDown(int keycode) {
if(keycode == Input.Keys.LEFT)
{
rootBone.translation.add(-1f, 0, 0);
returntrue;
}
else if(keycode == Input.Keys.RIGHT){
rootBone.translation.add(1f,0,0);
returntrue;
}
returnfalse;
}
@Override
public boolean keyUp(int keycode) {
// TODO Auto-generated method stub
returnfalse;
}
@Override
public boolean keyTyped(char character) {
returnfalse;
}
@Override
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
// TODO Auto-generated method stub
returnfalse;
}
@Override
public boolean touchUp(int screenX, int screenY, int pointer, int button) {
// TODO Auto-generated method stub
returnfalse;
}
@Override
public boolean touchDragged(int screenX, int screenY, int pointer) {
// TODO Auto-generated method stub
returnfalse;
}
@Override
public boolean mouseMoved(int screenX, int screenY) {
// TODO Auto-generated method stub
returnfalse;
}
@Override
public boolean scrolled(int amount) {
// TODO Auto-generated method stub
returnfalse;
}
}
End result, you get this:
Press the arrow keys and the root bone is translated exactly as you would expect!
Now, I spent HOURS trying to do this, and for the life of me I couldn’t figure out why the heck it doesn’t work. Sometimes going back to the basics gives you a clue.
In my test I used two models, one an animated bending arm, somewhat like the above. The other was an axe with a single bone for “attaching”. The exactly same code above failed to work. Somethings up here…
So after I get the above working fine, I have an idea… is it the animation? So I comment out this line:
animationController.animate(“Bend”,-1,1f,null,0f);
BOOM! No longer works.
So it seems changes you make to the bones controlling a Model only propagate if there is an animation playing. A hackable workaround seems to be to export an empty animation, but there has to be a better way. So at least I know why I wasted several hours on something that should have just worked. Now I am going to dig into the code for animate() and see if there is a call I can make manually without requiring an attached animation.
EDIT:
Got it!
Gotta admit it took a bit of digging, but I figured out what I am missing. Each time you make a change to the bones you need to call calculateTransforms() on the ModelInstance that owns the bone! Change the code like so:
public boolean keyDown(int keycode) {
if(keycode == Input.Keys.LEFT)
{
rootBone.translation.add(-1f, 0, 0);
blobModelInstance.calculateTransforms();
return true;
}
else if(keycode == Input.Keys.RIGHT){
rootBone.translation.add(1f,0,0);
blobModelInstance.calculateTransforms();
return true;
}
return false;
}
And presto, it works!
Just a warning, calculateTransforms() doesn’t appear to be light weight, so use with caution.
If you are curious where in the process calculateTransforms is called when you call animate(), it’s the end() call in BaseAnimationController.java called from the method applyAnimations().