Displaying Image Sequences


Applying Image Effects

ImagesTests uses a Swing timer to animate its image effects rather than the active rendering approach developed in early chapters. This is purely a matter of preventing the code from becoming overly complicated since the high accuracy offered by active rendering isn't required. The visual effects employed here are generally composed from 5 to 10 distinct frames, displayed over the course of one or two seconds; this implies a need for a maximum of 10 FPS, which is within the capabilities of the Swing timer.

If necessary, the effects techniques can be easily translated to an active rendering setting.


The timer-driven framework is illustrated by Figure 6-4. The details of actionPerformed( ) and paintComponent( ) are explained below.

Figure 6-4. ImagesTests and the Swing timer


ImagesTests maintains a global variable (counter) that starts at 0 and is incremented at the end of each paintComponent( ) call, modulo 100.

The modulo operation isn't significant but is used to keep the counter value from becoming excessively large.


counter is used in many places in the code, often to generate input arguments to the visual effects.

Starting ImagesTests

The main( ) method for ImagesTests creates a JFrame and adds the ImagesTests JPanel to it:

     public static void main(String args[])     {        // switch on translucency acceleration in Windows        System.setProperty("sun.java2d.translaccel", "true");        System.setProperty("sun.java2d.ddforcevram", "true");        // switch on hardware acceleration if using OpenGL with pbuffers        // System.setProperty("sun.java2d.opengl", "true");             ImagesTests ttPanel = new ImagesTests( );        // create a JFrame to hold the test JPanel        JFrame app = new JFrame("Image Tests");        app.getContentPane( ).add(ttPanel, BorderLayout.CENTER);        app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);        app.pack( );        app.setResizable(false);        app.setVisible(true);     } // end of main( )

More interesting are the calls to setProperty( ). If I require hardware acceleration of translucent images in Windows (e.g., for the PNG files basn6a08.png and basn6a16.png), then the Java 2D translaccel and ddforcevram flags should be switched on. They also accelerate alpha composite operations. On Linux/Solaris, only the opengl flag is required for hardware acceleration, but pbuffers are an OpenGL extension, so may not be supported by the graphics card. The simplest solution is to try code with and without the flag and see what happens. The ImagesTests constructor initiates image loading, creates the ImageSFXs visual effects object, obtains references to the o images, and starts the timer:

     // globals     private ImagesLoader imsLoader;   // the image loader     private int counter;     private boolean justStarted;     private ImageSFXs imageSfx;       // the visual effects class     private GraphicsDevice gd;      // for reporting accl. memory usage     private int accelMemory;     private DecimalFormat df;     public ImagesTests( )     {       df = new DecimalFormat("0.0");  // 1 dp       GraphicsEnvironment ge =              GraphicsEnvironment.getLocalGraphicsEnvironment( );       gd = ge.getDefaultScreenDevice( );       accelMemory = gd.getAvailableAcceleratedMemory( );  // in bytes       System.out.println("Initial Acc. Mem.: " +             df.format( ((double)accelMemory)/(1024*1024) ) + " MB" );           setBackground(Color.white);       setPreferredSize( new Dimension(PWIDTH, PHEIGHT) );       // load and initialise the images       imsLoader = new ImagesLoader(IMS_FILE);   // "imsInfo.txt"       imageSfx = new ImageSFXs( );       initImages( );       counter = 0;       justStarted = true;       new Timer(PERIOD, this).start( );    // PERIOD = 0.1 sec     } // end of ImagesTests( )

The GraphicsDevice.getAvailableAcceleratedMemory( ) call returns the current amount of available hardware-accelerated memory. The application continues to report this value as it changes to give an indication of when BufferedImage objects become managed images. This is explained more fully later in this chapter.

Initializing Images

initImages( ) does three tasks: It stores references to the o images as global variables, creates ImagesPlayers objects for the n and s images and references the first g fighter image, its left image:

     // global variables     // hold the single 'o' images     private BufferedImage atomic, balls, bee, cheese, eyeChart,                           house, pumpkin, scooter,                           fighter, ufo, owl, basn8, basn16;     // for manipulating the 'n' and 's' images     private ImagesPlayer numbersPlayer, figurePlayer, carsPlayer,                          catsPlayer, kaboomPlayer;     private void initImages( )     {       // initialize the 'o' image variables       atomic = imsLoader.getImage("atomic");       balls = imsLoader.getImage("balls");       bee = imsLoader.getImage("bee");       cheese = imsLoader.getImage("cheese");       eyeChart = imsLoader.getImage("eyeChart");       house = imsLoader.getImage("house");       pumpkin = imsLoader.getImage("pumpkin");       scooter = imsLoader.getImage("scooter");       ufo = imsLoader.getImage("ufo");       owl = imsLoader.getImage("owl");       basn8 = imsLoader.getImage("basn6a08");       basn16 = imsLoader.getImage("basn6a16");           /* Initialize ImagesPlayers for the 'n' and 's' images.          The 'numbers' sequence is not cycled, the other are.       */       numbersPlayer = new ImagesPlayer("numbers", PERIOD, 1, false, imsLoader);       numbersPlayer.setWatcher(this);             // report the sequence's finish back to ImagesTests       figurePlayer =  new ImagesPlayer("figure", PERIOD, 2, true, imsLoader);       carsPlayer = new ImagesPlayer("cars", PERIOD, 1, true, imsLoader);       catsPlayer = new ImagesPlayer("cats", PERIOD, 0.5, true, imsLoader);       kaboomPlayer = new ImagesPlayer("kaboom", PERIOD, 1.5, true, imsLoader);       // the 1st 'g' image for 'fighter' is set using a filename prefix       fighter = imsLoader.getImage("fighter", "left");     }  // end of initImages( )

The ImagesPlayer class wraps up code for playing a sequence of images. ImagesTests uses ImagesPlayer objects for animating the n and s figure, cars, kaboom, and cats images. Each sequence is shown repeatedly.

numbers is also an n type, made up of several images, but its ImagesPlayer is set up a little differently. The player will call sequenceEnded( ) in ImagesTests when the end of the sequence is reached, and it doesn't play the images again. The callback requires that ImagesTests implements the ImagesPlayerWatcher interface:

     public class ImagesTests extends JPanel                  implements ActionListener, ImagesPlayerWatcher     {  // other methods       public void sequenceEnded(String imageName)       // called by ImagesPlayer when its images sequence has finished       {  System.out.println( imageName + " sequence has ended");  }     }

The name of the sequence (i.e., numbers) is passed as an argument to sequenceEnded( ) by its player. The implementation in ImagesTests only prints out a message, but it could do something more useful. For example, the end of an animation sequence could trigger the start of the next stage in a game.

Updating the Images

Image updating is carried out by imagesUpdate( ) when actionPerformed( ) is called (i.e., every 0.1 second):

     public void actionPerformed(ActionEvent e)     // triggered by the timer: update, repaint     {       if (justStarted)   // don't do updates the first time through         justStarted = false;       else         imagesUpdate( );         repaint( );     } // end of actionPerformed( )     private void imagesUpdate( )     {       // numbered images ('n' images); using ImagesPlayer       numbersPlayer.updateTick( );       if (counter%30 == 0)     // restart image sequence periodically         numbersPlayer.restartAt(2);       figurePlayer.updateTick( );       // strip images ('s' images); using ImagesPlayer       carsPlayer.updateTick( );       catsPlayer.updateTick( );       kaboomPlayer.updateTick( );       // grouped images ('g' images)       // The 'fighter' images are the only 'g' images in this example.       updateFighter( );     } // end of imagesUpdate( )

imagesUpdate( ) does nothing to the o images, since they are processed by paintComponent( ); instead, it concentrates on the n, s, and g images.

updateTick( ) is called in all of the ImagesPlayers (i.e., for numbers, figure, cars, cats, and kaboom). This informs the players that another animation period has passed in ImagesTests. This is used to calculate timings and determine which of the images in a sequence is the current one.

The n numbers images are utilized differently: When the counter value reaches a multiple of 30, the sequence is restarted at image number 2:

     if (counter%30 == 0)       numbersPlayer.restartAt(2);

The on-screen behavior of numbers is to step through its six images (pictures numbered 0 to 5) and stop after calling sequenceEnded( ) in ImagesTests. Later, when ImagesTests's counter reaches a multiple of 30, the sequence will restart at picture 2, step through to picture 5 and stop again (after calling sequenceEnded( ) again). This behavior will repeat whenever the counter reaches another multiple of 30.

With a little more work, behaviors such as this can be quite useful. For example, a repeating animation may skip its first few frames since they contain startup images. This is the case for a seated figure that stands up and starts dancing. The numbers behavior illustrates that ImagesPlayer can do more than endlessly cycle through image sequences.

updateFighter( ) deals with the g fighter images, defined in imsInfo.txt:

     g fighter  left.gif right.gif still.gif up.gif

Back in initImages( ), the global BufferedImage variable, fighter, was set to refer to the "left" image. updateFighter( ) cycles through the other images using the counter value modulo 4:

     private void updateFighter( )     /* The images are shown using their filename prefixes (although a        positional approach could be used, which would allow an        ImagesPlayer to be used.     */     { int posn = counter % 4;  // number of fighter images;             // could use  imsLoader.numImages("fighter")       switch(posn) {         case 0:           fighter = imsLoader.getImage("fighter", "left");           break;         case 1:           fighter = imsLoader.getImage("fighter", "right");           break;         case 2:           fighter = imsLoader.getImage("fighter", "still");           break;         case 3:           fighter = imsLoader.getImage("fighter", "up");           break;         default:           System.out.println("Unknown fighter group name");           fighter = imsLoader.getImage("fighter", "left");           break;       }     }  // end of updateFighter( )

This code only updates the fighter reference; the image is not displayed until paintComponent( ) is called.

Painting the Images

paintComponent( ) has four jobs:

  • Applies a visual effect to each o image and displays the result

  • Requests the current image from each ImagesPlayer and displays it

  • Displays any change in the amount of hardware accelerated memory (VRAM)

  • Increments the counter (modulo 100)

Here's the implementation:

     public void paintComponent(Graphics g)     {        super.paintComponent(g);        Graphics2D g2d = (Graphics2D)g;        //antialiasing        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,                             RenderingHints.VALUE_ANTIALIAS_ON);            // smoother (and slower) image transforms  (e.g., for resizing)        g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION,                          RenderingHints.VALUE_INTERPOLATION_BILINEAR);        // clear the background        g2d.setColor(Color.blue);        g2d.fillRect(0, 0, PWIDTH, PHEIGHT);        // ------------------ 'o' images ---------------------        /* The programmer must manually edit the code here in order to           draw the 'o' images with different visual effects. */        // drawImage(g2d, atomic, 10, 25);   // only draw the image        rotatingImage(g2d, atomic, 10, 25);        mixedImage(g2d, balls, 110, 25);        teleImage = teleportImage(g2d, bee, teleImage, 210, 25);        flippingImage(g2d, cheese, 310, 25);        blurringImage(g2d, eyeChart, 410, 25);        reddenImage(g2d, house, 540, 25);        zapImage = zapImage(g2d, pumpkin, zapImage, 710, 25);        brighteningImage(g2d, scooter, 10, 160);        fadingImage(g2d, ufo, 110, 140);        negatingImage(g2d, owl, 450, 250);        mixedImage(g2d, basn8, 650, 250);        resizingImage(g2d, basn16, 750, 250);        // --------------- numbered images -------------------        drawImage(g2d, numbersPlayer.getCurrentImage( ), 280, 140);        drawImage(g2d, figurePlayer.getCurrentImage( ), 550, 140);        // --------------- strip images ----------------------        drawImage(g2d, catsPlayer.getCurrentImage( ), 10, 235);        drawImage(g2d, kaboomPlayer.getCurrentImage( ), 150, 250);        drawImage(g2d, carsPlayer.getCurrentImage( ), 250, 250);        // --------------- grouped images --------------------        drawImage(g2d, fighter, 350, 250);        reportAccelMemory( );        counter = (counter + 1)% 100;    // 0-99 is a large enough range     } // end of paintComponent( )

The calls to Graphics2D.setRenderingHint( ) show how Java 2D can make rendering requests, based around a key and value scheme.

The anti-aliasing rendering hint has no appreciable effect in this example since no lines, shapes, or text are drawn in the JPanel. Consequently, it might be better not to bother with it, thereby gaining a little extra speed. The interpolation hint is more useful though, especially for the resizing operation. For instance, there is a noticeable improvement in the resized smoothness of basn6a16 with the hint compared to when the hint is absent.

The 11 visual effects applied to the o images are explained below. However, all the methods have a similar interface, requiring a reference to the graphics context, the name of the image, and the (x, y) coordinate where the modified image will be drawn.

The n and s images are managed by ImagesPlayer objects, so the current image is obtained by calling the objects' getCurrentImage( ) method. The returned image reference is passed to drawImage( ), which wraps a little extra error processing around Graphics' drawImage( ) method:

     private void drawImage(Graphics2D g2d, BufferedImage im, int x, int y)     /* Draw the image, or a yellow box with ?? in it if        there is no image. */     {        if (im == null) {          // System.out.println("Null image supplied");          g2d.setColor(Color.yellow);          g2d.fillRect(x, y, 20, 20);          g2d.setColor(Color.black);          g2d.drawString("??", x+10, y+10);        }        else          g2d.drawImage(im, x, y, this);     }

Information on Accelerated Memory

reportAccelMemory( ) prints the total amount of VRAM left and the size of the change since the last report. This method is called at the end of every animation loop but only writes output if the VRAM quantity has changed:

     private void reportAccelMemory( )     // report any change in the amount of accelerated memory     {       int mem = gd.getAvailableAcceleratedMemory( );   // in bytes       int memChange = mem - accelMemory;       if (memChange != 0)         System.out.println(counter + ". Acc. Mem: " +                   df.format( ((double)accelMemory)/(1024*1024) ) +                               " MB; Change: " +                   df.format( ((double)memChange)/1024 ) + " K");       accelMemory = mem;     }

A typical run of ImagesTests produces the following stream of messages edited to emphasize the memory related prints:

     DirectDraw surfaces constrained to use vram     Initial Acc. Mem.: 179.6 MB     Reading file: Images/imsInfo.txt         // many information lines printed by the loader         0. Acc. Mem: 179.6 MB; Change: -1464.8 K     1. Acc. Mem: 178.1 MB; Change: -115.5 K     3. Acc. Mem: 178.0 MB; Change: -113.2 K     4. Acc. Mem: 177.9 MB; Change: -16.3 K     5. Acc. Mem: 177.9 MB; Change: -176.8 K     numbers sequence has ended     6. Acc. Mem: 177.7 MB; Change: -339.0 K     7. Acc. Mem: 177.4 MB; Change: -99.0 K          // 9 similar accelerated memory lines edited out     18. Acc. Mem: 176.6 MB; Change: -16.2 K     19. Acc. Mem: 176.6 MB; Change: -93.9 K     21. Acc. Mem: 176.5 MB; Change: -48.8 K     25. Acc. Mem: 176.4 MB; Change: -60.0 K     numbers sequence has ended     numbers sequence has ended     numbers sequence has ended     numbers sequence has ended          // etc.

The images use about 120 K in total and appear to be moved into VRAM at load time, together with space for other rendering tasks (see line number 0). The large additional allocation is probably caused by Swing, which uses VolatileImage for its double buffering.

The later VRAM allocations are due to the rendering carried out by the visual effect operations, and they stop occurring after the counter reaches 25 (or thereabouts). Since each loop takes about 0.1 seconds, this means that new VRAM allocations cease after about 2.5 seconds. VRAM isn't claimed in every animation loop; for instance, no VRAM change is reported when the counter is 20, 22, and 24.

This behavior can be understood by considering how the visual effects methods behave. Typically, about every few animation frames, they generate new images based on the original o images. The operations are cyclic, i.e., after a certain number of frames they start over. The longest running cyclic is the fade method, which completes one cycle after 25 frames (2.5 seconds). Some of the operations write directly to the screen, and so will not require additional VRAM; others use temporary BufferedImage variables. These will probably trigger the VRAM allocations. Once these claims have been granted, the space can be reused by the JVM when the methods restart their image processing cycle.

Consider if the ddforcevram flag is commented out from main( ) in ImagesTests:

     // System.setProperty("sun.java2d.ddforcevram", "true");

Only the first reduction to VRAM occurs (of about 1.4 MB), and the subsequent requests are never made. In this case, the benefits of using the flag are fairly minimal, but its utility depends on the mix of graphics operations used in the application.

More information can be obtained about the low-level workings of Java 2D by turning on logging:

     java -Dsun.java2d.trace=log,count,out:log.txt ImagesTests

This will record all the internal calls made by Java 2D, together with a count of the calls, to the text file log.txt. Unfortunately, the sheer volume of data can be overwhelming. However, if only the call counts are recorded, then the data will be more manageable:

     java -Dsun.java2d.trace=count,out:log.txt ImagesTests

The vast majority of the calls, about 92 percent, are software rendering operations for drawing filled blocks of color (the MaskFill( ) function). The percentage of hardware-assisted copies (blits) is greater when the ddforcevram flag is switched on. These operations have "Win32," "DD," or "D3D" in their names. Nevertheless, the percentage increases from a paltry 0.5 percent to 2.3 percent.

The comparatively few hardware-based operations in the log is a reflection of Java's lack of support for image processing operations in Windows. Undoubtedly, this will improve in future versions of the JVM and depends on the mix of operations that an application utilizes. It may be worth moving the application to FSEM since VolatileImages are automatically utilized for page flipping in FSEM.



Killer Game Programming in Java
Killer Game Programming in Java
ISBN: 0596007302
EAN: 2147483647
Year: 2006
Pages: 340

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net