Transforming Sprites

Now we are going to play with a fun hardware aspect of the sprites. The sprites (and sometimes backgrounds!) have a special feature that allows the pixel coordinates to be transformed by a 2x2 matrix (affine matrix).

The affine matrix can be used to rotate, scale, and/or even skew the sprite image.

We are going to take advantage of this hardware feature to squish the ball when it hits the ground.

The Affine Matrix

The affine matrix is composed of four values: PA, PB, PC, and PD. For DS (and GBA), The values are in 8.8 fixed point format.

The DS uses the affine matrix to transform pixel coordinates.

For each pixel of the sprite, the image coordinate is calculated with the formulas:

u = x * PA + y * PB + width / 2
v = x * PC + y * PD + height / 2

Where x and y are the distance from the center of the sprite. u and v select a pixel from the OBJ vram source.

There are a few basic types of matrices, explained below.

Identity

The above diagram shows an identity matrix. If we use this matrix then the u and v coordinates will be equal to the x and y coordinates, thus not affecting the image output from the source.

Transformation by Identity Matrix (no change)

Rotation

Another basic type of matrix is the rotation matrix.

2x2 Rotation Matrix

In the above diagram, a represents the desired angle. For a practical application, cos and sin can be done with a LUT (look-up table/array containing a bunch of precalculated values) containing 8.8 fixed point values.

When using this kind of matrix, the image will appear rotated by the angle specified.

Notice the parts of the image that reach outside of the red boundary, this is a problem that needs to be checked when transforming sprites (explained along with scaling).

Scaling

This is the one we're going to use, we want to scale the ball by some amount to make it look cooler when it bounces. x_scale/y_scale are the factors that will be multiplied by the x/y coordinates to get the scaled u/v coordinates.

This is the ball in the air...


We will scale [squish] it when it touches the ground.

I know it makes the ball look a bit deformed, but the image will only be seen for a single frame.

Scaling the Ball

Now let's apply this stuff to our bouncy ball. The first thing we are going to do is...allow the ball to fall somewhat past the platform level.

We then measure the vertical distance between the platform and the top of the ball. This is our height. When the ball is touching the platform, we will scale the height of the ball to this difference, so the ball appears squished against the platform.

With a height value, we can also calculate a width value from it to preserve the area of the ball. We can do this with the formula:

width = diameter2 / height

To translate this width value to a value for PA (x_scale) we do:

PA = diameter / width

For y_scale, it's:

PD = diameter / height

Let's start by editing ballUpdate to let the ball fall somewhat past the platform before it bounces. Here's the old code:

    if( b->y + c_radius >= c_platform_level )
    {
        // apply ground friction to X velocity
        b->xvel = (b->xvel * (256-c_ground_friction)) >> 8;
        
        // mount Y on platform
        b->y = c_platform_level - c_radius;
            
        // negate Y velocity, also apply the bounce damper
        b->yvel = -(b->yvel * (256-c_bounce_damper)) >> 8;
        
        // clamp Y to mininum velocity (minimum after bouncing, so the ball does not settle)
        if( b->yvel > -min_yvel )
            b->yvel = -min_yvel;
    }

We will modify it to only bounce when the ball reaches the point where it's past minimum height (min_height, as defined earlier). We also calculate the height value, if the ball isn't touching the platform, we will specify the regular height (diameter). height is in *.8 fixed point.

    if( b->y + c_radius >= c_platform_level )
    {
        // apply ground friction to X velocity
        // (yes this may be done multiple times)
        b->xvel = (b->xvel * (256-c_ground_friction)) >> 8;
        
        // check if the ball has been squished to minimum height
        if( b->y > c_platform_level - min_height )
        {
            // mount Y on platform
            b->y = c_platform_level - min_height;
            
            // negate Y velocity, also apply the bounce damper
            b->yvel = -(b->yvel * (256-c_bounce_damper)) >> 8;
        
            // clamp Y to mininum velocity (minimum after bouncing, so the ball does not settle)
            if( b->yvel > -min_yvel )
                b->yvel = -min_yvel;
        }
       
        // calculate the height
        b->height = (c_platform_level - b->y) * 2;
    }
    else
    {
        b->height = c_diam << 8;
    }

Now let's modify our ballRender code.

What we want to do is modify the routine to setup an affine matrix containing the scaling parameters from the height value. We must understand one more concept before we do this though...

Have a look at the picture below, it's the ball squished to 50% height, it also shows the sprite's rendering box! The rendering box's top left corner is the sprite's X and Y offsets.

For our ball, the rendering region is 16x16. We have a problem here, when the ball gets squished, the width of the ball extends past the rendering box. The sprite will be rendered somewhat like this:

Thankfully, there is an easy solution to this -- the double-size flag (in attribute0).

The advantage of the double-size flag is that the sprite's rendering size will be doubled, giving you twice the space to render the object. The disadvantage is that the sprite will consume double the amount of rendering cycles.

Now we will be able to squish the ball to half it's height. Note that the X/Y coordinates of the sprite will change. (subtract width/2!)

Let's write the code finally. First thing we want to change is the coordinates, we must subtract c_radius * 2 instead of * 1 to compensate for the double-sized sprite.

    int x, y;
    x = ((b->x - c_radius * 2) >> 8) - camera_x;
    y = ((b->y - c_radius * 2) >> 8) - camera_y;

Now enable rotation/scaling with the double-size flag in attribute0.

    sprite[0] = (y & 255) | ATTR0_ROTSCALE_DOUBLE;

In rot/scale mode, attribute1 changes a bit, there are no more X/Y flip flags. Instead, there is a 5 bit affine matrix selection. Select the affine matrix specified in the ball structure.

    sprite[1] = (x & 511) | ATTR1_SIZE_16 | ATTR1_ROTDATA( b->sprite_affine_index );

Now we can setup the affine matrix values. As mentioned before, the data is interleaved into the sprite parameters with every 4th Hword.

Make a pointer to the affine matrix we want to modify.

    u16* affine;
    affine = OAM + b->sprite_affine_index * 16 + 3;

We multiply the affine selection by 16 (Hwords) and add 3 to point to PA. affine[0] = PA, affine[4] = PB, affine[8] = PC, affine[12] = PD.

The scale matrix will not use PB and PC, we will clear them to zero.

    affine[4] = 0;
    affine[8] = 0;

Now we have a bit of fixed point math to deal with. The affine parameters are in 8.8 fixed point, our height value is in *.8 too (* means a sane amount).

We must give PA our horizontal scaling value. As mentioned above the formula is:

PA = diameter / width

To get width, we do:

width = d2 / height

To do this in fixed point, we must shift d2 left by 16 bits. Then, when we divide, we will end up with a *.8 value.

Hey wait a minute, this is too much math, we can simplify the PA operation a bit.

If we replace width with the width formula, we see the following:

PA = d / (d2 / h)

We do a little bit of math to it and end up with:

PA = h / d

Now, as mentioned before, divides can be a little slow for the DS. Since d is a constant value, we can flip this operation into a multiplication problem.

PA = h * (1/d)

In fixed point, we will scale 1/d by shifting the value left 16 bits. We will then shift the result right 16 bits, this gives us much much better precision.

    int pa = (b->height * (65536/c_diam)) >> 16;

The format is in the correct 8.8 fixed point. The compiler will optimize the 65536/constant into a single value. We need the PD value too, which is:

PD = diameter / height

We already have height / diameter, so we just need the reciprocal.

    int pd = 65536 / pa;

65536 is *.16 fixed point, we divide by *.8 fixed point, we get the correct *.8 fixed point value.

Load the parameters into the affine matrix.

    affine[0] = pa;
    affine[12] = pd;

That should do it! Compile and run the game, you should see a much better boucing effect!

Previous: The bouncy ballContentsNext: Boingy sound