Roland's Cross-Fade Code

This page last updated February 9, 1997.

These routines are taken from my game in progress, and they implement what is called a "cross-fade" in demos. Everything here has been tested and works.

A cross-fade is when one image smoothly changes to another without transitional tricks like stippling. It's the kind of fade you'd see on a TV program. Say that we have a two-color source image that looks like this:

+----------------------------------------+
|                                        |
|                Color 1                 |
|----------------------------------------|
|                                        |
|                Color 2                 |
+----------------------------------------+
We want to cross-fade it into the 3-color destination image, which looks like this:

+------------------+-----------+---------+
|                  |           |         |
|                  |           |         |
|  Color 3         |  Color 4  | Color 5 |
|                  |           |         |
|                  |           |         |
+------------------+-----------+---------+
To do so, we must make a new, composite bitmap, which looks like this:

+------------------+-----------+---------+
|                  |           |         |
|  Color 20        | Color 22  | Color 24|
|------------------+-----------+---------+
|                  |           |         |
|  Color 21        | Color 23  | Color 25|
+------------------+-----------+---------+

Colors 20-25 are assumed to be "scratch" colors, not used anywhere else on the screen at the time. Notice that by setting the scratch colors to the proper values, we can make this composite bitmap look like either of the two simpler bitmaps. Thus, we initially set the scratch colors up so that the composite bitmap looks like the first bitmap, then we shift the colors until it looks like the second bitmap.

The main reason this technique isn't used much is because it can take too many scratch colors. The number of scratch colors required is the number of colors in the source bitmap times the number of colors in the destination bitmap. So two 16-color pictures would require 256 scratch colors. (Actually, it doesn't always require this many -- it depends upon the particular images -- but not having enough would spoil the effect.)

In my game, objects can fade either in or out, basically becoming more or less transparent. Some of the view class (display manager) routines manage a color allocation system. Part of the palette contains the "basic colors". These are colors that are used by the game's PCX graphics. They don't change, or they change in some expected way like fading between red and orange. The other part of the palette contains the "scratch colors", which can be allocated and deallocated as necessary. The routines allocate/deallocate ranges of colors, and for simplicity they don't handle wraparound past color 255. A #define is used to set the highest basic color.

The fade routines work by determining how many colors are in the sprite and how many colors are in the background and generating a bitmap that can be faded from one to the other. If you're careful to keep the number of colors in the sprite down, you won't need 256+ colors to do a single fade. One caveat with my system is that the number of colors required for the fade is determined when the fade is first started, and only then. If you're doing cel-based animation, in addition to using the "dynamic" fade, you must make sure that the cel you start the fade with contains all the colors in all the other cels. This could be kind of awkward in some situations, but it hasn't affected me enough to become a problem.

In a "static" fade, the fade bitmap is generated only when the fade is first started. The sprite and background images must not change, and they must not move relative to one another. In a "dynamic" fade, the fade bitmap is generated every frame, which is slower, but means that the sprite can move around the screen.

The background, by the way, includes not just the playfield but also the other sprites which are "below" the fading one on the screen.

The number of scratch colors required for the fade is the number of basic colors times the number of colors in the sprite. (Remember that color 0 counts as a color in the background.) In my game I have about 20 basic colors, and maybe 3-4 per sprite. So that's 60-80 colors per fade, and there could 2 or 3 fades going on at once. Whether or not that's enough depends on what you're doing. I intended this as a special effect, not something to be used constantly.

This code was cut down from the full class, so something important may have been snipped accidentally. Drop me a line if you have any questions.

#define HIGHEST_BASIC_COLOR 19

#define MAX_FADES 20

struct fade_struct {
  fade_struct *next_struct;
  anim_system *object;
  unsigned int fade_in;
  fix speed;
  fix percent_done;
  unsigned int static_display;
  unsigned int color_range_start;
  unsigned int colors_used;
  unsigned char color_mapping[HIGHEST_BASIC_COLOR + 1];
};

class view {
public:
  static int color_available[256];
  static fade_struct active_fades_head, free_fades_head;

  static void set_up_class();
  static unsigned int allocate_colors(unsigned int num_colors);
/* Returns the first color in the allocated range, or zero if the allocation
   failed. */
  static void return_colors(unsigned int range_start,
    unsigned int num_colors);
  static void change_palette(unsigned int index, RGB *new_color);
/* Since only a certain number of palette changes are processed each
   interrupt, and they are done in LIFO order, flooding this routine with
   change requests (like fades) can make some changes take a long while to
   get done. */
  static void construct_fade_bitmap(fade_struct *fade);
  static void start_fade(anim_system *object, unsigned int fade_in,
    fix speed, unsigned int static_display = NO);
/* Fades only work between "basic colors". Overlapping two objects that are
   fading will produce unpredictable results. */
/* Fade speed is in parts of 100. A speed of 2.5 will require 40 updates to
   complete. */
/* When start_fade() is called, object->fade_bitmap should be the bitmap to
   fade in or out, and object->sprite should be a buffer area to be used by
   the fade routines. This buffer area will be deallocated by the fade
   routines when it is no longer needed. */
/* IMPORTANT: The colors used by the fade bitmap are determined when
   start_fade() is called. If this bitmap is changed later, the new bitmap
   cannot contain any extra colors. */
  static unsigned int is_fade_running(anim_system *object);
  static void update_fade();
};

#ifdef COMPILING_VIEW
  int view::color_available[256];
  fade_struct view::active_fades_head, view::free_fades_head;
#endif





void view::set_up_class() {
  unsigned int stepper;
  fade_struct *fade_maker;

  for (stepper = 0;stepper < 256;stepper++)
    color_available[stepper] = YES;
  active_fades_head.next_struct = 0;
  free_fades_head.next_struct = 0;
  for (stepper = MAX_FADES;stepper;stepper--) {
    fade_maker = new fade_struct;
    fade_maker->next_struct = free_fades_head.next_struct;
    free_fades_head.next_struct = fade_maker;
  }
}



void view::shut_down_class() {
  fade_struct *fade_deleter;

  while (active_fades_head.next_struct) {
    fade_deleter = active_fades_head.next_struct;
    active_fades_head.next_struct = fade_deleter->next_struct;
    delete fade_deleter;
  }
  while (free_fades_head.next_struct) {
    fade_deleter = free_fades_head.next_struct;
    free_fades_head.next_struct = fade_deleter->next_struct;
    delete fade_deleter;
  }
}



void view::update() {

// Update any fades in progress.
  update_fade();
}



unsigned int view::allocate_colors(unsigned int num_colors) {
  unsigned int color_step;
  unsigned int range_start;
  unsigned int checks_left;

  color_step = HIGHEST_BASIC_COLOR + 1;
  range_start = color_step;
  checks_left = num_colors;
  do {
    if (color_available[color_step++])
      checks_left--;
    else {
      range_start = color_step;
      checks_left = num_colors;
    }
  } while (checks_left && color_step < 256);
  if (checks_left)
// Not enough colors available.
    return 0;
  else {
    color_step = range_start;
    while (num_colors--)
      color_available[color_step++] = NO;
    return range_start;
  }
}



void view::return_colors(unsigned int range_start, unsigned int num_colors) {

  while (num_colors--)
    color_available[range_start++] = YES;
}



void view::change_palette(unsigned int index, RGB *new_color) {

/* In my game this interfaces with my interrupt routine, but you could put
   anything here. */
}



void view::construct_fade_bitmap(fade_struct *fade) {
  anim_system *object;
  BITMAP *source_bitmap, *dest_bitmap;
  unsigned char *source_line, *dest_line;
  int step_y, step_x;
  unsigned char *color_mapping;

  object = fade->object;
  source_bitmap = object->fade_bitmap;
  dest_bitmap = object->sprite;
  color_mapping = fade->color_mapping;
  if (object->is_physical_coords) {
    playfield::render_world(dest_bitmap, 0, 0, dest_bitmap->w, dest_bitmap->h,
      (object->x_pos - object->hot_spot_x) + view_x_pos,
      (object->y_pos - object->hot_spot_y) + view_y_pos);
    anim_system::draw_objects(dest_bitmap,
      (object->x_pos - object->hot_spot_x) + view_x_pos,
      (object->y_pos - object->hot_spot_y) + view_y_pos, NO, object);
  }
  else {
    playfield::render_world(dest_bitmap, 0, 0, dest_bitmap->w,
      dest_bitmap->h, object->x_pos - object->hot_spot_x,
      object->y_pos - object->hot_spot_y);
    anim_system::draw_objects(dest_bitmap,
      object->x_pos - object->hot_spot_x,
      object->y_pos - object->hot_spot_y, NO, object);
  }
  for (step_y = source_bitmap->h - 1;step_y >= 0;step_y--) {
    source_line = source_bitmap->line[step_y];
    dest_line = dest_bitmap->line[step_y];
    for (step_x = source_bitmap->w - 1;step_x >= 0;step_x--)
      if (source_line[step_x])
        dest_line[step_x] += color_mapping[source_line[step_x]];
  }
}



void view::start_fade(anim_system *object, unsigned int fade_in, fix speed,
    unsigned int static_display) {
  fade_struct *new_fade;
  unsigned int colors_required;
  unsigned int color_range_start;
  unsigned int stepper;
  int step_y, step_x;
  unsigned int color_used[256];
  BITMAP *fade_bitmap;
  unsigned char *line_pointer;

/* The fade fails (the object just appears/disappears) if there are no fade
   structures left or there are insufficient colors available. */
  if (free_fades_head.next_struct) {
// Determine color mapping. We ignore color 0 (since it never fades).
    for (stepper = HIGHEST_BASIC_COLOR;stepper;stepper--)
      color_used[stepper] = NO;
    fade_bitmap = object->fade_bitmap;
    for (step_y = fade_bitmap->h - 1;step_y >= 0;step_y--) {
      line_pointer = fade_bitmap->line[step_y];
      for (step_x = fade_bitmap->w - 1;step_x >= 0;step_x--)
        color_used[line_pointer[step_x]] = YES;
    }
    colors_required = 0;
    for (stepper = HIGHEST_BASIC_COLOR;stepper;stepper--)
      if (color_used[stepper])
        colors_required++;
    colors_required *= HIGHEST_BASIC_COLOR + 1;
    if (colors_required == 0 ||
      (color_range_start = allocate_colors(colors_required)) == 0) {
// Empty bitmap, or not enough colors available.
      destroy_bitmap(object->sprite);
      if (fade_in)
        object->sprite = fade_bitmap;
      else
        object->sprite = blank_bitmap;
      return;
    }
    new_fade = free_fades_head.next_struct;
    free_fades_head.next_struct = new_fade->next_struct;
    new_fade->object = object;
    new_fade->fade_in = fade_in;
    new_fade->speed = speed;
    new_fade->percent_done = 0;
    new_fade->static_display = static_display;
    new_fade->color_range_start = color_range_start;
    new_fade->colors_used = colors_required;
    for (stepper = HIGHEST_BASIC_COLOR;stepper;stepper--)
      if (color_used[stepper]) {
        new_fade->color_mapping[stepper] = color_range_start;
        color_range_start += HIGHEST_BASIC_COLOR + 1;
      }
      else
        new_fade->color_mapping[stepper] = 0;
    new_fade->next_struct = active_fades_head.next_struct;
    active_fades_head.next_struct = new_fade;
    if (static_display)
      construct_fade_bitmap(new_fade);
  }
  else {
    destroy_bitmap(object->sprite);
    if (fade_in)
      object->sprite = object->fade_bitmap;
    else
      object->sprite = blank_bitmap;
    return;
  }
}



unsigned int view::is_fade_running(anim_system *object) {
  fade_struct *fade_stepper;

  fade_stepper = active_fades_head.next_struct;
  while (fade_stepper) {
    if (fade_stepper->object == object)
      return YES;
    fade_stepper = fade_stepper->next_struct;
  }
  return NO;
}



void view::update_fade() {
  fade_struct *fade_stepper, *fade_memory;
  int color_stepper, range_stepper;
  fix visible_percent;
  fix color_fraction;
  unsigned char start_index;
  RGB color_changer;

  fade_stepper = active_fades_head.next_struct;
  fade_memory = &active_fades_head;
  while (fade_stepper) {
    fade_stepper->percent_done += fade_stepper->speed;
    if (fade_stepper->percent_done >= 100) {
      destroy_bitmap(fade_stepper->object->sprite);
      if (fade_stepper->fade_in)
        fade_stepper->object->sprite = fade_stepper->object->fade_bitmap;
      else
        fade_stepper->object->sprite = blank_bitmap;
      return_colors(fade_stepper->color_range_start,
        fade_stepper->colors_used);
      fade_memory->next_struct = fade_stepper->next_struct;
      fade_stepper->next_struct = free_fades_head.next_struct;
      free_fades_head.next_struct = fade_stepper;
      fade_stepper = fade_memory->next_struct;
    }
    else {
      if (!fade_stepper->static_display)
        construct_fade_bitmap(fade_stepper);
      if (fade_stepper->fade_in)
        visible_percent = fade_stepper->percent_done;
      else
        visible_percent = 100 - fade_stepper->percent_done;
      for (color_stepper = HIGHEST_BASIC_COLOR;color_stepper
        ;color_stepper--) {
        if (fade_stepper->color_mapping[color_stepper]) {
          start_index = fade_stepper->color_mapping[color_stepper];
          for (range_stepper = HIGHEST_BASIC_COLOR;range_stepper >= 0
            ;range_stepper--) {
            color_fraction = (fix) (current_palette[color_stepper].r
              - current_palette[range_stepper].r) / 100;
            color_changer.r = current_palette[range_stepper].r
              + (int) (color_fraction * visible_percent);
            color_fraction = (fix) (current_palette[color_stepper].g
              - current_palette[range_stepper].g) / 100;
            color_changer.g = current_palette[range_stepper].g
              + (int) (color_fraction * visible_percent);
            color_fraction = (fix) (current_palette[color_stepper].b
              - current_palette[range_stepper].b) / 100;
            color_changer.b = current_palette[range_stepper].b
              + (int) (color_fraction * visible_percent);
            change_palette(start_index + range_stepper, &color_changer);
          }
        }
      }
      fade_memory = fade_stepper;
      fade_stepper = fade_stepper->next_struct;
    }
  }
}