So, this moving bones in LibGDX models is easier than I thought… Sorta

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:

BlobBlender

 

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:

BonedBlob

 

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().

Programming


Scroll to Top