Sol's Graphics Tutorial

12 - Infinite Sprites

Here's another effect that originated on the Amiga. And again, I wasn't there. The original name for the effect was "infinite bobs", where bob is a "blitter object". Like the previous effect, it's based on reusing buffers. But instead of shrinking our framebuffer, we use several framebuffers.

I don't know how many framebuffers the original Amiga effect used, but we'll probably use quite bit more.

The basic idea is to cycle through a bunch of framebuffers without clearing them. Everything we draw on a framebuffer will remain there, and be seen again once we cycle back to it. We only need to draw one sprite per frame, but once we've cycled through a few times, the user will see multiple copies of the sprite moving on the screen.

Take a copy of the previous chapter, resulting in ch12.

Let's start again with a picture (right-click, save as), and a pointer after the includes:

int *gBall;

and load it in the init() function:

void init()
{
  int x, y, n;
  gBall = (int*)stbi_load("ball.png", &x, &y, &n, 4);
}

(we started using stb_image in chapter 7)

Then we need a routine to draw the ball. Replace the render() function with the following:

void drawball(int x, int y)
{
  for (int i = 0; i < 64; i++)
    for (int j = 0; j < 64; j++)
      if (gBall[i * 64 + j] != 0xff000000)
        gFrameBuffer[x + j + (y + i) * WINDOW_WIDTH] = gBall[i * 64 + j];
}

void render(Uint64 aTicks)
{
  for (int i = 0; i < WINDOW_WIDTH * WINDOW_HEIGHT; i++)
    gFrameBuffer[i] = 0xff000000;
    
  drawball(20, 20);
}

Compile and run. You should see the ball near the top left corner. Let's animate the ball. Replace the drawball call in render with:

int xpos = 
    sin(aTicks * 0.0006) *
    sin(aTicks * 0.00115) *
    (WINDOW_WIDTH - 128) / 2 +
    WINDOW_WIDTH / 2 - 32;

int ypos = 
    cos(aTicks * 0.0007) *
    sin(aTicks * 0.00125) *
    (WINDOW_HEIGHT - 128) / 2 +
    WINDOW_HEIGHT / 2 - 32;

drawball(xpos, ypos);

Compile and run. That ought to be chaotic enough. As always, the magic numbers don't have any specific significance, feel free to play with them and see what changes.

Next we'll need a pile of framebuffers. First we need a pointer, add it after our gBall definition up after includes. Let's also add a frame counter variable:

int* gFrameBufferPile;
int gFrame = 0;

Then, add the following to the init() function (order doesn't matter, but let's say to the end of the function):

gFrameBufferPile = new int[WINDOW_WIDTH * WINDOW_HEIGHT * 64];
for (int i = 0; i < WINDOW_WIDTH * WINDOW_HEIGHT * 64; i++)
  gFrameBufferPile[i] = 0xff000000;

Yet again, if we didn't need to care about alpha, this would be so much easier.

Finally, let's put the pile to work. Replace the framebuffer-clearing code in render() with:

gFrame = (gFrame + 1) % 64;
gFrameBuffer = gFrameBufferPile + WINDOW_WIDTH * WINDOW_HEIGHT * gFrame;

Yes, this causes us to leak the original gFrameBuffer memory, but we haven't really been paying too much attention to details like that so far in any case.

Compile and run.

It looks nice, but it gets quite busy before long. Let's add some color. Change the drawball() function to this:

void drawball(int x, int y, int c)
{
  for (int i = 0; i < 64; i++)
    for (int j = 0; j < 64; j++)
      if (gBall[i * 64 + j] != 0xff000000)
        gFrameBuffer[x + j + (y + i) * WINDOW_WIDTH] = 
          blend_mul(gBall[i * 64 + j], c) | 0xff000000;
}

(if you don't have blend_mul, it was introduced in 05 - Blending a Bit)

And the call to drawball in render() changes to:

drawball(xpos, ypos,
    ((int)(sin(aTicks * 0.0001 + 0 * PI * 2 / 3) * 127 + 128) << 16) |
    ((int)(sin(aTicks * 0.0001 + 1 * PI * 2 / 3) * 127 + 128) << 8) |
    ((int)(sin(aTicks * 0.0001 + 2 * PI * 2 / 3) * 127 + 128) << 0));

And we're done. The color calculation simply oscillates R, G and B values so that when one of the components is high, the two others are (relatively) low.

Other things to try:

  • Play around with the ball's path
  • Draw several balls with different curves
  • Add clipping to the ball-drawing routine
  • Add scaling to the ball-drawing routine
  • Make the new balls cast a shadow on old ones
  • Figure out how to only have, say, 1024 sprites this way

Next up: 13 - Simple Reflections

Any comments etc. can be emailed to me.