In the last issue I explained how to plot pixels on the Mode-13h graphics screen and how to change the red, green and blue intensities of the colors. A logical advance, it seems to me, is plotting multiple pixels with different colors: bitmaps and sprites. Sprites are usually small bitmaps with, possibly, a series of images which form small animations. In this article I will discuss single image sprites/bitmaps only.
Sprites are used a lot in games like Doom, Raptor, Magic Carpet and of course Windows and OS/2 icons can be seen as sprites. You could treat this article as one of the first steps on the long way of games-programming. The article will explain how to display bitmaps and sprites in graphics Mode-13h, how to load images from files and finally how to optimize the graphics routines.
Since sprites and bitmaps are used extensively in applications it would be wise to read them to memory, as disk access slows everything down considerably. Reading the (packed or unpacked) image data ('data' hereafter) from a file in the program's initialisation and using memory in the rest of the application is good programming practice.
Before an image can be read to memory, the structure of the file must be known. What are the dimensions of the image? Where can the palette be found? And where is the data?
For this article I have chosen the relatively simple CEL format, since I don't want to bore you with data compression. CEL files can be created with Jim Kent's Autodesk Animator. A CEL file is build up of a header, a palette and the data. The header and palette are always fixed in size, so the locations of the three chunks is also always the same. The only variables are the image dimensions (width and height) and thus the size of the data.
Figure 1 shows the complete layout of a CEL file.
| Offset | Size | Field | Description |
|---|---|---|---|
| 0000 | 2 | id | File-ID, always: $1991 |
| 0002 | 2 | width | The image width in bytes |
| 0004 | 2 | height | The image height in bytes |
| 0006 | 2 | xofs | The horizontal offset on the screen |
| 0008 | 2 | yofs | The vertical offset on the screen |
| 000a | 2 | pixels | Number of bits per pixel, usually 8 |
| 000c | 2 | isize | Image size in bytes |
| 000e | 18 | filler | Filler to make header size 32 bytes |
| 0020 | 768 | palette | The palette: 256 r,g,b values |
| 0320 | isize | img-data | The unpacked image data |
One way of declaring the CEL-header would be like this:
type
celheader = record
id, width, height, xofs, yofs, pixels, isize : word;
filler : array[0..17] of byte;
end;
Assign an untyped file, reset it with a block size of 1 and BlockRead the first 32 bytes (FileSize(celheader)) to a variable of type celheader. Now all the information we need can be accessed through that variable.
The palette is declared as follows:
type
rgb_rec = record
r, g, b : byte;
end;
pal_type = array[0..255] of rgb_rec;
The next 768 bytes (that is, SizeOf(pal_type)) of the file can be read to a variable of type pal_type. The palette can be activated using the setpal procedure from the palette unit (see Listing 1).
Listing 1 Procudure to set a complete palette
procedure setpal(var pal : pal_type);
var i : byte;
begin
port[$3c8] := 0; { initialize color 0 and set }
for i := 0 to 255 do begin
port[$3c9] := pal[i].r;
port[$3c9] := pal[i].g;
port[$3c9] := pal[i].b;
end;
end;
Next, reading the data into memory can be done in just two fairly simple lines of code, assuming that width and height are set correctly:
getmem(image, width*height); blockread(imgfile, image^, width*height);
Now the data resides sequentially in the reserved memory area.
If the data was now moved to the screen, the image would look scrambled (unless the image width just happens to be exactly 320). Let's see how to display the image correctly...
Figure 2 The example sprite program in action
The first algorithm for displaying the image on screen which pops into mind might just look like this:
for j := 0 to pred(height) do
for i := 0 to pred(width) do
mem[$aOOO:(yofs + j) * 320 + xofs + i] :=
image^[j * width + i + 4];
(image is a dynamic array). It does work, but it's not very fast. Pascal is converted to assembler during compilation and the resulting assembler mul (for multiplication) statements are very slow relative to assembler mov statements. So, to gain speed, the multiplications need to be removed from the inner loop and also, if possible, from the outer loop.
Looking at the code, we see that the expression yofs + j is multiplied by 320 in the inner loop. The parameter yofs (which can be taken from the CEL header) is the y component of the starting coordinate. The variable j is the outer loop counter. Both are constant in the inner loop (yofs is always constant).
So, we see that (yofs + j) * 320 can be moved at least to the outer loop. Also xofs is always constant, so this too can be removed from the inner loop. Finally, j * width is also a constant in the inner loop.
Taking all this into account, we can introduce some optimisations to the original code. Amending the code accordingly give, the following:
for j := 0 to pred(height) do
begin
scrofs := (yofs + j) * 320 + xofs;
imgofs := j * width + 4;
for i := 0 to pred(width) do
mem[$aOOO:scrofs + i] := image^[imgofs + i];
end;
Multiplications by the loop counter can (in most cases) be replaced by a successive addition:
scrofs := yofs * 320 + xofs;
imgofs := 4;
for j := 0 to pred(height) do
begin
for i := 0 to pred(width) do
mem[$aOOO:scrofs + i] := image^[imgofs + i];
inc(scrofs, 320);
inc(imgofs, width);
end;
This code does exactly the same as the first code fragment, but with only one multiplication instead of 2*width*height multiplications.
The optimising is not completely done, though. The inner loop does nothing more than move width bytes from one place to another. Pascal happens to have a procedure which does exactly that: Move.
So, finally, the original code is optimized to the example shown in Listing 2, which is a PutSprite procedure for displaying a sprite on the screen. The width and height are taken from the image data as the first four bytes. This makes memory handling in the rest of the program easier. This is also the reason why imgofs is initialized as 4.
The PutSprite procedure can be used for bitmaps of any size, as long as the width and height of the image are less than 320 and 200 respectively.
The last piece of code is about the same speed as its assembler (BASM) equivalent (which can be found in the Sprite unit on the free disk with this issue, along with full example programs and sample CEL files). A final optimization can be done to use word moves or double word moves, instead of byte moves. This increases the speed of the procedure a lot. It also introduces a small problem when the width is odd and not even. I leave this to you to solve...
Listing 2 Procedure to display a bitmap
procedure putsprite(x,y : word; image: sprptr);
var
j, width, height, imgofs, scrofs : word;
begin
{ get width and height from image data }
width := image^[0] + image^[1] shl 8;
height := image^[2] + image^[3] shl 8;
{ draw to screen }
imgofs := 4;
scrofs := y * 320 + x;
for j := 0 to pred(height) do begin
move(image^[imgofs], mem[$aOOO:scrofs], width);
inc(scrofs, 320);
inc(imgofs, width);
end;
end;
The first time I introduced you to graphics programming using direct screen writing. This time I used the theory of the first article to display bitmaps. Next time I will continue in this fashion. I'm thinking about bitmap scaling or polygon drawing...
[This article appeared in the last issue of The Pascal Magazine.]
Bas van Gaalen lives in Eindhoven in The Netherlands. He can be contacted on the Internet as bas@il.ft.hse.nl or on Fidonet at 2:284/123.2
[addresses not valid anymore!]