Sol's Graphics for Beginners

(ch18.cpp)

(prebuilt win32 exe)

18 - Bumping Along

In ancient history (of the 1960's), well before I was born, people like Gouraud and Phong invented most of the basic computer graphics algorithms. What we know as 'gouraud shading' is linear interpolation of light values over a surface. In gouraud shading you only need normal vectors for each vertex. These normal vectors were then used to calculate lighting. Phong went one step further and 'phong shading' is linear interpolation of normal vectors over the surface.

Bump mapping is an extension to phong shading, where the normal vectors are distorted according to a 'bump map', just like the color of the surface can be changed with a 'texture map'. Both texture and bump maps can be used to make relatively simple geometry look very detailed and complex.

We'll do bump mapping in 2d by calculating a lookup table which will then be used to distort our 'light map'. Thus, we're 'faking' bump mapping. On the other hand, bump mapping is a 'fake' effect to begin with..

We'll start from the chapter 17 skeleton again. You'll also need to download (right-click, save as) the picture, the light texture and the heightmap.

There's fairly little new in this tutorial, but let's go through things step by step nevertheless.

First, make a copy of some older project and copy the skeleton code over the source.

Next, go to 05 - Blending a Bit and copy-paste the 'blend_add()' function above the 'render()' function.

Next, we'll load the images. Add the following after the screen surface definition, near top of the file:

// Picture surface
SDL_Surface *gPicture;
// Heightmap surface
SDL_Surface *gHeightmap;
// Texture surface
SDL_Surface *gTexture;

and the loading code in init():

SDL_Surface *temp = SDL_LoadBMP("picture18.bmp");
gPicture = SDL_ConvertSurface(temp, gScreen->format, SDL_SWSURFACE);
SDL_FreeSurface(temp);

temp = SDL_LoadBMP("heightmap18.bmp");
gHeightmap = SDL_ConvertSurface(temp, gScreen->format, SDL_SWSURFACE);
SDL_FreeSurface(temp);

temp = SDL_LoadBMP("texture18.bmp");
gTexture = SDL_ConvertSurface(temp, gScreen->format, SDL_SWSURFACE);
SDL_FreeSurface(temp);

Let's start by just drawing the image. Find the 'rendering here' comment in render(), and paste the following below it:

if (SDL_MUSTLOCK(gPicture))
  if (SDL_LockSurface(gPicture) < 0) 
    return;

int i, j;
for (i = 0; i < HEIGHT; i++)
{
  for (j = 0; j < WIDTH; j++)
  {
    ((unsigned int*)gScreen->pixels)[(j) + (i) * PITCH] = 
      ((unsigned int*)gPicture->pixels)[(j) + (i) * (gPicture->pitch / 4)];
  }
}

if (SDL_MUSTLOCK(gPicture)) 
  SDL_UnlockSurface(gPicture);

When you compile and run, you should get the picture in the window.

Let's alter the rendering a bit to render the 'light' on top of it. Copy-paste the following over the last thing that you pasted in:

if (SDL_MUSTLOCK(gPicture))
  if (SDL_LockSurface(gPicture) < 0) 
    return;

if (SDL_MUSTLOCK(gTexture))
  if (SDL_LockSurface(gTexture) < 0) 
    return;

int i, j;
for (i = 0; i < HEIGHT; i++)
{
  for (j = 0; j < WIDTH; j++)
  {
    int u = j;
    int v = i;
    if (v < 0 || v >= gTexture->w ||
        u < 0 || u >= gTexture->h)
    {
      ((unsigned int*)gScreen->pixels)[(j) + (i) * PITCH] = 
        ((unsigned int*)gPicture->pixels)[(j) + (i) * (gPicture->pitch / 4)];
    }
    else
    {
      ((unsigned int*)gScreen->pixels)[(j) + (i) * PITCH] = 
        blend_add(
        ((unsigned int*)gPicture->pixels)[(j) + (i) * (gPicture->pitch / 4)],
        ((unsigned int*)gTexture->pixels)[(u) + (v) * (gTexture->pitch / 4)]);
    }
  }
}

if (SDL_MUSTLOCK(gTexture)) 
  SDL_UnlockSurface(gTexture);

if (SDL_MUSTLOCK(gPicture)) 
  SDL_UnlockSurface(gPicture);

Here we're making 'u' and 'v' the x- and y-coordinates of the light texture. If the u and v coordinates are outside the bounds of the texture, we're plotting the image as is. If we're inside the texture, we add the texture to the picture before plotting.

There are reasons why we're doing things in this slightly awkward way. Let's modify things a bit. Change the 'int u..' and 'int v..' lines to the following:

int u = j + (int)(sin((tick + i * 5) * 0.01234987) * 7);
int v = i + (int)(sin((tick + j * 5) * 0.01254987) * 7);

Compile and run. We're distorting the texture with the couple 'sin' values. Feel free to play with the values to see what happens.

The bad side of an effect like this is that it's difficult to predict when clipping is needed, and thus we're forced to check whether we're on the map at each pixel. One alternative would be to use a huge texture, or a wrapping one. In small resolutions this might be feasible.

Our eventual goal is to distort the light based on the height map.

First, let's add the lookup table, near the top of the file, after the definition of surfaces:

// Lookup table
short *gLut;

Next, paste the following in the init() function, after the surfaces are loaded:

int i,j;
gLut = new short[WIDTH * HEIGHT];

if (SDL_MUSTLOCK(gHeightmap))
  if (SDL_LockSurface(gHeightmap) < 0) 
    return;

for (i = 0; i < HEIGHT; i++)
{
  for (j = 0; j < WIDTH; j++)
  {
    gLut[i * WIDTH + j] = 
      ((unsigned int*)gHeightmap->pixels)[j + i * (gHeightmap->pitch / 4)] & 0xff;
  }
}

if (SDL_MUSTLOCK(gHeightmap)) 
  SDL_UnlockSurface(gHeightmap);

Here we allocate the lookup table and then copy 8-bit values from the heightmap image to it.

Next, change the 'int u..' and 'int v..' lines in render() to:

int u = j + gLut[i * WIDTH + j];
int v = i + gLut[i * WIDTH + j];

When you run, you'll see that the light is distorted based on the heightmap. It's still a bit far from what we'll want eventually, but let's animate the light so you'll see what is happening.

Add the following lines before the for() loops in render():

int posx = (int)((sin(tick * 0.000645234) + 1) * WIDTH / 4);
int posy = (int)((sin(tick * 0.000445234) + 1) * HEIGHT / 4);

Substract posx from u and posy from v. Compile and run.

We'll need to adjust the lookup table a bit to reach the effect we're after. We'll want to distort the image both horizontally and vertically depending on the slopes of the heightfield image. Change the lookup table calculation to look like this:

int i,j;
gLut = new short[WIDTH * HEIGHT];
memset(gLut, 0, sizeof(short) * WIDTH * HEIGHT);

if (SDL_MUSTLOCK(gHeightmap))
  if (SDL_LockSurface(gHeightmap) < 0) 
    return;

for (i = 1; i < HEIGHT - 1; i++)
{
  for (j = 1; j < WIDTH - 1; j++)
  {
    int ydiff = 
      ((((unsigned int*)gHeightmap->pixels)[j + (i - 1) * 
                                            (gHeightmap->pitch / 4)] & 0xff) -
        (((unsigned int*)gHeightmap->pixels)[j + (i + 1) * 
                                            (gHeightmap->pitch / 4)] & 0xff));
    int xdiff = 
      ((((unsigned int*)gHeightmap->pixels)[j - 1 + i * 
                                            (gHeightmap->pitch / 4)] & 0xff) -
        (((unsigned int*)gHeightmap->pixels)[j + 1 + i * 
                                            (gHeightmap->pitch / 4)] & 0xff));

    gLut[i * WIDTH + j] = ((ydiff & 0xff) << 8) | (xdiff & 0xff);
  }
}

if (SDL_MUSTLOCK(gHeightmap)) 
  SDL_UnlockSurface(gHeightmap);

So, what's changed? First off, inside the loops we're calculating x and y slopes for each pixel by substracting the next pixel value from the last one. Then, we're combining the two values into one 16-bit value.

Since we're reading the previous and next pixel, we must be careful not to read outside the image. The for loops have been changed to start from the second pixel and to stop before the last pixel. The borders of our lookup table will be left uninitialized, so we need to add a memset to clear all the values to zero before we begin.

Note that the gLut table now contains two 8-bit values. Unlike with the 2d tunnel, both of the values are signed - the result from the substraction can come out as negative. We'll have to be careful to keep the values signed when we're reading the lookup table. To make things safer, we could have defined the lookup table as an array of two signed 8-bit values.

In render() the 'int u..' and 'int v..' lines change to:

int u = j + ((signed char)gLut[i * WIDTH + j]) - posx;
int v = i + (gLut[i * WIDTH + j] / 256) - posy;

Compile and run.

Note that we're taking special care to preserve the 'signedness' of our values. The bottom 8 bits are handled by casting the value to signed char, which we expect to be a 8-bit value. The top 8 bits are handled by dividing the value by 256, which has the same effect as a signed shift by 8 bits would have. Since the C standard does not guarantee the shifts to be signed or unsigned, we're using a division here and hoping that the compiler is smart enough to convert it to a signed shift.

Additional things to try:

  • Try different heightmaps.
  • Try different kinds of light maps.
  • Try different blending types.
  • Remove the image; leave just a black background.
  • Add mouse control, and control the light source with the mouse.

Next up is an even simpler effect that we'll use for 19 - Distorted Transitions..

Having problems? Improvement ideas? Just want to discuss this tutorial? Try the forums!

Any comments etc. can be emailed to me.