Chapter 7 -- BGI Graphics Part 2





Introduction

Hi folks! Nice to see ya this time. Wow! Seems that you're eager to read all of the lines in this chapter! Is all the preparation in the last chapter done? You are set! Let's go!

What to Learn

This time I'd like to talk about animating images, saving and loading images, transparent background sprites, and linking graphic files. Oh yes! I will also tell you about custom fill and line patterns. I tell you the 3-D basics, too. This requires mathematics... Oh well, simple ones. Just sine and cosine. :-) Ready?

Animating Images

Remember last chapter's getimage and putimage? I'm sure you do. Now, why do I recommend you to learn this pretty well? It's because you can animate your image. Using a simple loop, you can move the images on the screen. The moving images is called sprites. This jargon is pretty important in animation world, including for game programmers.

OK, look at this excerpt: Put this between init and destroy part.


procedure animate;
var
  backbuf     : pointer;
  backsize, i : word;
begin
  { Calculate background buffer size first }
  backsize:=imagesize(0,0,20,20);

  { Reserve the buffer }
  getmem(backbuf,backsize);

  { Prepare the colors }
  setfillstyle(1,14);
  setcolor(14);

  for i:=1 to 500 do
  begin
    { Save the background first }
    getimage(0+i,0,20+i,20,backbuf^);

    { Draw something (Pacman) }
    sector(10+i,10,30,330,10,10);
    delay(5);   { Delay a bit }

    { Restore the background }
    putimage(0+i,0,backbuf^,0);
  end;

  { Release the buffer }
  freemem(backbuf,backsize);
end;

The main animation is inside the loop i. It essentially has these steps:

  1. Save the background first.
  2. Draw something on it.
  3. Delay a bit.
  4. Restore the background.

I add the +i line to x coordinates in order the sprite to move. This animation has a little blinky, right? Later, I would like to tell you more advanced idea of animation, but not in this chapter.

The buffer size is a constant. Removing them out of the loop is to make things faster. Why is it a constant? It is because the size of the sprite is a constant. Taking the background is a matter of taking a little square before it is occupied by the sprites. So, just save the overwritten background, not the entire screen. :-)

Moving the color setting and the fill setting is also the effort to make things faster. This trick is done if the sprite has only one color. If the sprites are multi-colored, you need to put this in the loop intact.

Try to replace sector(10+i, ... part with this:
sector(10+i,10,30-(i mod 30),330+(i mod 30),10,10);
Now Pacman's is live!

Now you asked: why just we fill a black bar to restore the background, it will all be the same? I said unto you:If the background is black, it will do just fine. But when it came to a colored background, it went into trouble. Put these lines just after the begin of animate procedure.

setfillstyle(6,6);
bar(0,0,600,30);

Did you notice that? The background is not affected! If you just put a black bar, the bacground will be erased to black! See the difference?

Saving and Loading Images

Actually, this is a very simple yet useful trick. Last time, we learn drawing things on the screen. I think that it is better to us to save what we have drawn from the screen. Saved images can later be reloaded and be put in the same manner.

Suppose you have drawn a cute robot on the screen. Say that takes about 15 instructions to draw it. But when it is looped, I'm sure that a simple putimage of that image is much faster than executing 15 instructions to draw the same robot. That's why most of game programmers prefer saving their images in files.

Saving images needs several format. The famous formats recently are BMP, PCX, GIF, JPEG, TIFF, TGA, and the last one: PNG, to mention a few. I'm not going to discuss that format here. I just tell a format RAW. Simple and easy. It does no compression or any complex things. It contains 3 words of header. The first is for the width of the image. Next is for the height. And last is for the number of bits used for the image. 16 colors use 4 bit, since 24 = 16. The rest is image data

Fortunately, we don't need to bother with those lay-out since Borland Pascal's getimage is fully compliant with that structure. So, we can just save the buffer to disk after we perform getimage. Saving to disk is done by blockwrite. I think that I have told you that in the first lesson. Loading from disk is done through blockread. I've told that too. After loading the image to the buffer, you can apply putimage normally.

Transparent Background Sprites

After knowing saving and loading sprites, now you encounter one problem: the black background of the sprite! That makes things ugly, right? Yeah! There are two ways to make things nicer. All of these methods are directed toward making transparent background sprites. That doesn't means that the sprite is really transparent. It just makes the black background is transparent.

The first method is to put the image as XORput. Remember the fourth parameter of putimage? I fill it with 0, right? For xorput, fill it with 1. The background did disappear. But all the sprites is transparent! The image beneath it is still seen. This is useful when you want to draw ghosts on your game. :-) If you don't want to draw ghosts, then you should try the next trick.

The second trick is done through two putimages. However, we need to save the sprites twice. One for the holer, the other is for the original one. The first thing I want to mention is that we need overhead time to make the holer. It's quite a long overhead process. So, it is best done outside the game you want to make.

The first step is you make the original first and save it to disk. Then you change every black pixel (color 0) into white (color 15). If the pixel is not black, turn it into black (color 0). So, it's a kind of silhouette. After you've done that, save it to disk. Pixel changing is done best by getpixel and putpixel. Here's an excerpt:


for x:=0 to 20 do
  for y:=0 to 20 do
  begin
    c:=getpixel(x,y);
    if c=0 then c:=15 else c:=0;
    putpixel(x,y,c);
  end;

Pretty easy right? But that's pretty slow, so be patient. The excerpt above convert an 20x20 image. You can modify them to suit your need. Of course all the variables: x, y, and c are words. After the loop, you can put some codes to save the resulting image.

Implementing holer and original image is this way. First, load both images into buffers. The original image is stored in img, and the holer is stored in holer. Then, you put the holer first as ANDput. After that, you put the original image as ORput. Here's how to do it:


putimage(10,10,holer^,3);   { Put the holer as ANDput }
putimage(10,10,img^,2);     { Put the original as ORput at the same location as the holer }

Easy right? The holer does its job, to hole things so that the hole fits the original image. After that, the original image beseated comfortably.

This idea is originally from logic operations. Every thing AND-ed with 0 is 0, and AND-ed with 1 is left unchanged. Then, every thing OR-ed with 0 is left unchanged, and OR-ed with 1 is always 1. Well, that's the background. If you still confused, just make this as an input to you. You need not to know further. :-)

Linking Graphic Files

You find it is very unpractical to load the graphic files every time you want to execute your graphic programs. So, Borland Pascal provides a way to link the graphic files. Linking graphic files make things faster, since the program doesn't need to load the driver from disk.

Unfortunately, the BGI or CHR files are not yet ready to link inside the programs. So, we need to convert them into OBJ so that the compiler can easily embed them into our program's single EXE. The pictures we had made can also be converted into OBJs. What is OBJ anyway? It's an object file format, usually used by compiler to combine things.

Borland Pascal has a file called BINOBJ.EXE (other versions may be called BGIOBJ.EXE). Here's how: First you need to know which file to convert. In this example we use EGAVGA.BGI. Then you need to know the procedure's name of that file. Why? you asked. It's because we need some address to access the embedded file. That address was represented as procedure name. Suppose the procedure name is VGADriver.

Before we continue, make sure that the intended files are exist. Usually BINOBJ.EXE is located in BIN directory. Make sure you have set the correct path. Then change to BGI directory and type the line below and press enter:

binobj egavga.bgi egavga.obj vgadriver

Voila! You have successfully created EGAVGA.OBJ. This file is later used inside the VGADriver procedure. So, back into Pascal editor and put these lines in any place (but of course it didn't cut other procedures / functions):

procedure VGADriver; external;
{$L .\bgi\egavga.obj }

Yep! Simple and easy! You need to specify that VGADriver is an external routine taken from EGAVGA.OBJ, so that the compiler wouldn't spit an error. :-) You may specify the path of the OBJ files. If you simply omit the path, Borland Pascal will look it in the object directories (see Option | Directories). Don't remove the {$L ... line! It's not a comment, but it IS a part of the program.

After linking the graphic file, the initialization is slightly different. Recall the init procedure in the last chapter. Now, check the initgraph line. Omit the path in the third parameter so that initgraph line would look like this:

initgraph(gd,gm,'');

Yes, omit the path by simply placing two single-quotes! No, no, don't run that yet. It's not done yet! After that, place this line just after the main begin:

if registerbgidriver(@vgadriver)<>0 then halt;

We need to register the procedure so that Pascal knows that it's already been embedded.

We could do the same to font files. It takes the same way. Except the registering part. Registering font files is done through registerbgifont rather than registerbgidriver. You also need to pass the address of the font procedure to it, just like the if statement above. Simply putting an @ sign in front of the procedure name! Easy, right?

Custom Pattern

This is completely depends on how good are you in design. Me, not. Although I could create cute games, but I am still far below the required skill as an artist.

Pascal gives you tools to create custom line and custom fill patterns. First of all, I would like to tell you about the line patterns. We use setlinestyle to accomplish that matter. It takes three parameters. The first is for the line style number. Try it between 0 to 4. The second is for the pattern. Leave this 0 if you use default style. The last one is for the thickness, only accepts 1 and 3 as the parameter.

If you fill the first parameter as 4, then you need to fill the second parameter, in word. You can arrange the pattern from bits. If BP encounters bit 1, it draws a pixel, if the bit is 0, it leaves the background intact. So, the pattern is done in bits. I'm very impatient doing this so I never create my own (except this one below). :-) Here's how. Create a 16-bit pattern like this:

1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 = 9999 Hex

This is a pattern that draws the line two dots each time. So you'd put this into setlinestyle, like this:
setlinestyle(4,$9999,1);

Now try drawing lines, rectangles, circles, and so on. It will appear somewhat dashed. Weird pattern. :-) Now, you can try your own.

Creating fill pattern is more complex than line pattern. It needs to create a 8x8 pattern. You need to make it tileable. The pattern is done in bits. So, we need to create 8 bytes of pattern. Remember that each byte contains 8 bits. Look at the following pattern:

1 0 0 1 1 0 0 1  = 99 Hex
1 1 0 0 1 1 0 0  = CC Hex
0 1 1 0 0 1 1 0  = 66 Hex
0 0 1 1 0 0 1 1  = 33 Hex
0 0 1 1 0 0 1 1  = 33 Hex
0 1 1 0 0 1 1 0  = 66 Hex
1 1 0 0 1 1 0 0  = CC Hex
1 0 0 1 1 0 0 1  = 99 Hex

You put these numbers into a fillpatterntype variable. Means that you cannot put this into ordinary array of byte. Borland Pascal provides a specific type (that is fillpatterntype) to fill the pattern with. So, this is one of the example of filling patterns:

const
  mypattern : fillpatterntype = ( $99, $CC, $66, $33, $33, $66, $CC, $99 );

To set custom pattern, you need to call setfillpattern first. It takes two parameters. The first is the pattern itself, the second is the color. So, invoking setfillpattern(mypattern,14); will set the fill pattern to your style and it is yellow. After doing setfillpattern, you can see the direct effect when you fill things.

The 3-D Basics

The first thing I want to mention is that 3-D world needs a lot of mathematics. Sure! Well, sometimes it just needs sine and cosine to perform calculations, sometimes it needs complex stuffs. Let's begin.

We need to build 4x1 matrix to perform calculations. It is for the coordinate's x, y, and z. The last is left blank. This can be performed with array:

type
  coord3d = array [1..4] of integer; { you can employ shortint instead of integer }

Now, the 3-D projections need different origin. In 2-D graphics, usually the origin is top-left (0,0), then extends to the right and to the bottom until (639,479). In 3-D graphics, the origin is best placed in the center (320,240). However, it is absolutely up to you and it depends on the situation.

The projection is done through this formula:
X = x * 16/ z
Y = y * 16/ z

The uppercase letters denote 2-D coordinates, lowercases for 3-D ones. Because the origin is in the center, we need to add the capital X and the capital Y with 320 and 240 respectively. The constant 16 is needed when the created graphics is too small to view. Well, you can choose any number you want.

Then, we need to define the 2-D coordinates as well. It is a matrix of 3x1 with the last entry left blank. Why do I always make the last entry blank? It makes things faster. In many matrix transformations, you'll know why should we left it blank. It is simply to make things easier to multiply or doing matrix operations. The 2-D definition looks like this:

type
  coord2d  = array [1..3] of integer;

After that, we build a procedure that convert 3-D coordinates into 2-D ones. Since functions cannot return a record, I build a procedure to deal with that matter. Alternatively, you could turn it into pointers. Here you are:


procedure convert(xyz : coord3d; var xy : coord2d);
begin
  xy[1]:=(xyz[1]*16) div xyz[3] + 320;
  xy[2]:=(xyz[2]*16) div xyz[3] + 240;
end;

Probably the hardest thing for beginners of 3-D world is to model a 3-D shape. I experience such difficulty when I was learning 3-D world. The easiest shape to imagine is a box. So, this time I'd like to draw a 3-D box, wireframe one. You can use negative number as the coordinates. The box is here drawn in the center.


const
  box : array [1..8] of coord3d = ( (-3,3,1,0), (3,3,1,0), (3,-3,1,0), (-3,-3,1,0),
        (-3,3,3,0), (3,3,3,0), (3,-3,3,0), (-3,-3,3,0) );

procedure drawbox;
var
  proj : array[1..8] of coord2d;
  i    : byte;
begin
  for i:=1 to 8 do convert(box[i],proj[i]);
  line(proj[1,1],proj[1,2],proj[2,1],proj[2,2]);
  line(proj[2,1],proj[2,2],proj[3,1],proj[3,2]);
  line(proj[3,1],proj[3,2],proj[4,1],proj[4,2]);
  line(proj[1,1],proj[1,2],proj[4,1],proj[4,2]);
  line(proj[1,1],proj[1,2],proj[5,1],proj[5,2]);
  line(proj[2,1],proj[2,2],proj[6,1],proj[6,2]);
  line(proj[3,1],proj[3,2],proj[7,1],proj[7,2]);
  line(proj[4,1],proj[4,2],proj[8,1],proj[8,2]);
  line(proj[5,1],proj[5,2],proj[6,1],proj[6,2]);
  line(proj[6,1],proj[6,2],proj[7,1],proj[7,2]);
  line(proj[7,1],proj[7,2],proj[8,1],proj[8,2]);
  line(proj[5,1],proj[5,2],proj[8,1],proj[8,2]);
end;

Try putting it between init and destroy part. A 3-D box appear. Seems flat and easy to draw. We connect those vertex manually? Wow! What a job! At this moment, yeah! That's the way.

The only transformation I would like to tell about this time is translation. Other transformations will be discussed later. It is done by simply adding x, y, and z components by a value. The translation should look like this:


procedure translation(var xyz : coord3d; trans : coord3d);
begin
  xyz[1]:=xyz[1]+trans[1];
  xyz[2]:=xyz[2]+trans[2];
  xyz[3]:=xyz[3]+trans[3];
end;


Easy right? OK, now let's translate the box by adding this before drawbox. Define matrix trans as coord3d holding this value: 1,0,0,0.


for i:=1 to 20 do
begin
  for j:=1 to 8 do translation(box[j],trans);
  drawbox;
  delay(5);
  if i=20 then break;
  cleardevice;
end;


You see how the box moves? You've learnt 3-D programming! :-)

Notes

Phew! Finally we came into an end! The graphics chapters I told so far is just a mere basics. So, if you want to be a graphic guru, you need to learn a LOT more! That's all folks! Not quite long, right? Shall we go to the quiz or the next lesson?


Where to go?

Back to main page
Back to Pascal Tutorial Lesson 2 contents
To the quiz
To Chapter 8 about overlaying programs.
My page of programming link
Contact me here


By: Roby Joehanes, © August 1997