Smooth Moves Smooth Moves

by Chet Haase
02/23/2006

Are you interested in doing some animations in your Java applications, but find yourself plagued by results that seem stuttery and choppy? Want to figure out the problems and smooth out those animations to make them better and more seamless in your application? This article examines some of the factors that affect animation smoothness and things that you can do in your code to make your animations look better.

In my blog entry, I looked at the various problems contributing to an animation looking choppy. I found that much of the choppiness in my animations came from two general sources:

Minimizing the Color Difference

The key idea here is to make our animations smoother by reducing the amount of color change for each frame of the animation.

How is this done? We have some particular object that we need to animate from here to there; we can't just alter the colors along the way to suit this approach, can we?

Yes and no. You may not be able to simply decide that you don't want a black space invader and you'd be happy with a light gray; sometimes that just doesn't work with the whole Evil motif. But there are things you can do to mitigate the strong contrast between the background color and the object color.

Vertical Retrace

As discussed in my blog, vertical retrace artifacts come from rendering to the screen at the same time as the screen is being refreshed from video memory. Suppose we are trying to move our image between frame n and frame n+1 like so:

vsyncNoArtifact

Now suppose that the vertical retrace (represented by the red line below) is happening right in the middle of this area as we are doing this copy:

vsyncNoArtifact

There are various workarounds to this issue, mostly related to the same problems and solutions described above for color distance, including:

There is also a "fix" to the problem: avoid encountering the vertical retrace completely. This is done by waiting for the "vertical blank interval," which is a slot in time after the refresh has reached the bottom of the screen and before it starts again at the top. If you can successfully synchronize your application to be timed with this interval, then you can be fairly assured that such tearing operations will not happen because your application will update the screen only when it is safe to do so.

This fix is easy for anyone writing a fullscreen Java application; applications that use the FlipBufferStrategy get this for free. When that buffer strategy copies its contents to the screen from the back buffer, it specifically waits for the vertical blank interface, and thus avoids tearing completely.

The fix is not as easy for typical windowed (non-fullscreen) applications, because there is currently no way to tell Java to wait for this interval, and there is no way for your code to know when it is a good time to go ahead with the copy. We hope to address this in a future release (I just filed a bug on it last week!), but in the meantime there is no way to get this behavior.

Or is there?

After I hacked a prototype of the fix for the fix in the Java 2D implementation (which will be used in any real fix we provide in Java), I tried a similar approach with application code--and it worked!

Of course, the fix for application code is a bit different because there is no Java API to access this functionality. But there is native API available, at least on Windows, so with a very small amount of JNI code, I was able to successfully synchronize on the vertical retrace interval. I put this code into the SmoothAnimation application, discussed below, so you can try it out for yourself (assuming you are on Windows; I've only implemented a solution for that platform so far).

SmoothAnimation: Demonstrating the Problems and Solutions

Now we've talked about the problems and solutions; let's look at some code.

I wrote a sample application, SmoothAnimation, to play around with various factors and possible solutions to the choppy animation problem. The application renders two animations at the same rate: a fading animation that fades an object in and out between complete opacity and complete transparency, and a moving animation that shifts and object from left to right and back again at some steady rate. Running this application will show the problems we've discussed above pretty clearly; the fading animation looks quite smooth, while the moving animation looks quite choppy. The application also allows various flags to be toggled with keyboard options, to see the impact that different rendering approaches has on the perceived smoothness of the animations.

I'll put some snippets below as I discuss the relevant parts, but you are encouraged to run and download the whole application. Here are some ways you can use the sample code, in ascending order of detail and difficulty:

1) Creating the graphics

The application first creates the graphics that it will use. In all cases, the application creates an image that will be copied later using drawImage() during the animation, but this image may be created from an actual image (I use my personal favorite, duke.gif) or from rendering commands (I use a solid rectangle, which contrasts sharply with the white background to better illustrate the points in this article). At first, the application uses a solid black rectangle, but this can be toggled during the application, as we'll see below.

Here is the image creation routine:

void createAnimationImage() {
    GraphicsConfiguration gc = GraphicsEnvironment.
        getLocalGraphicsEnvironment().
        getDefaultScreenDevice().getDefaultConfiguration();
    image = gc.createCompatibleImage(imageW, imageH, Transparency.TRANSLUCENT);
    Graphics2D gImg = (Graphics2D)image.getGraphics();
    if (useImage) {
        try {
            Image originalImage = ImageIO.read(new File("duke.gif"));
            gImg.drawImage(originalImage, 0, 0, imageW, imageH, null);
            gImg.dispose();
        } catch (Exception e) {}
    } else {
    // use graphics
    Color graphicsColor;
    graphicsColor = Color.black;
    gImg.setColor(graphicsColor);
    gImg.fillRect(0, 0, imageW, imageH);
    }
}
2) Running the Timer

The next step is to start a timer, which will run the animation loop. I do this by using my TimingFramework classes. You could do this with the simpler timer classes built into core, but TimingFramework has some additional facilities (like being able to reverse the animation at the end of each cycle) that make it easier for more involved animation usage. Here is the code to create and start the timer:

TimingController timer = new TimingController(
    new Cycle(1000, 30), 
    new Envelope(TimingController.INFINITE, 500, 
                 Envelope.RepeatBehavior.REVERSE, 
                 Envelope.EndBehavior.HOLD), 
    component);
timer.start();

I'll leave it as an exercise to the reader to check out TimingFramework for the details (there is an article linked from the project that describes the innards of the framework), but the basics here are:

Now let's look at what happens as the animation runs. We will get callbacks into the timingEvent() method whenever the timing framework determines that it is time for another frame to be rendered:

public void timingEvent(long cycleElapsedTime,
        long totalElapsedTime, 
        float fraction) {
opacity = fraction;
moveX = moveMinX + (int)(fraction * (float)(moveMaxX - moveMinX));
repaint();
}

Here, the cycleElapsedTime and totalElapsedTime parameters are not used; we only care about the fraction, which represents the fraction of animation elapsed between the start and endpoints of the total animation. We use this fraction to set the opacity value, used to calculate the translucency of our fading animation, and the moveX variable, used to position the moving object in our moving animation. After we've set these values, we call repaint() to force the application to render itself with these new values.

3) Rendering onto the Screen

Finally, let's look at the meat of the application, rendering the graphics onto the screen.

There are three main tasks in the paintComponent() method: erasing to the background color (I specifically wanted white, to contrast with the default black rectangle object), drawing a fading animation, and drawing a moving animation.

Erasing the background: This is simple; we just erase to white, like so:

g.setColor(Color.white);
g.fillRect(0, 0, getWidth(), getHeight());

Render the fading animation: In this step, we use the current value of opacity (calculated at every step of the animation, as seen later in timingEvent()) to create a new AlphaComposite object and set that on the Graphics2D object. Then we render our existing image using this Graphics2D object.

Graphics2D gFade = (Graphics2D)g.create();
AlphaComposite newComposite = 
    AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 
                               opacity);
gFade.setComposite(newComposite);
gFade.drawImage(image, fadeX, fadeY, null);
gFade.dispose();

Note that I'm creating and disposing a new Graphics2D object here (cloned from the one passed into paintComponent()). This allows me to set the composite on the graphics object without having to worry about resetting it when I'm done (so I won't side-effect the rendering of other objects using the original graphics object). The opacity variable is set during the calls to timingEvent() (described above). fadeX and fadeY are just instance variables that declare where I want this thing to appear.

Render the moving animation: This part is simple:

g.drawImage(image, moveX, moveY, null);

We simply copy the image into the appropriate location, determined by the moveX and moveY parameters. moveY is static (we are only moving the object in the X direction), and moveX is set during each call to timingEvent().

4) Handling Vertical Retrace

The application also contains an optional piece to alleviate the problems with vertical retrace described above. I made this work only for the case where the application is running on Windows and DirectDraw is available, but even if you cannot get this part to work on your target system, it's interesting to see how you might go about doing something like this in general.

Here are the relevant bits that make this work:

That's it for the default behavior of the application; it creates the image, sets up the timer, and paints the two animations happily forever. But there's more; there are keyboard commands you can use while the application is running to try out different approaches to rendering to mitigate the choppiness and see the results:

To run the example, cd into the directory and compile everything. I've included a makefile to help out if you're going to try out the vertical retrace fix on Windows; you can either use the makefile directly (after changing a couple of variables in that file) or just see how it's doing the build. Otherwise, just compile the Java file com/sun/animation/SmoothAnimation.java and run it:

java com.sun.animation.SmoothAnimation

Conclusion

Check out the sample code, compile it, play with it, and see what you think. There are many more involved solutions than the ones I've toyed with here, but I think you'll see that anything that minimizes the rate of change of color helps in the overall problem.

Resources

Chet Haase worked on the Java SE team at Sun for years, most recently as an architect in the Java Client Group.


 Feed java.net RSS Feeds