Using transparent boards with AniSprite

Skip to main content (skip navigation menu)






Using transparent boards with AniSprite

 

A feature introduced with AniSprite 2.0 is the support for transparent boards. A board, in AniSprite terminology, is the background image on top of which the sprites move. At any position where there is no sprite, the board shows. And with a transparent board, at any position where there is no sprite, nothing shows... Or rather, something shows that AniSprite does not know about. With a transparent board, you can draw your board with all sprites over a background that was created by other means.

As a side note, AniSprite already provided "call-backs" to let you draw inside a sprite with standard Windows GDI calls. In a way, a transparent board does for boards what the call-backs do for sprites.

Another side note is that a masked board would be a more accurate description of AniSprite's transparent board. A transparent board is still a board; but it has a second board, which functions as a mask, linked to it. Just like a sprite has an image and a mask, a transparent board has an "image" (containing all sprites) and a mask (containing all sprite masks).

A typical use of transparent backgrounds is to move sprites over the desktop. AniSprite (at least version 2.x) is restricted to 256 colour sprites and boards. That is, if you grabbed the desktop image and used that as the board for animation, AniSprite may need to convert the grabbed image to 256 colours if it was in a different resolution. If the desktop image were, for instance, an RGB image (because the display is in RGB mode), the board that was derived from the desktop image is visibly different from the original desktop image.

Transparent boards can help. You still grab the desktop image like before, but you do not create a board from it. Instead, you set the AS_MODE_MASKED flag and create the board without image source. Then you continue with making sprites, attaching them to the board, moving the sprites around, etc. Isn't there additional effort involved in using transparent boards? Well, there is, and quite an involved effort too: you must display the background beneath the transparent board before AniSprite draws its board with all the sprites on top of this background. This sounds rather simple, and it is rather simple, but you also want to avoid flicker. AniSprite can no longer help you here, because AniSprite does not control what is below the board.

Grabbing the screen

I would like to step back at this point, to add some notes to the subject of grabbing the screen, which I glossed over a few paragraphs earlier.

You typically do not control the video mode that is currently set in Microsoft Windows. It may be set in a 256-colour mode or in one of the RGB modes (or it may even be in 16-colour mode). To account for palette modes, you should grab the system palette and select/realize that palette before grabbing the screen. The following snippet illustrates this.

Grabbing the system palette and the screen
  LPBITMAPINFO lpBoardInfo;
  HPALETTE hpal, hpalOrg;
  HDC hdc;

  /* ... */

  lpBoardInfo = as_AllocResource(sizeof(BITMAPINFO) + 256 * sizeof(RGBQUAD));

  hpal = as_GrabSysPalette(lpBoardInfo->bmiColors);
  hdc = GetDC(hwnd);
  hpalOrg = SelectPalette(hdc, hpal, FALSE);
  RealizePalette(hdc);
  hbmpBackground = as_GrabScreen(hdc, NULL);
  SelectPalette(hdc, hpalOrg, TRUE);
  ReleaseDC(hwnd, hdc);

  as_FreeResource(lpBoardInfo);

Now what if the current video mode is an RGB mode? There is no real palette in RGB modes, so should there not be "if-then-else" constructs around the palette handling? Well, one could embrace the palette handling in "if" test clauses, but Microsoft designed the palette functions to be "drop-through" if the hardware does not use a palette. That is, just always selecting a palette never does any harm.

Creating the board

For completeness, below is a snippet for creating a transparent board. Note that transparent boards have limited options: you cannot use luma or alpha masks with a transparent board.

Creating a transparent board
  LPBITMAPINFO lpBoardInfo;
  int width, height;
  ASBOARD Board;

  /* ... */

  lpBoardInfo = as_AllocResource(sizeof(BITMAPINFO) + 256 * sizeof(RGBQUAD));

  /* grab the system palette in lpBoardInfo->bmiColors, see above */

  width = GetSystemMetrics(SM_CXSCREEN);
  height = GetSystemMetrics(SM_CYSCREEN);

  as_CreateBitmapInfoHeader(lpBoardInfo, width, height,
                            lpBoardInfo->bmiColors, 256);
  Board = as_CreateBoard(lpBoardInfo, NULL, NULL, AS_MODE_MASKED);

  as_FreeResource(lpBoardInfo);

A transparent board is not based on an image, but the function as_CreateBoard() needs a BITMAPINFO structure to initialize the DIB structures that it uses internally. The above code snippet uses AniSprite function as_CreateBitmapInfoHeader() to fill in a BITMAPINFO structure.

The code to grab the screen and to create a transparent board has been shown here in two separate snippets. In the example program from which these snippets were extracted, they are intermingled. This is already visible from the code: the as_GrabSysPalette() function stores the colour table directly in the BITMAPINFO structure which is subsequently used to create the board from. Although both snippets show a memory allocation/free pair (for completeness), the real program has only one pair.

Back to the main track: drawing the transparent board

Sprite operations on a transparent board are not different from the operations on normal boards. But typically, after having moved a number of sprites, you update the board by calling as_PaintBoard(). For a normal board, this would be sufficient, but for a transparent board, you must draw the background before AniSprite draws the board with its sprites on top of that background. There are at least three approaches that you can take here:

  1. First draw the board in a NULL device context to get the list of all changed areas (the "clip region"). Then select that clip region in order to draw the background only in those areas that need updating. Finally call as_PaintBoard() again to draw the sprites on top of the new (restored) background.
  2. When moving the sprites, call the standard Windows functions InvalidateRect() for both the old and the new positions of the sprite. This will generate a WM_PAINT message and upon reception of the WM_PAINT message you draw the complete board. The advantage is that you need code to handle the WM_PAINT message anyway. Microsoft Windows is also smart enough to generate a clip region that includes only those areas that were "invalidated". So while your code repaints the entire background and board, Windows will only touch the areas that change, with an important gain in performance.
  3. If you create the transparent board with the AS_MODE_NODRAW flag, calling as_PaintBoard() does not display anything on the screen. But you do have access to updated DIBs for the board and its mask plus the list of updated (or "invalid") rectangles. Now you do everything yourself. This option may be useful if you are using the AniSprite board with another utility that takes DIBs as input. You could, for example, use the board DIB and its mask DIB to create a sprite that represents the complete board and paint it on another board.

This paper will first focus on the second approach: we draw all sprites in the WM_PAINT message handling and let Microsoft Windows do all the clipping. This lays the groundwork for drawing transparent boards. In a later section, this paper shows how to optimize the routine, which brings us to the first approach.

Let us also assume that the background image on top of which we want to display the transparent board is a device dependent bitmap (HBITMAP, for example, a bitmap returned by as_GrabScreen(). As stated in the introduction, moving sprites on top of the grabbed desktop image is a popular use for transparent boards.

When the program receives a WM_PAINT message, it will drop into the following code snippet:

Handling the WM_PAINT message
  PAINTSTRUCT ps;
  HDC hdc;
  HPALETTE hpal, hpalOrg;
  ASBOARD Board;

  /* ... */

  case WM_PAINT:
    hdc = BeginPaint(hwnd, &ps);
    hpalOrg = SelectPalette(hdc, hpal, FALSE);
    RealizePalette(hdc);

    ShowBuffered(hdc, hbmpBackground, Board);

    SelectPalette(hdc, hpalOrg, TRUE);
    RealizePalette(hdc);
    EndPaint(hwnd, &ps);
    break;

  /* ... */

Aside of the palette handling, the real work happens inside function ShowBuffered(). The focus in the next sections is in increasingly improved versions of this function.

Painting the transparent board, simple version

One way to do it is to call BitBlt() for the background and to call as_PaintBoard() sequentially, as in the following snippet:

ShowBuffered(), simple version
void ShowBuffered(HDC hdc, HBITMAP hbmp, ASBOARD Board)
{
  int width = as_GetBoardValue(Board, AS_VALUE_WIDTH, 0);
  int height = as_GetBoardValue(Board, AS_VALUE_HEIGHT, 0);

  HDC hdcMem = CreateCompatibleDC(hdc);
  hbmp = SelectObject(hdcMem, hbmp);
  BitBlt(hdc, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY);
  hbmp = SelectObject(hdcMem, hbmp);
  DeleteDC(hdcMem);

  as_PaintBoard(hdc, Board, 0, 0, TRUE);
}

The first part of this function is standard code to put a device dependent bitmap (a DDB) on the screen; in our case this is the DDB of the background image. Function as_PaintBoard() then draws the sprites on top of it.

There is one (major) problem with this simple function: flicker. When you BitBlt() the background onto the screen, it erases all sprites. Then the sprites re-appear on the screen when as_PaintBoard() executes. In the days of DOS games, two popular work-arounds were to either do all blitting during the "vertical retrace" of the computer monitor or to use an off-screen memory buffer an flip pages. The vertical-retrace option is gone on Microsoft Windows; it just isn't quick enough to blit a screen in the very short vertical retrace period (see also the article "Detecting Vertical Retrace in Microsoft Windows"). Page flipping is not an option either on Microsoft Windows, but its alternative off-screen memory is a "memory DC".

So let us turn to memory DCs next.

Avoiding flicker

The idea is to copy the background image and the transparent board to a "hidden" screen first (a memory DC) and then to copy this memory DC on the real display. Now all "flicker" due to overwriting occurs on the memory DC, ergo, the flicker is hidden from us.

The previous ShowBuffered() function already created a memory DC because the BitBlt() function can only handle DCs, whereas we provide ShowBuffered() with a handle to a device dependent bitmap. The next ShowBuffered() function requires needs a second memory DC and it needs an appropriately sized "work" (or scratch) bitmap.

Windows maps colour tables in DIBs to the palettes selected in DCs (if the DC is palette-aware), so the palette that must be selected in the temporary DC as well. Note again that if the display is set to more than 256 colours, palette operations are unnecessary (but harmless).

ShowBuffered(), flicker free (but slow) version
void ShowBuffered(HDC hdc, HPALETTE hpal, HBITMAP hbmp, ASBOARD Board)
{
  HDC hdcMem, hdcBuffer;
  HBITMAP hbmpBuffer;

  int width = as_GetBoardValue(Board, AS_VALUE_WIDTH, 0);
  int height = as_GetBoardValue(Board, AS_VALUE_HEIGHT, 0);

  /* create a second "work" buffer, to avoid flicker */
  hdcBuffer = CreateCompatibleDC(hdc);
  hpal = SelectPalette(hdcBuffer, hpal, FALSE);
  RealizePalette(hdcBuffer);
  hbmpBuffer = CreateCompatibleBitmap(hdc, width, height);
  hbmpBuffer = SelectObject(hdcBuffer, hbmpBuffer);

  /* copy the grabbed image in the buffer */
  hdcMem = CreateCompatibleDC(hdc);
  hbmp = SelectObject(hdcMem, hbmp);
  BitBlt(hdcBuffer, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY);
  hbmp = SelectObject(hdcMem, hbmp);
  DeleteDC(hdcMem);

  /* put the board on top */
  as_PaintBoard(hdcBuffer, Board, 0, 0, TRUE);

  /* display the whole set on the screen */
  BitBlt(hdc, 0, 0, width, height, hdcBuffer, 0, 0, SRCCOPY);

  /* clean up: delete the work buffer */
  hbmpBuffer = SelectObject(hdcBuffer, hbmpBuffer);
  DeleteObject(hbmpBuffer);
  SelectPalette(hdcBuffer, hpal, TRUE);
  DeleteDC(hdcBuffer);
}

This solved the problem, but it confronts us with a new one: speed, or lack thereof. A simple optimization is to create the work buffer only once, instead of creating and deleting it for every board redraw. But that is not the real cause for the slow operation of the new ShowBuffered() compared with the older one. The slowdown is caused by the disappearance of the clip region.

Redrawing every pixel on the board is obviously much slower than just refreshing those parts of the board where sprites disappeared or popped up; and redrawing the complete screen is quite what the above example does. When discussing of the three possible approaches, I wrote that Microsoft Windows creates a clip region that includes only those areas that were invalidated. However, Microsoft Windows only generates clip regions for display device contexts, not for memory device contexts. So in the above version of ShowBuffered(), the entire background image is updated in the work buffer, then all sprites are put on top of the work buffer, and, finally, only a portion of the work buffer gets copied to the screen (through the clip region in the screen DC).

The next step, then, is to reduce the operations on the work buffer, by attaching a clip region to the memory DCs as well.

Reducing work with clip regions

The Microsoft Windows API function GetClipBox() returns the bounding box of the region, and the bounding box can be large even for a small region. For example, when two sprites in opposite corners of the board move a few pixels, the clip region contains two disjunct small rectangles and the clip box contains everything in between as well.

Region versus bounding box

Microsoft documents a new function, GetRandomRgn(), for Windows 2000 and later, but according to Feng Yuan, this function was already present in Windows 95 (the implementation of GetRandomRgn() in Windows 9x is not fully compatible with that of Windows 2000). Function GetRandomRgn() retrieves the system clip region and the function GetRegionData() subsequently allows you to inspect the individual rectangles that make up the clip region. An alternative, and usually more convenient, is to get the clip region from AniSprite itself. AniSprite creates a clip region internally, and it makes it available to the applications. Additional advantages are that AniSprite is faster in generating the clip region than Microsoft Windows' GDI, so this removes a possible performance bottleneck. Below is a routine that creates a clip region for a board. Note that in sprite animation, the "clip region" is usually called the "dirty rectangle list".

Create a clip region from the dirty rectangle list
HRGN CreateDirtyRegion(ASBOARD Board)
{
  HRGN hrgn1, hrgn2;
  LPRECT rc;
  int i;

  /* let AniSprite collect the "dirty rectangle" list by painting in
   * a NULL HDC
   */
  as_PaintBoard(NULL, Board, 0, 0, FALSE);
  hrgn1 = NULL;

  /* create the region for the first rectangle */
  rc = as_GetBoardData(Board, AS_DATA_REGIONRECT, 0);
  if (rc != NULL)
    hrgn1 = CreateRectRgn(rc->left, rc->top, rc->right, rc->bottom);

  /* add other rectangles in the list */
  for (i = 1; (rc=as_GetBoardData(Board,AS_DATA_REGIONRECT,i)) != NULL; i++) {
    hrgn2 = CreateRectRgn(rc->left, rc->top, rc->right, rc->bottom);
    CombineRgn(hrgn1, hrgn1, hrgn2, RGN_OR);
    DeleteObject(hrgn2);        /* no longer need hrgn2 */
  } /* if */

  return hrgn1;
}

The ShowBuffered() function changes only little from the previous version. I added a call to CreateDirtyRegion() and selected that region into all DCs. The ShowBuffered() function needs to have one additional parameter that tells AniSprite whether or not to ignore the dirty rectangle list and to paint the complete board. When you call ShowBuffered() from the timer, you will want to repaint only the dirty rectangle list. When you call ShowBuffered() from a WM_PAINT message, you will need to ignore AniSprite's dirty rectangle list and to use the clip region provided by Windows.

ShowBuffered(), final version
void ShowBuffered(HDC hdc, HPALETTE hpal, HBITMAP hbmp, ASBOARD Board, BOOL PaintAll)
{
  HDC hdcMem, hdcBuffer;
  HBITMAP hbmpBuffer;
  HRGN hrgn;
  int width, height;

  width = as_GetBoardValue(Board, AS_VALUE_WIDTH, 0);
  height = as_GetBoardValue(Board, AS_VALUE_HEIGHT, 0);
  hrgn = CreateDirtyRegion(Board);

  /* create a second "work" buffer, to avoid flicker */
  hdcBuffer = CreateCompatibleDC(hdc);
  hpal = SelectPalette(hdcBuffer, hpal, FALSE);
  RealizePalette(hdcBuffer);
  hbmpBuffer = CreateCompatibleBitmap(hdc, width, height);
  hbmpBuffer = SelectObject(hdcBuffer, hbmpBuffer);
  SelectClipRgn(hdcBuffer, hrgn);

  /* copy the grabbed image in the buffer */
  hdcMem = CreateCompatibleDC(hdc);
  hbmp = SelectObject(hdcMem, hbmp);
  SelectClipRgn(hdcMem, hrgn);
  BitBlt(hdcBuffer, 0, 0, width, height, hdcMem, 0, 0, SRCCOPY);
  hbmp = SelectObject(hdcMem, hbmp);
  DeleteDC(hdcMem);

  /* put the board on top */
  as_PaintBoard(hdcBuffer, Board, 0, 0, PaintAll);

  /* display the whole set on the screen */
  SelectClipRgn(hdc, hrgn);
  BitBlt(hdc, 0, 0, width, height, hdcBuffer, 0, 0, SRCCOPY);

  /* clean up */
  hbmpBuffer = SelectObject(hdcBuffer, hbmpBuffer);
  DeleteObject(hbmpBuffer);
  SelectPalette(hdcBuffer, hpal, TRUE);
  DeleteDC(hdcBuffer);
  DeleteObject(hrgn);
}

This concludes the journey. While I admit that it has been a fairly long exposé, brevity does a subject like that of this paper no good, in my opinion: details count.

For more information and for a complete and working example program that includes the code snippets in this paper, download the AniSprite evaluation version and look for the file TRANSP.C.

Further reading

Yuan, Feng; "Windows Graphics Programming"; Hewlett-Packard Professional Books; 2001; ISBN 0-13-086985-6.
The description of GetRandomRgn() is in chapter 5.5, including an example program.