Local Info

Topics

j3d.org

Animation and Dynamic Updates Example

In this example, we show how to make dynamic changes to the scene graph. Though it only presents animation of an object, the same process applies to changing scene graph structure or other interactions. If you would like to see the complete version of this code, it can be found in the examples/basic directory and is named AnimationDemo.java and RotationAnimation.java.

 

Setting up the application

Adding animation or any form of dynamic updates starts with the same basic application structure as you saw in the initial Hello World demo. On top of that we now add some more code that will demonstrate the movement of an object over time. Though simple in design, it illustrates the concepts that you will be using time and again. The new code makes very few changes to that skeleton you had - only 3 lines of code difference.

Understanding the Aviatrix3D Runtime model

The design of Aviatrix3D scene graph restricts you from making random changes to the scene graph, by only allowing updates a certain very specific times. The primary motivator for this is dealing the the need to buffer and syncrhonise the updates in a multi-threaded situation. Allowing the user to make changes to the data at arbitrary times can cause a lot of headaches to the internal implementation, so the design has taken the safety-first approach to reduce synchronisation costs as much as possible.

Making updates to the scene graph is a two step process - you start by telling a node that you would like to make changes to it by registering a listener instance. At some time later, your listener will be called to say it is safe to update the nominated node (but no other) at that point, this is when you can make changes.

Aviatrix3D makes some distinctions in the types of updates based on the effect they have on the scene graph. The two options you have are for bounds or data. You register for bound changes whenever your effects will change the bounds of a node. For example, adding or removing children of a group, changing a transform or setting the vertices of the geometry. Data updates are used for when the node's data will not change any bounding information, such as material colours, light direction parameters, etc.

Although you can register the callbacks for these at any time, you may decide that you want to synchronise the changes within a given frame. To do this, another observer class is provided. This class is registered with the render manager instance and is called at the start of every frame before any other updates are done. During this call, it is safe to assume that the scene graph structure is consistent and up to date (it is also a point where picking is enabled too, but we'll cover that in a later tutorial). For the most part, we expect that applications will always use both APIs so this tutorial will cover them as one.

Registering and updating the scene graph

In order to update the scene graph, you need to first register with the node that you want to modify an instance of NodeUpdateListener. This interface has two methods defined - one each for data and bound update callbacks. The provided data to these will be explained shortly.

To register for these callbacks, you need to call one of two methods. If you want to change the data associated with a node, for example the colour or direction of a light, then dataChanged() on SceneGraphObject is the desired method. The only argument is the listener instance to be called when the updates should be made. If you want to change something associated with bounds, then you need to make use of the boundsChanged() method of Node. Both of these methods take just a single argument - the instance of the listener that you would like to be called when it is ready.

For our example, we need to modify a transform to move the object about, so our class definition starts with the following code:

public class RotationAnimation
    implements NodeUpdateListener
{
    private Vector3f translation;
    private Matrix4f matrix;
    private TransformGroup transform;
    private float angle;

    public RotationAnimation(TransformGroup tx)
    {
        translation = new Vector3f();
        matrix = new Matrix4f();
        matrix.setIdentity();
        transform = tx;
    }

Fairly simple code - we need a TransformGroup instance to modify, and some local variables to keep track of the previous value (we're just going to be translating the object in a circle). The interesting code comes in the next part - handling the callback.

There are two methods that you have the choice of in the NodeUpdateListener interface. Since our changes are going to effect the bounds, then our code should go into the updateNodeBoundsChanges() method. In this method we will calculate the new position the object should be at and set the transform. Since we're inside the right callback, we won't be given any errors by the API.

public void updateNodeBoundsChanges(Object src)
{
    angle += Math.PI / 1000;

    float x = 0.5f * (float)Math.sin(angle);
    float y = 0.5f * (float)Math.cos(angle);

    translation.x = x;
    translation.y = y;

    matrix.setTranslation(translation);

    transform.setTransform(matrix);
}

public void updateNodeDataChanges(Object src)
{
}

The second method, in this example, remains empty as we have nothing to do there.

Processing per-frame notifications

For smooth animation, you will need to modify the transforms of the geometry on a regular basis. Typically this will be every frame, so your animation code starts with creating an instance of the interface that handles this - ApplicationUpdateObserver. This interface has two methods named updateSceneGraph() and appShutdown().

When the update method is called by Aviatrix3D, you are now at the beginning of the frame and may perform a number of actions. In this example, we are using this method to simply register with the TransformGroup that we would like to change something that would effect its bounds - naming the transformation matrix. In more complex applications, it can become a sync point for your application code with outside influences such as network updates of database calls.

Our basic class definition now looks like this:

public class RotationAnimation
    implements ApplicationUpdateObserver, NodeUpdateListener
{
   ...

Inside the method required by ApplicationUpdateObserver, there is only a single action - a notification to the transform that we'd like to make a change to it that it that would effect the bounds:

public void updateSceneGraph()
{
    transform.boundsChanged(this);
}

The second method, appShutdown() is used to deal with the internal interactions with AWT/Swing and JOGL. Internally, our rendering loop runs continuously and prods the application code to update the scene graph at specific points in time. At the other end of this is the loop that is directly talking to OpenGL performing the rendering functions. Because this rendering loop is typically quite an extensive time sink (iterating through every item of geometry, texture, shader etc) when the user terminates the application, the code needs to interrupt this process. This is particularly important on multi-CPU machines where the application thread may be completely separated from the rendering thread. In our experience, JOGL does not take kindly to having it's GL context ripped from underneath it as the application is closing down, and the rendering loop is still trying to toss GL requests at the GL context. nVidia drivers in particular crash hard under these sorts of circumstances. This callback is used to let you know that the internals of Aviatrix3D have detected the shutdown, cleanly handled it's internal cleanups and now it's your application's turn. If you don't care about this, just leave the method empty.

That completes the listener side of the code. The last step that needs to be taken is to register the application observer with the appropriate handler and pass in the right values to the scene graph.

Completing the code

As the last step, we need to create an instance of our RotationAnimation class. For this exercise, we are going to use the same base code as the BasicDemo from the Hello World tutorial. To add in the rotation functionality, head to the end of the setupSceneGraph() method you had before. Now just add in the last two lines of code:

    ...

    scene.setActiveView(vp);

    sceneManager.setScene(scene);

    RotationAnimation anim = new RotationAnimation(shape_transform);
    sceneManager.setApplicationObserver(anim);
}

The last line of code here is where the observer is registered with the render manager. Once the renderer is started, it will now receive calls every frame, resulting in the animated triangle that you should now be seeing.

That's pretty much all there is to how Aviatrix3D works with dynamic updates. There are a few more rules to play with, but most of them are variations on the themes presented in this tutorial.