Sprites and Animation Part 1
Written by Larry Salomon Jr.
Some time ago, I posted an article to comp.os.os2.programmer.misc describing how a friend and I wrote an animated demo application using home-grown sprite routines. As I stated then, the routines were very tightly integrated into the application; thus, they were not suitable for general availability. I received a few requests to "clean them up" and since animation had been in the Want Ads for some time, I decided to design and implement a sprite library. The result is presented in i495.zip. This is a demonstration program that uses the sprite library to simulate the Long Island Expressway on my trip to and from work. :)
This multipart series will go through the design and implementation of the sprite library; additionally, its uses for animation will be briefly touched upon. The reader is expected to be competent in PM application development as well as have at least introductory knowledge to the Gpi subsystem. Any background knowledge on animation techniques will be very helpful also.
A caveat: I intentionally left out the source code for the sprite library from part 1 so that no one who doesn't already know this stuff will make any incorrect assumptions about how the code works; the source will be included in later parts of this series. In the meantime, you can enjoy using the SPRITE.H and SPRITE.LIB files. :) The library was compiled using IBM C-Set++, so it is a 32-bit library. It was not compiled for multithreaded applications, nor was z-ordering implemented. By next month, I will have released a new version of Common/2 (available at your nearest hobbes shadow) containing the sprite subsystem and corresponding programming reference; unlike the remainder of the library, however, I will make the source for this component available so do not think that the usefulness of the series will be diminished.
Another caveat: the library was developed on my SVGA system at work; when showing it to some fellow colleagues at the IBM PSP Technical Interchange on an XGA system, the transparancy was - shall we say - overdone. A quick chat with Kelvin Lawrence yielded nothing other than the SVGA drivers do not work properly all of the time. I guess I broke the library so that it would work on my SVGA screen. :)
What is a sprite? The term dates at least as far back as the VIC-20 and refers to a graphical object that is animated in some form or another; for performance reasons, these sprites were typically bitmapped graphics. Since animation often depicts everyday objects, and these objects have arbitrary (i.e. non-rectangular) shapes, bitmaps - in the Gpi sense - cannot be used alone to represent a sprite because they overpaint what is underneath. This wouldn't be a problem normally except that bitmaps must be rectangular in shape.
Presentation Manager defines a resource called a pointer (for this discussion, we will lump icons in this category since the only difference between the two is the first two bytes in the file defining the pointer or icon), which has an color which is transparent, i.e. the areas painted by this "color" show what was underneath instead. Unfortunately for us, the size of the pointer is fixed (its size can be determined using the WinQuerySysValue() function), making it unsuitable for most animation (assuming we stick to documented functions for drawing the pointer). If we were able to create a transparent "color" for our bitmaps, we could have irregular shapes surrounded by transparency to make the shape "rectangular". This is, then, our immediate goal; there are other functions which will also be needed, but without the ability to draw a sprite the remainder of the library is rather useless.
Let us begin by looking at the design of the library as well as the decisions that were made and how they affect the implementation. My original intent for the library was to have four functions:
- Create a sprite from a bitmap
- Destroy a sprite
- Draw a sprite
- Move a sprite
We must first explain the third for - speaking in hindsight - the implementation of the drawing will affect the method of creation.
Blast From the Past
I must confess that I learned the technique for drawing a sprite from Charles Petzold's article series in the Microsoft Systems Journal where he developed a game of checkers for OS/2. The checker pieces are irregular so he described how to draw them and keep the underlying image intact where the piece was not drawn. How is it done? Well, if you'll think back to the days of boolean logic, you'll remember that the AND and OR functions have the following properties:
x AND 1 = x
x OR 0 = x
If we require that all regions in the original bitmap that are to be transparent must have the color black, we can use the OR function with the target being x to implement the transparent portions. If we only do this, however, the non-transparent portions of the bitmap will get OR'd with whatever was underneath. Somehow, we need to insure that all portions on the target where the original bitmap is not transparent are turned to black, so the OR function will not alter the bitmap in any way.
Phantom of the Opera
To accomplish this, we need a second bitmap. Called a mask, it is a monochrome bitmap which we combine with the target to change the portions of the target to black as stated in the last paragraph. This is accomplished by making the portions of the mask corresponding to the transparent areas white, and the non-transparent areas black, and then AND'ing this mask with the target.
I realize this is quite confusing, so let's present an example of what is being said. To the left is the original bitmap; to the right is the mask. Both are surrounded by a light gray rectangle only for illustrative purposes.
We will paint the original bitmap on the following background, using black as the transparency color, as described above.
First, we need to mask out the areas of the target where the image will be painted. This is done using GpiBitBlt() with the ROP_SRCAND operation, and it yields the following:
Finally, we paint the original bitmap on top of this using the ROP_SRCPAINT operation (the equivalent of the OR function), yielding the following:
The Four Revisited
To implement the create sprite from bitmap function, we need to also create a mask which is associated with the bitmap. The application programmer should not have to manage this and insure the proper mask is used with the corresponding bitmap, etc., so the sprite data structure should keep track of both.
When you look at the code, you'll think that it is rather trivial. However, actually developing the code to its present state was a bit of work. Of course, we have to have a memory device context and associated presentation space. And, since we stated that the mask is a monochrome bitmap, we can call GpiQueryBitmapInfoHeader() to get information about the original image, set cPlanes and cBitCount to 1 and call GpiCreateBitmap() to create the bitmap. This is then set into the presentation space.
Now comes the tricky part. How do you tell the GpiBitBlt() function to make all black portions white and all non-black portions black? To quote an extremely old piece of IBM documentation, the Programming Guide (page 24-7) from the OS/2 1.2 Programmer's Toolkit:
- If you are copying a color bit map to a monochrome bit map or device, those pels that have the same color as the current image-background color in the source presentation space adopt the image background color of the target presentation space. So, for example, if your current image background color in the source presentation space is blue, all blue pels in the bit map take on the color of the current image background in the target presentation space.
- All other pels in the color bit map adopt the current image-foreground color of the target presentation space.
Unfortunately, I decided to use GpiWCBitBlt() instead of GpiBitBlt() for the mask creation. Since there is no source presentation space it is difficult to determine what will happen given the above text. I could not determine what the rule is for GpiWCBitBlt() (since what I observed makes no sense), so either the text is wrong, I am just being stupid, or we are seeing the broken SVGA drivers in action. :) By trial-and-error I figured out that, in order to create the mask, we call GpiSetColor(CLR_WHITE) and GpiSetBackColor(CLR_BLACK) and then call GpiWCBitBlt() to actually initialize the mask bitmap.
Sprite destruction should be fairly trivial, and we have already discussed how the sprites are drawn, so let's look at how they are moved.
The Quest to Avoid Flicker
When a sprite is moved, there are two scenarios to consider:
- The bounding rectangle of the sprite at the old and new positions do not overlap
- The bounding rectangle of the sprite at the old and new positions overlap
The first case is trivial to implement - you paint the background over the old position, and redraw in the new position. The second case, however, is not so simple; if we treated the second case as the first, there is a noticeable flicker in the regions that overlap. To avoid this, we need to insure that we call GpiBitBlt() only once. This is accomplished using a work-area which is another bitmap set in a presentation space associated with a memory device context. The strategy is thus:
- Determine the rectangle that bounds the bounding rectangles of the sprite at the old and new positions.
- Copy the background from the rectangle calculated in step 1 into the work area.
- Draw the sprite at the new position, relative to the old position.
- Copy the contents of the work area to the screen presentation space.
Step 4 has the visible effect of both erasing the sprite at the old position and redrawing it in the new position. This is a hybrid form of double-buffering, which is defined to be the procedure by which the next "frame" in a sequence is prepared off-screen while the first screen is being displayed. The prepared screen is then displayed while the next frame is prepared, and so on. The effect produced is much smoother than if the frames were prepared on-screen, as you can imagine.
Note that the algorithm we used is only good when the overlapping is present. Attempting to use this when there is no overlap could result is severely degraded performance when you consider that the distance between the old and new position could be quite a lot, making the area blit'd to the screen quite large and CPU intensive in execution.
In the last section, you probably noticed a few loose ends, namely the concept of a background and the management of the work area used when moving the sprite from one position to another. Next month, we'll tie up these loose ends with the introduction of the playground; we will also start to look at the implementation of i495.exe followed by the code for the library itself.