Troubleshooting LibGDX exceptions. Configuring Eclipse to be slightly less stupid.

5. March 2014

 

If you’ve been a regular reader of my blog you will probably know that I am not Eclipse’s biggest cheerleader.  “I hate it” might be a bit strong, but not by much.  Recently though, I’ve been forced to use it more and more.  Frankly, if you want to work in cross platform Java, especially using Google’s tools, you are pretty much forced to these days.  One of my biggest annoyances is the obtuseness of the interface.  Today I am going to show one such example.

 

One of the things about working with LibGDX in Eclipse is it uses a fair number of threads and when an exception is thrown, if it wasn’t directly in your code, you are kinda screwed.  Look at this example, from code I am currently working on:

 

image

 

What I see here is I caught a “Throwable” derived exception and am throwing a RuntimeException in turn.  I can look up in the thread stack trace to see where it was thrown ( although the code above makes it pretty obvious, but stay with me for a moment).

 

If I look at the debug window, I can see that stack trace, to see where the exception came from:

image

 

There is a really useful piece of information here ( the exception type was GdxRuntimeException ), but the stack trace itself is pretty flat.  That is because each stack is thread specific and frankly, we just created this thread.  So our stack basically consists of Thread.run() and well, that’s about it.  We can tell by our code that the exception was thrown somewhere in LwjglApplication.mainLoop() but that is all we’ve got to work with.  We could set a breakpoint in mainLoop and run until it triggers, but that is extremely annoying.  What we want to do instead is stop execution when the exception is THROWN, not when it is caught.

 

This can be done in Eclipse, but in the way of Eclipse, it certainly isn’t intuitive.  Since we know our exception type is GdxRuntimeException, we will trigger it to stop execution whenever one of those is thrown.  Let’s look at how.

 

In Eclipse, switch to Debug Perspective.

image

 

Now locate the Breakpoint panel and the J! icon.  ( Intuitive eh? )

image

 

In the resulting dialog, in the Choose an exception text field, enter GdxRu*.  The asterisk is a wildcard, so it will return any Exception that start with GdxRu.  Click OK.

image

 

By the way, the default is *Exception*, meaning breakpoint on all Exceptions with the word Exception in them.  That sounds wonderful and all, problem is, it simply doesn’t work… yay Eclipse.  There is probably some 5 year old bug report somewhere explaining why.

OK, bitching aside, your exception list should now contain an entry for GdxRuntimeException:

image

 

Now right click the Exception and select Breakpoint Properties…

image

 

Make sure the following settings are enabled:

image

 

Now when you run the code again, the debugger will stop where the exception is thrown.

image

Wahoo!  Now that information is about 1000x more useful.  Now we see where our Exception is actually being generated.

 

A couple tips.

You can toggle the breakpoint exception off and on using the checkbox next to it in the breakpoint window:

image

 

Remember how I said earlier that setting *Exception* as the breakpoint simply doesn’t work?  So, what do you do if you want to stop on all exceptions?  You set a breakpoint on Throwable and set “Subclasses of this Exception” to true, like so:

image

 

Warning though, Exceptions are commonly thrown, so expect hit a whole lot of breakpoints!

 

I know this information is probably common knowledge to people that live in Eclipse, but for those of us that come from other IDEs, its frustrating until figured out, and certainly not intuitive to figure out.  Thus this post, hope a few of you found it useful.

Programming ,




Exporting 3D model to LibGDX directly from Blender

26. February 2014

 

Last month I wrote about creating and exporting a model from Blender to LIbGDX.  Part of the process involved exporting to FBX then running fbx-conv.  Wouldn’t it be nice if you could export directly from Blender?  Thankfully you can!  A week or so back @Dancovich told me about his Blender plugin on Twitter.  I intended to check it out right away, but truth told, recent experience had made me pretty sick of Blender, so I’ve taken my time.  Today we are going to look at that plugin.

 

First, download it from Github (direct zip download here)

Copy the folder io_scene_g3d to your Blender plugins folder.

image

In my case on my Windows 8 install, the plugin directory is: C:\Program Files\Blender Foundation\Blender\2.69\scripts\addons, like so:

image

Your location will depend on the operating system you use and how you chose to install Blender.  The github page linked above has more details.

 

Now fire up Blender 2.69 ( note, the plugin currently only supports Blender 2.69!

Select File->User Preferences

image

Then select the Addons tab

image

Now scroll down and locate Import-Export: LibGDX G3D Exporter and check it.

image

 

 

Now you are able to export directly from Blender to FBX.

 

Select File->Export->LibGDX G3D text format.

image

 

As you can see, currently there is no binary support.  During development I tend to stick with g3dj anyway.

 

Here is the scene from Blender:

image

 

And now that I run it in LibGDX?

image

 

Ahhh, crap.  It’s an easy enough problem though.  The exporter saved my texture as an absolute path, I instead want a relative path.  Opening up the generated g3dj file, I see:

image

Change that to:

image

And you are good to go.  You can probably change Blender to work in relative paths and avoid this problem all together.  If not, altering the script to strip the paths should be a no brainer.  Now with that change we run it and:

image

Hmmmm… that’s not what you were expecting is it?  What’s going on here?

 

Well, fbx-conv automatically flips the axis from Blender Z up to LibGDX Y up.  This exporter does not.  You can easily perform the same thing in code by rotating –90 degrees about the X axis, like:

modelInstance.transform.rotate(1, 0, 0, -90);

 

Then run the code and:

 

image

 

Woot, identical to Blender!

 

I havent got the chance to test the exporter all that extensively, Ive not really done any work with Blender in the last week or so, so I cant really tell you how well it performs with animations.  That said, especially during development, this could be a huge time saver for quick iterations.  The developer is actively looking for people to try it out and report bugs back.  So if you fancy exporting from Blender to LibGDX directly try it out and let @Dancovich on Twitter know if you encounter any bugs.

Programming, Art , ,




Ok now, what the hell is going on here. Blender and bones are going to be the death of me

18. February 2014

So i’ve posted a lot lately about my recent experiences with working with bones exported from Blender to LibGDX.  I encountered two problems, first only the base of the bone was available once exported.  Second, bones that were external to the mesh weren’t being updated.  The first isn’t really a big deal, except the solution to it seemingly is impacted by the second problem.  There was a thread over on LibGDX forums I posted my experiences on, and Xoppa, the guy behind the 3D portions of LibGDX posted that my observations simply weren’t correct.  This post here is mostly a recap of that thread.  I am not sure if anything in this thread will be of value to any of you, but it does illustrate that sometimes… who knows, it might just be gremlins!

 

So I set about creating a minimal sample to illustrate the problem I was having.  I have literally done this a few dozen, perhaps hundred, of times the past week.

 

I created an ultra simple model in Blender, with a bone external to mesh.  In my experiences to this point, every time I try to get the position of this external bone, the results will always be 0,0,0.  All internal bones will work fine, but the external one won’t.

 

Here is the model:

11

 

Simple enough, a mesh with a simple 3 bone armature bound to it.  What you don’t see is I’ve also done a small animation sequence ( as Default Take ).

 

I then load it with the following code.  The idea is draw a sphere at the location of each bone.  Pretty much what I expected to see is two two spheres, with the third one missing ( as it will actually be at 0,0,0, the same location as the first one.

package com.gamefromscratch;

 

import com.badlogic.gdx.ApplicationListener;

import com.badlogic.gdx.Files.FileType;

import com.badlogic.gdx.Gdx;

import com.badlogic.gdx.graphics.Color;

import com.badlogic.gdx.graphics.GL10;

import com.badlogic.gdx.graphics.GL20;

import com.badlogic.gdx.graphics.PerspectiveCamera;

import com.badlogic.gdx.graphics.VertexAttributes.Usage;

import com.badlogic.gdx.graphics.g3d.Environment;

import com.badlogic.gdx.graphics.g3d.Material;

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;

import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;

import com.badlogic.gdx.math.Vector3;

 

 

public class TankDemo implements ApplicationListener {

    private PerspectiveCamera camera;

    private ModelBatch modelBatch;

    private AnimationController animationController;

    

    private Model model;

    private ModelInstance modelInstance;

 

    private Model pivot;

    private ModelInstance p1, p2, p3;

    private Node bone1,bone2,bone3;

    

    private Environment environment;

    

    @Override

    public void create() {  

    camera = new PerspectiveCamera(

   75,

                Gdx.graphics.getWidth(),

                Gdx.graphics.getHeight());

        

        camera.position.set(0f,0f,-8f);

        camera.lookAt(0f,0f,0f);

        camera.near = 0.1f; 

        camera.far = 300.0f;

 

        modelBatch = new ModelBatch();

        

        JsonReader jsonReader = new JsonReader();

        G3dModelLoader modelLoader = new G3dModelLoader(jsonReader);

        model = modelLoader.loadModel(Gdx.files.getFileHandle("data/demo.g3dj", FileType.Internal));

        modelInstance = new ModelInstance(model);

        

        animationController = new AnimationController(modelInstance);

        animationController.animate("Default Take",-1,null,0);

        

        bone1 = modelInstance.getNode("Bone");

        bone2 = modelInstance.getNode("Bone_001");

        bone3 = modelInstance.getNode("Bone_002");

        

        ModelBuilder mb = new ModelBuilder();

        

        pivot = mb.createSphere(0.5f,0.5f,0.5f,10,10,GL20.GL_LINES,new Material(ColorAttribute.createDiffuse(Color.RED)),Usage.Position | Usage.Normal);

        p1 = new ModelInstance(pivot);

        p2 = new ModelInstance(pivot);

        p3 = new ModelInstance(pivot);

        

 

        

        environment = new Environment();

        environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.8f, 0.8f, 0.8f, 1.0f));

    }

 

    @Override

    public void dispose() {

        modelBatch.dispose();

        model.dispose();

    }

 

    @Override

    public void render() {

        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);

        

        animationController.update(Gdx.graphics.getDeltaTime());

        Vector3 pos = new Vector3();

        bone1.globalTransform.getTranslation(pos);

        p1.transform.set(bone1.globalTransform);

        p2.transform.set(bone2.globalTransform);

        p3.transform.set(bone3.globalTransform);

        

        camera.update();

        modelBatch.begin(camera);

        modelBatch.render(modelInstance, environment);

        modelBatch.render(p1, environment);

        modelBatch.render(p2, environment);

        modelBatch.render(p3, environment);

        

        modelBatch.end();

    }

 

    @Override

    public void resize(int width, int height) {

    }

 

    @Override

    public void pause() {

    }

 

    @Override

    public void resume() {

    }

 

}

 

Then when I run this code I see:

1

 

Or, with the line rendering the main mesh itself commented out to more clearly show the bone locations:

2

 

For #$@#$@ing #@$@#$ @#$@$’s sake...

 

Why the swearing?  Because this is working EXACTLY how it is supposed to.  EXACTLY how I expected it to a week ago.  Exactly how Xoppa said it should.

 

What it isn’t doing is behaving EXACTLY how it has been for the last 50 or so times I tried the same thing!  Literally every time I did this before, that final bone didn’t update.  No bones in the armature that were external to the mesh itself were updated.  So I thought maybe that’s it…  maybe something about this export caused the third bone to be part of the geometry… maybe that’s it!

Wtf

No such luck.  The exported g3dj file looks just like any dozen of others I have generated in the past.

 

This is unbelievably frustrating at this point, as a problem I have been trying furiously to work around simply seems to no longer exist.  At this point I simply have NO idea what I was doing in the past that caused the entire process to break.  And that doesn’t leave me with a warm fuzzy feeling.

 

Getting content out of Blender has never been the funnest process, but this last week has been an exercise in frustration… and at the end of the day, the source of the frustration seems to no longer exist.

 

There are a few differences between this example and some of my prior ones.  Generally I load two different Models from disk ( instead of building one using ModelBuilder ), so perhaps once I add another model to the mix I will start seeing the old behaviour ( although that really wouldn’t make much sense ).  Also, I am using a different computer than I normally use ( on my Mac today ), but it is the same version level of Blender and LibGDX, so that shouldn’t be a factor either.

 

At this point, I just don’t know what to say… maybe the fates simply hated me last week… that would explain the infernal cold they inflicted upon me!

Programming , ,




No bones about it. Bones in LibGDX and Blender

14. February 2014

This is one of those things I’ve been fighting with for the past few days so I thought I would share a bit.

 

First a bit of a primer for those of you that aren’t all that familiar with bones yet.  Bones are a common way of animating 3D geometry.  Essentially you add an armature (skeleton) to your scene and bind it to the geometry of your mesh.  Moving the bones the compose your armature will then update the bound geometry.

 

Let’s take a look at an example in Blender.  Here is the anatomy of skeletal animation in Blender:

Blender

 

Each bone in turn has a “weight”.  This is the amount of influence the bone’s movements have on the geometry.  First you need to parent the mesh to the armature.  Simply select the mesh, then shift select the bones and press Ctrl+P.

 

Each bone then has a certain weight attached to it.  The colour determines how much influence a bone has over the geometry.  Consider the following, it’s the weight mapping for the top most bone in that example:

Blender2

 

So now, if in pose mode, I rotate that bone, we see:

Blender3

As you can see, moving the bone changes the surrounding geometry based on the bones influence area.  In a nutshell, that is how bone animation works in Blender.  I cover more of the “how to” in the Blender to LibGDX tutorial if you want details.

 

 

So that’s bones in Blender, lets take a look at the LibGDX side of the equation.  Here is how the skeleton is represented in a g3dj file:

{"id": "Armature", 

"rotation": [-0.707107,  0.000000,  0.000000,  0.707107], 

"scale": [ 1.000000,  1.000000,  1.000000], 

"translation": [ 0.012381, -0.935900, -0.017023], 

"children": [

  {"id": "Bone", 

  "rotation": [ 0.500000, -0.500000,  0.500000,  0.500000], 

  "scale": [ 1.000000,  1.000000,  1.000000], 

  "children": [

    {"id": "Bone_001", 

    "rotation": [ 0.000000,  0.009739, -0.000000,  0.999953], 

    "scale": [ 1.000000,  1.000000,  1.000000], 

    "translation": [ 1.000000,  0.000000,  0.000000], 

    "children": [

      {"id": "Bone_002", 

      "rotation": [-0.000000, -0.013871,  0.000000,  0.999904], 

      "scale": [ 1.000000,  1.000000,  1.000000], 

      "translation": [ 1.575528,  0.000000,  0.000000]}

      ]}

    ]}

  ]}

]

 

You can also see the bones as part of the geometry node as well, like so:

{"id": "Cube",
   "translation": [-0.012381, -0.017023, 0.935900],
   "parts": [
   {"meshpartid": "shape1_part1",
   "materialid": "Material",
   "bones": [
      {"node": "Bone",
      "translation": [ 0.012381, 0.017023, -0.935900, 0.000000],
      "rotation": [ 0.500000, -0.500000, 0.500000, 0.500000],
      "scale": [ 1.000000, 1.000000, 1.000000, 0.000000]},

      {"node": "Bone_002",
      "translation": [ 0.012381, 0.047709, 1.639329, 0.000000],
      "rotation": [ 0.502062, -0.502062, 0.497930, 0.497930],
      "scale": [ 1.000000, 1.000000, 1.000000, 0.000000]},

      {"node": "Bone_001",
      "translation": [ 0.012381, 0.017023, 0.064100, 0.000000],
      "rotation": [ 0.495107, -0.495107, 0.504846, 0.504846],
      "scale": [ 1.000000, 1.000000, 1.000000, 0.000000]}
   ],
   "uvMapping": [[]]}
]},

 

The later are the bones that are contained in the mesh “Cube”.  This will be relevant in a minute.  Instead lets look at the Armature composition.

 

Each bone within the hierarchy is basically just a series of transforms relative to its parent.  The armature itself has a rotation, scale and translation, as do each child.  In your ModelInstance, the Armature is a hierarchy of Nodes, like so:

GDX1

 

Animations then are simply a series of transforms applied to bones over a period of time, like so:

GDX2

 

These values correspond with the keyframe values you set in Blender.

 

Now there are a couple gotchas to be aware of!

First off, in LibGDX a bone is probably more accurately called a joint.  Remember what a bone looked like in Blender:

GDX3

Only the “bone head” is used.  The tail effectively doesn’t exist.

 

So, positioning relative to a bone will bring you to the base, not the tail.  Therefore, if you want to say… use bones for positioning other limbs, you need to create an extra one, and this lead to a problem.

Say I want to create a bone then that I can search for in code to mount a weapon upon.  I would then have to do something like this:

Blender5

 

This allows me to locate the very tip of my geometry.  But there is a catch.  If I export it, I can see the new bone Bone_003 is part of my armature:

Gdx6

 

That said, remember the entry for “Cube” showed the bones it contains… yeah well, that’s a problem.

Gdx7

See… the new bone isn’t actually contained within the geometry.

As a direct result, when working with it in code in LIbGDX, it just doesn’t work.  It never returns the proper position, or at least the position I would expect.  I’ve also had some weird behaviour where an exported model with only a single bone can’t be programmatically updated as well.  I need to investigate this further.

 

As a result, I’ve decided that bones simply aren’t the way to go about it.  Instead what i’ve started doing is putting a null objet in where I want weapon mounts to appear.  It doesn’t seem to have the gotchas that bones have so far.

 

Sorry for the slow rate of updates, I am sick as a dog right now.  So if that post seemed a little incoherent, that’s why! :)

 

Programming , ,




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

5. February 2014

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 ,