Fastgraph 3D Tutorial

Chapter 2: Displaying Objects in 3D Space


Fig. 2.1 Walls and objects.

Defining Objects in Object Space

Virtually all objects we will describe in 3D space using Fastgraph will consist of faces. In Figure 2.1, above, you can see two walls and a cube. The cube has six faces, several of which are hidden. The walls have one face each (They are very thin walls). In this section, I will describe two different ways to define faces of objects in 3D space.

Let's think about the cube first. A cube is an object that is reuseable. That is, I may want to have many cubes in my 3D world. Ideally, I only want to define it once, and then put it many places. The best way to do this is by defining a set of coordinates in the cube's own object space.

In this context, object space simply means the cube is defined around the origin. The center of the cube is (0,0,0), and the corners of the cube are some distance away from the origin, say a distance of 1. This describes a generic cube. The cube will never actually be displayed at the origin. Every time we show the cube, it will be translated and rotated by Fastgraph. The translation and rotation moves the center of the cube to a point (x,y,z), and rotates it.

Figure 2.2 shows the coordinates of one face of the cube.


Fig. 2.2 Defining a cube face in object space.

Notice how the origin is in the middle of the cube. Later, I will refer to this as the "center of gravity" although that is a misnomer. We don't really know anything about the mass of the cube, and we can define the center to be anywhere we want. If we define the center to be at one corner, and then try to rotate the cube, it would spin around that corner. In general, it's a good idea to center an object around something in its approximate middle, so it will look good if you spin it.

Defining Objects in World Space

The walls in Figure 2.1 have different properties than the faces of the cube. Each wall is unique. They have different sizes, angles, textures, etc. We decide to define our walls in world space. That means walls are defined in terms of absolute coordinates, rather than centered around the origin. The wall coordinates may look something like figure 2.3:


Fig. 2.3 Defining a face in world space.

Notice how we define the the coordinates in a clockwise order. This is useful when you want to take advantage of Fastgraph's backface removal capabilities. You should get in the habit of doing this even if you don't use backface removal. Consistency will pay off.

You will use a different Fastgraph function to display an object defined in world space than to display an object defined in object space.

Note that defining walls in world space may not be the most efficient option. You may choose to define several walls in object space and reuse them by moving them to different locations in world space. It will depend on several factors: how many of your walls are unique, how your level editor works, etc. Put a little thought into this before you get started to avoid re-doing your work later on.

Rotation, Translation, and Projection

If you are unfamiliar with 3D programming, these words may sound a bit intimidating. So let's start with some simple definitions, then we will look at the Fastgraph functions that handle these activities.

Rotation
Rotation means moving around something, either the origin, the point of view, or the object's center of gravity. You rotate by angles. In Fastgraph, these angles are defined as degrees times 10, so once around a circle is 3600 degrees.

Translation
Translation means moving everything. In a game like a First Person Shooter (FPS), you don't move everything in your world, you move yourself. That is, you move your own point of view. If you give it a little thought, though, you will see that moving yourself towards an object, or moving the object towards you, involves exactly the same mathematical operation. So even if objects are stationary, they must be translated every frame if your point of view changes.

Projection
Projection is the process of generating a 2D representation of a 3D object. That is, displaying your 3D objects in a format that can be viewed on the screen.
Rotation, translation, and projection all involve matrix mathematics. The mathematics behind the manipulation of matrices is well documented. See, for example, Computer Graphics Principles and Practice by James Foley, Andries van Dam, et al. Fastgraph manipulates matrices internally in the usual way, as described in that book and others by Michael Abrash and John De Goes. I am not going to go into the details of matrix manipulation right now, but if you are interested, I suggest you look at those books, or have a look at Appendix 1.

For those who are not currently interested in getting bogged down in matrix mathematics, and who want to start writing some 3D programs, let's carry on with this tutorial.

Steps in Writing a 3D Program

Any time you write a 3D program, you are going to need to take the following steps. Some of them have already been documented, but we will go into more detail and provide some examples shortly.

  1. Initialization
    You will need to initialize Windows. Depending on your compiler, you may need to write (or cut and paste) certain functions and handlers. Then you will need to initialize Fastgraph. This is fairly simple, see the example. You may also want to initialize DirectDraw and Direct3D at this point, although it is by no means required.

  2. Define your space and your point of view
    This was described in the last chapter.

  3. Declare and initialize your objects
    Somehow, you need to have data for your walls, sprites, objects, or whatever else will go into your 3D world. You can create this in an external editor (such as FRED) and read from a file, or you can hard-code the data by typing it in, as we will do in the first example.

  4. Rotate and translate objects as needed
    For each face of each object in your world, call the appropriate Fastgraph functions to put the objects in the right place with respect to your point of view.

  5. Z-buffer and Z-clip the objects as needed
    The Z-buffering and Z-clipping are automatically handled during Fastgraph's projection. You merely need to enable Z-buffering and Z-clipping and define your clipping limits. More about this later.

  6. Project each object into a virtual buffer
    A virtual buffer is an area in memory that acts as a flat surface upon which you can draw or place bitmaps or project objects. When you project a face of an object, you can draw it either as a colored polygon, a Gouraud shaded polygon, or a texture-mapped polygon. (More about texture mapping later too!)

  7. Blit the virtual buffer to the screen
    This is the important step where all your hard work magically appears on the screen so you can see it.

  8. Optional: collect user input, and repeat the steps above
    Very few 3D programs are merely static projections of stationary objects. I will provide lots of examples of how to collect input and move about your 3D world, as well as allow objects in your 3D world to move independently.

  9. Shut down gracefully
    This is where you do your general housecleaning. You must close your virtual buffers and destroy them. Destroy whatever handles and data you have allocated. If you changed the screen resolution in step 1, put it back to what it was. Leave the system in the same state you found it.

That sounds like a tall order. 3D programs are not simple. But with Fastgraph's help, you should be able to work through each of the above steps without too much difficulty, and you will be writing 3D games in no time. Now, let's take a look at some code.

Source code for fgtutex1.c


#include "fgwin.h"
//----------------------------------------------------------------------
// Function declarations

LRESULT CALLBACK WindowProc(HWND,UINT,WPARAM,LPARAM);
void Init3D(void);
void InitVirtualBuffers(void);
void DrawCube(void);

// size of Window
#define winWidth 320
#define winHeight 300

#define vbWidth 312
#define vbHeight 273

//----------------------------------------------------------------------
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
                    PSTR szCmdParam, int nCmdShow)
{
   static char szAppName[] = "FGtutex1.c";
   HWND        hWnd;
   MSG         msg;
   WNDCLASSEX    wndclass;

   wndclass.cbSize        = sizeof(wndclass);
   wndclass.style         = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
   wndclass.lpfnWndProc   = WindowProc;
   wndclass.cbClsExtra    = 0;
   wndclass.cbWndExtra    = 0;
   wndclass.hInstance     = hInstance;
   wndclass.hIcon         = LoadIcon(NULL,IDI_APPLICATION);
   wndclass.hCursor       = LoadCursor(NULL,IDC_ARROW);
   wndclass.hbrBackground = NULL;
   wndclass.lpszMenuName  = NULL;
   wndclass.lpszClassName = szAppName;
   wndclass.hIconSm       = LoadIcon(NULL,IDI_APPLICATION);
   RegisterClassEx(&wndclass);

   hWnd = CreateWindowEx(WS_EX_TOPMOST,szAppName,"Fastgraph Tutorial Example 1",
      WS_OVERLAPPEDWINDOW,0,0,vbWidth,vbHeight,NULL,NULL,hInstance,NULL);

   ShowWindow(hWnd,nCmdShow);
   UpdateWindow(hWnd);

   while (GetMessage(&msg,NULL,0,0))
   {
      TranslateMessage(&msg);
      DispatchMessage(&msg);
   }
   return msg.wParam;
}

//----------------------------------------------------------------------
HDC      hDC;                  // handle of a windows device client
HPALETTE hPal;                 // handle of a windows palette
int      VBuffer;              // handle of a Fastgraph virtual buffer
int      ZBuffer;              // handle of a Fastgraph Z-buffer
UINT     cxClient, cyClient;   // width and height of client area

//----------------------------------------------------------------------
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam,
LPARAM lParam)
{
   PAINTSTRUCT ps;

   switch (message)
   {
      case WM_CREATE:
         hDC = GetDC(hWnd);    // find the handle of the window
         fg_setdc(hDC);        // tell Fastgraph the window handle
         hPal = fg_defpal();   // define a windows palette
         fg_realize(hPal);     // realize the palette


         InitVirtualBuffers(); // initialize the virtual buffers
         Init3D();             // initialize the 3D geometry
         DrawCube();           // draw a cube in the virtual buffer

         return 0;

      case WM_KEYDOWN:
         switch(wParam)
         {
            case VK_ESCAPE:
               DestroyWindow(hWnd);
               break;
         }
         return 0;

      case WM_PAINT:
         BeginPaint(hWnd,&ps);

         // blit the virtual buffer, scaling if necessary
         fg_vbscale(0,vbWidth-1,0,vbHeight-1,0,cxClient-1,0,cyClient-1);

         EndPaint(hWnd,&ps);
         return 0;

      case WM_SETFOCUS:
         fg_realize(hPal);    // realize the palette
         InvalidateRect(hWnd,NULL,TRUE);
         return 0;

      case WM_SIZE:
         cxClient = LOWORD(lParam);
         cyClient = HIWORD(lParam);
         return 0;

      case WM_DESTROY:
         fg_vbclose();        // close the virtual buffer
         fg_vbfree(VBuffer);  // free the virtual buffer
         fg_vbfin();          // shut down virtual buffers
         fg_zbfree(ZBuffer);  // free the Z-buffer
         DeleteObject(hPal);  // free the palette
         ReleaseDC(hWnd,hDC); // free the window handle
         PostQuitMessage(0);
         return 0;
   }
   return DefWindowProc(hWnd,message,wParam,lParam);
}

//----------------------------------------------------------------------
void InitVirtualBuffers(void)
{
   // initialize the virtual buffer system
   fg_vbinit();

   // establish the virtual buffer color depth at 256 colors
   fg_vbdepth(8);

   // allocate a virtual buffer the same size as the viewport
   VBuffer = fg_vballoc(vbWidth,vbHeight);

   // open the virtual buffer
   fg_vbopen(VBuffer);

   // initialize the vb colors
   fg_vbcolors();

   // fill the virtual buffer with white pixels
   fg_setcolor(-1);
   fg_fillpage();
}
//----------------------------------------------------------------------
void Init3D(void)
{
   // define 3D viewport
   fg_3Dviewport(0,vbWidth-1,0,vbHeight-1,1.0);

   // create and open the Z-buffer
   ZBuffer = fg_zballoc(vbWidth,vbHeight);
   fg_zbopen(ZBuffer);

   // set the Z clipping limits
   fg_3Dsetzclip(1.0,1000.0);
}

//----------------------------------------------------------------------
double CubeData[6][12]=
{
	{-1.0, 1.0,-1.0,  1.0, 1.0,-1.0,  1.0,-1.0,-1.0, -1.0,-1.0,-1.0}, // front
	{-1.0, 1.0, 1.0, -1.0, 1.0,-1.0, -1.0,-1.0,-1.0, -1.0,-1.0, 1.0}, // left
	{ 1.0, 1.0,-1.0,  1.0, 1.0, 1.0,  1.0,-1.0, 1.0,  1.0,-1.0,-1.0}, // right
	{-1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0,-1.0, -1.0, 1.0,-1.0}, // top
	{-1.0,-1.0,-1.0,  1.0,-1.0,-1.0,  1.0,-1.0, 1.0, -1.0,-1.0, 1.0}, // bottom
	{ 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0,-1.0, 1.0,  1.0,-1.0, 1.0}  // back
};

//----------------------------------------------------------------------
void DrawCube()
{
   int i;
   static int Colors[] = {19,20,21,22,23,24};

   // use the default render state (filled polygons with backface removal)
   fg_3Drenderstate(0);

   // set the point of view: at the origin looking down the z axis
   fg_3Dpov(0.0, 0.0, 0.0, 0, 0, 0);

   // put the cube at Z=10.0 and rotate it slightly
   fg_3Dsetobject(0.0, 0.0, 10.0, -300, 300, 300);

   // draw all the faces
   for (i = 0; i < 6; i++)
   {
      // set the color
      fg_setcolor(Colors[i]);

      // draw the face as a solid colored polygon
      fg_3Dpolygonobject(CubeData[i],4);
   }
}


With luck, you should be able to cut and paste the above code into your compiler. If you manage to compile and link with Fastgraph 6.0 for Windows, the result is a cube that looks like this:

Fig. 2.4 Output from fgtutex1.c.

I wrote the above program in straight C with some global variables. I did it that way because I wanted to make the program as short as possible (a bit of a challenge with Windows programs) so it would fit nicely on a web page. Later, I will give you some more interesting programs in C++. For now, let's dissect the above program and make sure we understand all the parts. I'll work down in order of the functions.

Notes on the source code

The first function in the above program is WinMain(). You should know how this works by now. If you don't, go look it up in a beginning book on Windows programming. Some compilers, like Borland C++Builder, hide this function from you. It's a nice one to hide.

The next function is WindowProc(). Again, you should generally know what this does before you embark on any kind of Windows programming. The interesting bits of our WindowProc() function are the parts that handle the Create, Paint and Destroy messages.

Under the WM_CREATE you see your first Fastgraph call: fg_setdc(). That function simply notifies Fastgraph which window to draw to. It doesn't have to be the first Fastgraph function you call, it just happens to be this time. If this were a DirectX program, you would need to call fg_ddsetup() as your first fastgraph function. See the Fastgraph manual for more information about that. Personally, I'm not too interested in DirectX, so we will skip that stuff for now.

After setting the device context with fg_setdc() we then initialize and realize the palette with fg_defpal() and fg_realize(). Again, see the Fastgraph manual for more information about how these work. They are only important in 8-bit SVGA programs (256 colors). If you are running in a high-color or true-color mode, you don't need to worry about the palette. But it does no harm to have those functions in there, and you may give your program to somebody using a 256-color display some day. It's best to be prepared to run under those conditions.

Next under WM_CREATE we call three functions to initialize the virtual buffers, initialize the 3D geometry system, and draw the cube. I'll get back to those in a minute. Virtually all the work in this program happens under WM_CREATE. All that is left after that is to wait for a keystroke and exit. Now let's take a quick look at the other Windows messages.

Under WM_PAINT we call fg_vbscale() to tell Fastgraph to take the virtual buffer and blit it to the screen while scaling it. So if the user stretches the corner of the window, our cube image will stretch with it. This is a good idea if you are using a scalable window. The unscaleable counterpart to fg_vbscale() is fg_vbpaste(). It is a little faster than fg_vbscale() because it doesn't have to calculate the scaling. I like to use fg_vbpaste() when I am writing a game that uses a full-screen mode, or a game that is in a fixed size in a window that can't be scaled. If you are trying to write a very fast FPS, consider using a fixed sized window so you can take advantage of the speed of fg_vbpaste().

Under WM_DESTROY we free all the memory and destroy all the handles. It is very important to clean up your virtual buffers, z-buffers, palettes, and of course, anything else you have allocated in your Windows program. Make it a habit to double check here so you don't miss anything.

The next function in our program is InitVirtualBuffers(). This calls the Fastgraph functions that initialize the virtual buffer system, define the color depth of the buffers, allocate memory for a virtual buffer, assign a handle to it, assign a color palette to it, and then open it and fill it with white pixels. This is all pretty simple and it is documented in the Fastgraph manual. Probably the most interesting thing here is the color depth. If the color depth of the virtual buffer does not match the color depth of the display, your program will work just fine. But it will be slower because Fastgraph will have to do "on the fly" conversions. It is not always possible to match the color depth of the virtual buffer to the color depth of the display. For example, if you want to display a JPEG file in a 256-color mode, you will need to make a high color or true color virtual buffer, and then let Fastgraph handle the color reduction on the fly. If you are interested in writing a high-speed 3D game, it is probably better if the color depth of the virtual buffer and the display match. For simplicity, we will force our color depth to 8 bits (256 colors).

The next function is Init3D(). This is where you use the functions we discussed Chapter 1. We initialize the 3D geometry system and the point of view. We establish a viewport, which is the area of the virtual buffer where 3D objects will be projected. We also allocate space for the Z-buffer and we set clipping limits for Z clipping. More about those concepts in the next chapter.

Finally, it is time to draw our cube in DrawCube(). We start by initializing the cube data as a set of points around the origin. These take the form of (x,y,z) coordinates of the four corners of the six sides. Remember to put these in clockwise order! The center of gravity is smack dab in the center of the cube, as in figure 2.2. We call the fg_3Drenderstate() function to set the render state, which in this case happens to be the default state: filled polygons with backface removal. We use the fg_3Dsetobject() function to tell Fastgraph the location and rotation of the cube. Then we set the color. Finally, we use fg_3Dpolygonobject() to display each face of the cube. That's it! Now you're a 3D programmer. Wasn't that easy?

Review

Objects in 3D space are usually built as a collection of polygons. Polygons can be defined in world space or object space. Once defined, polygons can be translated, rotated, and projected into a virtual buffer. The virtual buffer is then blitted to the screen. Incorporating this technology into a Windows program is not difficult. Fastgraph can greatly simplify the process.


 

Introduction
Chapter 1 | Chapter 2 | Chapter 3
Chapter 4 | Chapter 5 | Chapter 6 | Chapter 7
Appendix 1 | Appendix 2 | Appendix 3
Benchmarks
Fastgraph Home Page

 

become a computer game developer

copyright © 2007 Ted Gruber Software inc. all rights reserved.
This page written by Diana Gruber.