Graphic Animation Basics

Any graphics animation project is based on a small variation of the general main loop used in all embedded control projects. In this loop you always start with a clean screen and "paint" a scene based on the current status and position of a number of "objects". Then you wait a little to give the user a chance to see it, most often you simply calculate the new position of the objects during this time and then repeat.


while( TRUE)

{

    //1. clear the screen

    //2. paint objects

    //3. calculate new objects position (wait a little) 

}


The problem with this simple sequence is that step 3, no matter how fast your erase routine is, will produce a noticeable flicker. In the best case, for a fraction of a second your eyes might perceive a slight variation in the color/intensity of the picture and in the worst case (a large screen or a complex image that takes a while to be re-painted) a partial image can be seen forming on the screen.


Double Buffering

The obvious solution to the problem is to use a second image buffer. A place where the image can be painted (as slowly as needed) away from our eyes and then, once completed, it can be transferred directly "on top" of the (visible) screen.

The animation loop becames now:


while( TRUE)

{

    //1. clear the buffer

    //2. paint objects (on the buffer)

    //3. transfer the buffer image onto the screen

    //4. calculate new objects position (wait a little) 

}


At no point now an empty or partial image is visible to our eyes.


This is very easily done provided you do have the space for this second buffer.

For example, in the case of the PIC32MX4 MMB board, a QVGA 16-bit color buffer would require as much as (320x240x2) 150Kbytes of RAM!

This is clearly too much to ask from the little PIC32MX4 (32K RAM) but even the relatively larger PIC32MX7 (with only 128K RAM total).

So we have to come to some kind of a compromise to reduce the animation buffer size to a more reasonable value that would not only fit in the device RAM but also leave some room for the application data.

There are two ways this can be accomplished:

  1. Reducing the screen size (i.e. animating only a window not the entire screen)
  2. Reducing the color depth (using a palette of colors instead of the full 16-bit offered by the TFT panel)

Both compromises can be quite acceptable considering that the animation is NOT expected to produce photo realistic images as is the case of most games and or GUI apps.


Buffered.c Module

Looking at the lowest common denominator (the PIC32MX4 Micromedia boards) I will present here a simple graphic library that provides a (double) buffered 4 -bit color depth in a 256*200 pixel window, that is centered on the QVGA screen.


The area of the screen surrounding the animated window can still be accessed directly using the MAL Graphics lib (btw this example is based on the 2.0 version)

This is how the animation buffer is defined:


static char V[ VRES*HRES/2]; // image buffer (4-bit color)


With VRES and HRES set correspondingly to 200 and 256, the resulting buffer V[] is only 25K bytes large, which leaves approx 7K bytes of RAM space for the application data and stack on the smallest Micromedia boards or a whopping 100+ Kbytes on the larger MX7 models.


Drawing on the buffer (instead of the main screen) requires only a few new functions to replace those provided by the MAL display driver.


void copyV( void)
{
    int y;
    char *pV;
      
    // refresh the main screen
    CS_LAT_BIT = 0;
    SetAddress( 0, 0);
    RS_LAT_BIT = 1;            
    pV = &V[0];
    for( y=0; y<VRES*HRES/2; y++, pV++)
    {       
        WriteData( LUT[ (*pV) >> 4]);
        //     PMDIN1 = data;             
        WriteData( LUT[ (*pV) & 0xf]);  
        //    PMDIN1 = data;            
    }   
    CS_LAT_BIT = 1;
   
    // post a message, frame completed and update TimerX
    fFrame = 1;
    TimerX++;
   
} // copyV

void gClearScreen( void)
{ // fill with zeros the Active Video array
    memset( V, 0, VRES * HRES/2);

    // reset text cursor position
    cx = cy = 0;

} // gClearScreen



void initVideo( void)
{
     gClearScreen();
     InitGraph();

     // define HRES x VRES window
     SetReg(0x04,(HRES-1)>>8);
     SetReg(0x05,(HRES-1));
     SetReg(0x08,0x00);
     SetReg(0x09,(VRES-1));

     copyV();
     BacklightOn();

} // initVideo


The function copyV() takes care of copying the buffer (once complete) to the visible screen (window), while gClearScreen clears the buffer V[] content. 

The initVideo() function takes care of initializing the MAL Graphics lib (for your convenience) and defines the centered window. Notice this function uses the SetReg() primitives specific of the MAL Graphics lib (2.0) to access directly the registers of the HX8347 display driver. This means that the function is very specific to the Micromedia boards and if you will want to port it to other architectures, you will have to modify those registers definitions to match the display driver of your choice!


Finally let's define a new function to plot a single pixel onto the buffer:


void gPlotV( int x, int y)
{
    char *p;
   
    // clipped negative
    if ( (x < clipmx) || (y < clipmy))
        return;
    if ((x < clipx) && (y < clipy) )
//        uPlotV( x, y);
    {
        y = VRES-1-y;
        p = &V[ HRES/2 * y + (x>>1)];
        if (x&1)
            *p = (*p & 0xf0) | gColor;
        else
            *p = (*p & 0x0f) | (gColor << 4);
           
    } //uPlot   
} // plot

Notice that similarly to the MAL Graphics primitives, a color must be assigned before the call to the global variable gColor. This is going to be a 4-bit value that will then be translated to an actual 16-bit color on the screen during the copyV() process via a Look Up Table.


static WORD LUT[ 16] = {
    //0      1      2     3    4    5    6    7    8      
    BLACK, BRIGHTRED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE,
    // 9      A    B      C    D     E   F
    BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE
    };


You can modify the assigned colors to match your preferences of course.


Clipping

Drawing lines is a fun problem that we have already analyzed in chapter 12 of the book, but here we will take the liberty of introducing one more feature to the basic (Bresenham) algorithm: clipping, to improve performance (while drawing on the animation buffer) and to keep the buffer safe!

Yes, there is a safety issue here. If for any reason the application should try to access a point outside the boundaries of the animated window, we don't want the plot function to try and access a memory location outside the boundaries of the V[] array, which would produce a memory corruption (bug) potentially hard to catch, or  a memory access exception. So clipping we must in each call to the gPlotV() function using the clipx, clipmx horizontal boundaries and the clipy and clipmy vertical boundaries.

In theory we could rely on gPlotV[] to take care entirely of the problem, but to keep an eye on the application performance I added clipping control to the gLine() function too. This can save a lot of CPU cycles when long lines are painted across the buffer by a 3D geometry engine for example...


3D Geometry

Adding a little bit of geometry means adding a third dimension to your graphics animation projects. There are three elements to 3D graphics efficiency:

  1. Use 16-bit fixed point math
  2. Use discrete approximations for common trigonometric functions (sin, cos)
  3. Basic matrix manipulation

As we have seen in chapter 4 of the book, integer math is MUCH faster than floating point math, so whenever we can we should steer clear of the latter. 3D geometry is one such case where, at least in the demo applications that I will present below, fixed point 16-bit math is going to do a great job.

I chose to use an 8.8 format which means that each value is actually scaled by a factor of 256 (8 bit shift to the left) to represent it as a 16-bit integer. After each calculation all I need to do is shift back to the right (divide by 256) by 8 positions to obtain the actual value from the integer used to store it. Also remember to divide by 256 the result of each multiplication to keep all values in the same scale.

Trigonometry is at the root of most of the 3D  transformation involving a rotation of sorts and while polynomial series approximations are available in the PIC32 math library for all trig functions, using a table look up and optionally some interpolation can increase significantly the performance of the application as long as you don't need a precision in excess of 1 degree.

Matrix manipulation is not absolutely required, but it sure can make the math behind most 3D transformations (rotations, scaling, translations...) very compact. 

For this purpose I have included in each of the projects below a small  library module (geometryX.c) to take care of the 3D animation needs.

Here is an example of the initWorld() function used to define the rotation and translation matrix to be applied to an object to place it into a chose 3D point in space (x,y,z) with orientation (alpha, beta, gamma).

void initWorld( short alpha, short beta, short gamma,
                int x0, int y0, int z0)

{
    m[0][0] = (+fcos( gamma) * fcos( beta)) >>8;
    m[0][1] = (-fsin( gamma) * fcos( beta)) >>8;
    m[0][2] = fsin( beta);
    m[0][3] = x0 << 8;
    m[1][0] = ((fcos( gamma) * fsin( beta) * fsin( alpha)) >>16) + ((fsin( gamma)* fcos(alpha)) >>8);
    m[1][1] = ((fcos( gamma) * fcos( alpha)) >>8) - ((fsin( gamma) * fsin(beta) * fsin(alpha)) >>16);
    m[1][2] = (-fcos( beta) * fsin( alpha)) >>8;
    m[1][3] = y0 << 8;
    m[2][0] = ((fsin( gamma) * fsin( alpha)) >>8) - ((fcos(gamma) * fsin( beta) * fcos(alpha)) >>16);
    m[2][1] = ((fsin( gamma) * fsin( beta) * fcos( alpha)) >>16) + ((fsin( alpha) * fcos( gamma)) >>8);
    m[2][2] = (fcos( beta) * fcos( alpha)) >>8;
    m[2][3] = z0 << 8;
    m[3][0] = 0;
    m[3][1] = 0;
    m[3][2] = 0;
    m[3][3] = 1 << 8;

} // init World


Demo Projects Source Code

The first demo project ("Cubes") that will use geometry and double buffering is actually offering three demo modes (see instructions in the header):
  1. A simple cube (wire frame) is rotated in space
  2. 32 cubes (actually 38) are rotated in space while forming the number '32'
  3. A string is represented using small cubes and then each character is rotated independently

Cubes.zip


The second demo is adding inputs from the onboard accelerometer to rotate in space another 2-character object generated assembly (wire frame cubes)

CubesAcc.zip


The third demo adds surface filling by using a simple algorithm to split each polygon in a sum of triangles. Each triangle is then filled using a fast fill algorithm.

CubeFill.zip

The final demo assembles a Rubik cube and allows you to manipulate it using the MMB joystick. It takes all the code developed in the previous demos integrating animation, 3D geometry and optimal triangle filling.

Rubik.zip