Writing a 3D Engine


In this tutorial I'm hoping to give you some ideas about serious 3d engines, like their general structure, things to do, techniques, etc. By no means should you consider anything I say from now on as law cast in stone. First of all: you can write a 3d engine in as many ways as you want it considering anything from the application it's meant for, to your own taste. Obviously I wrote my own engine following my own criteria and the knowledge I had available. Second: I can't even hope to cover everything there is to know about 3d coding. And third: I don't even know a fraction of it :-). But anyways, I'll try to make this doc a useful resource from which you can at least get ideas that you can use in your own work.

It's pretty hard to structure an all-encompassing doc I'm trying to write, so my plan is to present my main engine structure (and alternatives, of course) in a main file and have other docs branching off as details. The thing is, though, that you won't see many of these details till I finish my whole engine. But you can always ask if you need anything specific. I'll see how things go...

Stuff to be Covered

Ok, now I'll outline the basic characteristics I planned for my engine (that is to say when am I gonna consider it "finished", cause I'm pretty sure I'll be adding new stuff as I learn it). This will give you an idea of what I'm gonna be talking about, and what you shouldn't expect anytime soon...

This is going to be a demo style engine, which should show any kind of objects in any kind of world without restrictions - the world's supposed to be totally dynamic. It also means that objects are generally composed of many small polygons. This means that BSPs and portals, for example, are out (don't worry if you're a beginner and aren't familiar with these terms, I'll discuss them later).

A little bit about object structure: The objects can be represented in hierarchies (and behave with appropriately controlled motion). They can be made of any type of polygon and mixtures of different types. By polygon type I mean its shading and texturing characteristics. Vertices are shared by polygons - no free polys by default. Hidden surface removal is done via backface culling and z-sorting. Clipping and culling of objects is done against the 3d view volume (or frustrum, frustum, whatever) to reduce unnecessary work.

Another note: the docs will present alternatives to my own methods (hopefully), but where optimization is concerned I can obviously only go as far as what I presently know and have implemented. But this time it won't be like in my previous docs, that I omit optimizations from the docs and code, cause I expect you to be familiar with the basics and be able to grasp what the optimized code's supposed to do, without much explanation. Another thing: if you're an expert in the field (and you're for some strange reason reading this :-), I'm totally open to suggestions about optimizations, different methods, whatever. Note: my source code and example 'pseudo' code is in C++, so you should be familiar with its basic concepts. Ok, time to jump right in!


This is the basic diagram of the processes going on in a 3d engine. It's all very simple, but I'm gonna use it to illustrate the different "chapters" of the doc. So why not go in sequence?...

Getting the data

You obviously need some data to display. All this data represents the world. The 3d world has to be created and saved sometime, so that's it's reusable at any time. There are any number of file formats you can use, but I didn't get into that yet, so I'll leave it for later.

Object structure is another important thing that has to be discussed. I'm sure you know the basic structures in 3d, like the vertex, polygon, etc. (If not, you shouldn't be reading this doc just yet, take a look at the previous tutorials). Maybe a little coding tip is in order here: when storing arrays (of anything other than basic types like int), it is almost necessary to use an array of pointers for speed and structural considerations. For example it's necessary to store vertices of polygons as pointers to the object's vertices, so that they are shared. Otherwise you'd have to transform each point many times. Plus, it's easier and faster to access pointers in general. Just an example. BTW, in practice it's more useful to make that array of pointers actually a pointer to a pointer. This way it can be dynamically allocated.

This is how i have my structures set up. This you really shouldn't take seriously, it'll just be useful for you to understand my further code examples. It's ugly and unorganized, maybe a lot of useless stuff is still left in there, but I'll get it together sometime... Some of the elements' function might be unclear to you for now, but they'll be used later. So here it is:

typedef struct Vtx2d             // 2d, projected vertex 
{
   int x, y;                     // coords
   unsigned char c;              // color, necessary for some shading routines
} Vtx2d;

typedef struct Texel             // texture coordinates
{
   int u, v;
} Texel;

class Vector
{
   Fixed x, y, z;                // whatever type you want

   // all the functions, like arithmetic, transformations, etc. defined as methods
   ...
};

class Vertex
{
  public:
   Vector world, obj;            // world and object coordinates
   Vector world_norm, obj_norm;  // world and object normals
   Vtx2d  scr;                   // projected, screen coordinates
   Texel  tex;                   // texture coordinates
   int clipped, out;             // visibility status flags
   int clipped_by[NUM_CLIP_PLANES];
};

class Poly
{
  protected:
   Vertex **v;                           // the vertex array - array of pointers, to be dynamically allocated
   int num_vertices;
   Vector world_norm, obj_norm;          // normals necessary for flat shading
   Fixed z;                              // depth value used for sorting
   int p, t, tx, e, f, g, ph, b;         // type flags - more than one can be enabled at the same time

  // some of the essential functions
  public:
   ... IS_A(...);                        // returns type information
   virtual void draw();                  // drawing function that's overloaded for different types
   ...
};

class TexPoly
{
   ...
   TexPoly(...):Poly() {}                // the Poly base class gets inherited by all types
};
...

class Object
{
  protected:
   Vertex *vertex; int num_vertices;       // array of vertices - the vertices are stored in the object structure, where they're indexed from by the polys
   Poly   **poly ; int num_polys;          // array of pointers to polys (this is necessary to be able to store different types of polys in the same array)
   Object **child; int num_children;       // the array of children as Object pointers
   Bitmap **texture;                       // the array of textures
   Bound  bbox, own_bbox;
   Matrix WrldMatrix;

  public:
   // essential functions
   void transform(const Matrix &M);
   void draw(...);                         // an object also draws itself, just like a Poly
   void make_box(...);                     // this creates a box object (maybe more appropriate as friend...)
};
As you can see above, the object should contain pointers to all its data: polygons, textures, child objects, etc., but I store the vertices as an integral part of the object. In fact it's the same with polys, but it's necessary to access them through pointers, so the polymorphism and virtual function system can be exploited from a simple base class Poly pointer. As you know from basic c++ thinking, a base class pointer can in fact point to any derived class, so this way I can have different types of polys 'stored' in the same array.

Ok, enough about code structure. On to physical object structure... An object is made of vertices and polygons, having links to textures, child objects and other things. Child objects are just like normal objects, but they are defined as part of, or subordinate to a parent object. We'll see later what all this entails, but the main thing is that you can group objects together like this in logical hierarchies. Take the classic hand example: The lower arm is subordinate to the upper arm; to the lower arm the wrist and hand are subordinate, from which tired, numb fingerlike structures are hangin' off... Anyways, this whole thing is supposed to behave as a unit, so it's logical to define it with the appropriate, hierarchical structure.

Getting the Transformations

Well this is a simple, and in another way very complicated topic. All I'm concerned about right here is the fact that I have to make my objects move in certain ways. Any movement can be defined by a rotation and a translation vector. How you get these is a whole other story that is totally application specific. It may be controlled by computer algorythms or human generated input. This all has to do with simulation which I'm not gonna get into. Every object has to be transformed in order to get a position in the world. This doesn't necessarily have to be controlled - objects can contain their own transformation information that is initialized and stays constant until it gets changed or incremented as needed.

Absolute, Constant Position

For example walls in a world have their predefined position and don't (normally) change. Since transformation information is contained in the object structure, you could say at init time: object[n].location=(12, 0, 1), object[n].rotation=(0, 90, 0) (this is all symbolic code, of course). This data will stay constant throughout the running of the program.

Relative Motion

A vehicle on the other hand can move around in which case a specific movement is added to its location and orientation information. This is incremental transformation, and it happens relative to present location and orientation, whatever those may be. For example if the vehicle was at (2, 0, 1) and facing straight along the z axis, and now it moves forward 1 unit and turns 5º, the new location will be (2, 0, 1)+(0, 0, 1)=(2, 0, 2) and it will be rotated around the y axis by 0+5=5º (further transformation with this example will get more complicated, but I'm not gonna get into that just yet).

Absolute Motion

Objects can also be controlled by absolute transformation, in which case the object gets placed into the world by specific location and orientation data. This happens relative to the world's center. An example: [frame 1] object[n].rotate(0, 0, 0), object[n].translate(2, 0, 1), [frame 2] object[n].rotate(0, 5, 0), object[n].translate(2, 0, 2). This absolute information is very likely to be generated by the computer, which might well use an incremental method for coming up with it (there could be exceptions, of course, like mathematical functions, parametric equations, random data, stuff like that). The difference from the relative, incremental method, is that the computer has total control over the object, it doesn't rely on its previous state.

Definition of Space and Transformation

But to understand the basic notion of transformations, I think a little more phylosophic and rigorous definition of space is necessary. As you know from your previous 3d coding, objects are defined by default in their own space, appropriately called object space. Here the object is centered on the origin, and all its vertices are defined relative to that point. When you translate that object into its final position in the world, that can be thought of as transferring it to world space. But you probably just thought of this as just moving the object around in one space, as we are familiar with in the real world. But when it comes to the mathematical representation of tranformations (and many of them!) this becomes inadequate.

First let's define what space itself is: a coordinate system defined by 3 axes. These axes are defined by vectors. In our real world we think of these as mutually orthogonal to one another and proportional. This doesn't necessarily have to be true... Let me put it this way: vectors are totally arbitrary and dependent on the space they're defined in. So a space defined by orthogonal vectors in a nonorthogonal space might become nonorthogonal in an orthogonal space. [figure to be drawn someday...] In fact the axes don't even have to be vectors, they can be defined by, say, functions. These can produce strange warped spaces, which I'm not even gonna get into. Suffice it to say that the relationship between spaces (thus the transformations) are defined by the relationships of their axes. [figures illustrating the basic transformations and their effects on relationships of coordinate axes]. A perfect way to represent a space (relative to another one, of course) is a 4x4 matrix. [matrix doc to follow, check out some others in the meantime].

Geometry Processing

What's understood under geometry processing is everything a vertex of an object goes through until it finally gets projected on the screen. That includes almost everything done in the engine, other than the rasterization of the final polygons.

Applying Transformations

An understanding of the notion of spaces and their relationships is important in correctly visualizing things like the order of transformations and their effects, for example. You have to understand which transformations happen in which spaces. I'll illustrate this with an example of the space transformations my vertices go through to get onto the screen. I'll be concerned with simple rotations and translations for now.

Transforming Into the World

The way I have things set up, I'm supposed to be able to control an object's orientation around its own axes and its exact position in space. To achieve the first transformation: We want the rotation to happen around the object's real axes; We know that this is the way the object is defined in object space, so we perform the rotations directly on the object coordinates. We don't put the resulting points back into the object data, but into separate world data.

vertex.world = vertex.obj.rotated_by(rotation_vector);

Now that we have the object rotated, we can put it into the world using a translation (translating the world data this time).

vertex.world += translation_vector;

What happens here is that the object's own frame of reference gets displaced in world space, along world axes. This way its origin will be located at the coordinates given by the translation vector, but all its vertices will still have the same relation to this point as they did to the origin in object space. (you might have to read that a few times :-)

Now I'd like to point out why the order is important: if you were to translate the object first, you'd move it off is space, its origin will be off center. The translation will happen along the object's original axes which coincide with the world axes. Rotations, as every transformation, operates on the present space. In this case that space is the world, whose origin doesn't coincide with the object's origin, as it did in object space, so the rotations won't happen around the object's intended axes, but around the world's axes. This of course gives a generally undesired effect - the object will seemingly move sideways (around the world's origin) as it rotates, so while its orientation will be right, its position will be also affected. You can check this with matrix multiplication. If you multiply a rotation matrix by a translation, its top 3x3 part, the axis/rotation definition, will not be affected. On the other hand, if you multiply a translation matrix by a rotation, the translation vector part of the matrix will actually get rotated. All this comes from the fact that matrix multiplication is not commutative. Thing to remember: rotate first, translate after.

Incremental Matrix Transformation

Another thing to be noted, is the difference between transforming by absolute vectors, and concatenating incremental matrices. As I said before, every transformation bumps the object into another space. From this follows, that more consecutive transformations will never happen around the same axes.

Say you already rotated and transformed the object, then want to move it around incrementally by say a key press. You create a matrix from the key input that is supposed to rotate the object by an adittional degree and move it forward by a unit. You multiply the object's transformation matrix (i'll call it the WorldMatrix, because it puts the object into the world) by this new little matrix.

This is a little pseudo-code of getting the incremental transformation matrix:
   while(!stop)
   {
      Vector rotate, translate;  //define it in here, so it gets automatically cleared every frame

      if(key_up_pressed)    translate.z=1;
      if(key_down_pressed)  translate.z=-1; 
      if(key_right_pressed) rotate.y=1;   
      ...
      Matrix rotation_matrix = create_rotation_matrix(rotation);
      Matrix translation_matrix = create_translation_matrix(translation);

      object.WorldMatrix *= rotation_matrix; //matrix concatenation
      object.WorldMatrix *= translation_matrix;

      object.draw();
   }
You might (or not) be surprised, that the object starts rotating around the world's center. The translation seems right, because the object moves into the screen a notch. Well, both of these may be desired effects in your engine, but I don't think so, especially for the first one. The wrong rotation happens, of course, because the object was in world space already, due to that previous transformation mentioned above. The other thing is, that if your object is a type of vehicle, it should move along its own axes. When a car is pointed 1 degree away, it doesn't keep going in a straight line (usually :-), as this object kept moving straight into positive z, no deviation in x. All this is due to the fact that the previous WorldMatrix in itself already defines space, so any further operation on it will happen relative to that space.

The way to avoid this is not to keep location and orientation in the WorldMatrix itself, but to keep it in two separate vectors. You increment these vectors and create a new WorldMatrix from them each time and you'll be fine.

Code for simple understanding:
   Vector rotate, translate; //define it out here, so it's static

   while(!stop)
   {
      if(key_up_pressed)    translate.z++;
      if(key_down_pressed)  translate.z--; 
      if(key_right_pressed) rotate.y++;   
      ...
      Matrix rotation_matrix = create_rotation_matrix(rotation); //create full transformation matrices, since "rotate" and "translate" are absolute 
      Matrix translation_matrix = create_translation_matrix(translation);

      object.WorldMatrix = Matrix();          //initialize to identity
      object.WorldMatrix *= rotation_matrix; 
    //an optimization would be to replace the above two lines with:
    //object.WorldMatrix = rotation_matrix;
      object.WorldMatrix *= translation_matrix;

      object.draw();
   }
The other fact, that the object doesn't rotate around its own original axes can't even be fixed even if you keep the translation in a separate vector and the rotation in a 3x3 matrix and increment that. This time the rotation won't be displaced, but it will still rotate around its axes as they are in its current state, not its original object world axes. This might be desired, though, in some applications, that's mainly why I presented it. The other reason is that it is often misused. Hopefully you understand the implications of incremental transformations now...

Camera Transformations

Ok, so I have my object placed and moving in the world the way I want it. Now all there's left is for me to do is look at it and be able to move around. I call this camera movement.

Since you wanna be able to view the world from any angle and position, but in your real view space you're really always located at the origin and looking at positive z, you have to transform the whole world to get it facing in front of you the way you want it. Well I don't even have to say it: we're using spaces, rotation and translation matrices.

The interesting thing, though, is that the order of transformations is reversed. If you think about it: you have to be placed in an absolute location in the world, then orient yourself in the view direction. This orientation has to happen around you, so you have to bring the origin of camera space to your position in the world. You do this, by a simple translation of the world. But it's the negative of your translation vector, because when you move forward, the world has to move back in order to be placed under you where you want it. Ok, now that your position is at the origin, you can safely do the rotation, and the world will rotate around you (you might have also experienced this in the event of hightened brain activity due to the effects of narcotic substances ;). Anyways, this also has to be a negative rotation: if you rotate right, the world has to rotate to the left in order for your intended viewed point to be straight in front of you.

The minor difficulty comes when you try to do further transformations. Consider this: you are at (0, 0, 1) and rotated 5 degrees around the y axis. If you do another forward motion, you will end up at (0, 0, 2) rotated by 5º. This is obviously not desired - you should move 5º to the right also, not just go straight. The way I solve this is to rotate my translation increment vector by the current rotation. So from the initial position (0, 0, 1), I will go forward almost one and a little to the side.

   void get_camera_matrix(Vector rotate, Vector move)
   {
      direction+=rotate;                    // increment rotation
      move.rotate(direction);               // rotate translation increment, so you're moving in the direction you're looking
      location+=move;                       // increment translation
   
      CameraMatrix=Matrix();                // clear camera matrix to identity
      CameraMatrix.translate(-location);    // translate world _in the opposite direction_ of where you're moving
      CameraMatrix.rotate(-direction);      // rotate        ||            ||
   }

This works ok for me, but you could use a much more logical solution. It involves incrementing the camera matrix by additional incremental transformation matrices. Now you see why I didn't use it, even though at the present I feel it's nicer. The reason this method works, is that the incremental method suits the translation-rotation order. As i said before, a subsequent rotation rotates the translation part of the matrix, which is exactly what i've achieved explicitly by rotating my translation increment. So what you should do is get the translation and rotation vectors from the keyboard as before, create the matrices from them, and multiply them in the order translation-rotation. Now concatenate this onto the camera's matrix. Don't clear the camera matrix, ever, if you use this method. (BTW, I changed my mind and I use this too now. It's just too simple to resist :-) But the above example is still zuseful for understanding what's really going on).

   void get_camera_matrix(Vector rotate, Vector move)
   {
      CameraMatrix.translate(-move);   
      CameraMatrix.rotate(-rotate);
      // BTW, incrementing a matrix made of low precision numbers such as fixed point can screw up
      // the integrity of the matrix, so you might have to orthonormalize it from time to time
   }

Ok, so now you have two matrices: WorldMatrix and CameraMatrix that together take your object straight into view space. What I do, is get every object's transformation as normal, then transform them by CameraMatrix. This concatenates onto the WorldMatrix, so now a transformation by WorldMatrix takes a vertex right into view space. If you wanna keep WorldMatrix clean in your object for incremental transformations, you'll have to create a whole new matrix in the object, say ViewMatrix that is the product of WorldMatrix and CameraMatrix. Same thing, just don't do the transformations separately on each vertex - the whole beauty of concatenation.

Pseudo code of my object drawing function (C++), which in fact does the transformations too, among everything else :-) :

   main()
   {
      ...
      for(int o=0; o < num_objects; o++)      // main object handling loop - "get transformation" stage
      {
         object[o].transform(WhateverMotion); // transform the object to world space
         object[o].transform(CameraMatrix);   // transform it to camera space
         object[o].draw();                    // render it
      }
   }
   ...

   Object::draw()
   {
      ...
      for(int v=0; v < num_vertices; v++)
         vertex[v].wrld = vertex[v].obj * WorldMatrix;   // the object coordinates always stay constant
      ...
      WorldMatrix=Matrix()                               // clear, because we're using absolute transformations
   }


Overview of Object to Camera Space Transformations

I'll repeat the order of all the transformations again:

                 WorldMatrix                 CameraMatrix
  OBJECT SPACE    -------->    WORLD SPACE    -------->    CAMERA/VIEW SPACE
                   Rot*Trl                     Trl*Rot


Hierarchical Transformations

(Note: I haven't implemented this yet, but I got a pretty good idea of how It's done. I'm pretty sure most of what I say below is true)

As I said way above, it makes sense to define objects in hierarchies. One reason is aesthetics in the code and application, but the main reason is so that you can transform objects as units. For example you have a body, with limbs as child objects; when you transform the body, it would be nice to have the limbs go along implicitly. This can be done pretty simply by transforming the children by the parent's transformation matrix. When the children are defined as part of a parent object, they are considered to be in the parent's object space. This means they are transformed relative to the parent's origin - WorldMatrix actually transforms to "parent object space".

Now when the parent is transformed, its transformation matrix gets passed down to all its children, where it multiplies to the child's existing transformation matrix. This will now put the child in world space. If the child itself has children, this newly created matrix gets passed down to them too. This can go for as many levels as needed.

In my engine this is pretty easy to code as I have an Object class with a draw() function. As I loop through all the child objects, I transform them by the current object's matrix, then send a message to draw themselves in turn. This creates a kind of recursive effect. In this recursion the place of the camera transformation is very important. It should only happen once to the parent object, as you can see in the above code, not inside the object drawing code (the way I have it now, as I just realized :-). This is a bit inconvenient because you have to do a monotonous transformation on every object in the main loop, so what you can do is check if the current object is a base object. If yes then concatenate the camera matrix on its world matrix inside the draw() routine. This is what i'm gonna do, and there are other ways... A little pseudo code:
   Object::draw()
   {
      // draw the object itself
      ...

      // draw its children
      for(int c=0; c < num_children; c++)
      {
         child[c]->transform(WorldMatrix); // child->WorldMatrix *= this->WorldMatrix
         // this of course messes up the child's world matrix, so you can't
         // use it incrementally anymore; there are ways around this
         child[c]->draw();
      }
   }

What all this means is that each child only moves by the amount specified by its WorldMatrix relative to the object above it in the hierarchy. This makes relative motion nicely controllable. Child objects should be defined in their object space as centered on their pivot point, with all its axes situated in logical directions. For example the shoulder should be at (0,0,0) of the upper arm's object space, with the x axis sticking out of the side, so a rotation around it would rotate the arm in the vertical zy plane. If you wanna shrug, you just translate it up on the y axis. Now that the object's orientation in object space is clear, we have to attach it to the parent. This is done simply by translating the whole thing to the coordinates of the joint in the parent's space. From now on no matter where the parent goes, the child will have the exact same relation to it.

              child.WorldMatrix                   parent.child_pos
  CHILD SPACE     -------->     RELATIVE POSITION    --------->    PARENT SPACE
               Rot and/or Trl                            Trl

You can see, that the child's WorldMatrix can be used as usual to move it relative to its own origin. What's needed is the other translation matix to put it where it's supposed to be in its parent's space. I think the parent should have the responsibility for its children's positions, so there will be position vector associated with each child. Here's the peudocode.

   class Object
   {
      ...
      Object **child;
      Vector *child_pos;                     // to be dynamically allocated
      ...
   };

   Object::draw()
   {
      ...
      for(int c=0; c < num_children; c++)
      {
         child[c]->translate(child_pos[c]);  // get the child into this parent object's space
         child[c]->transform(WorldMatrix);   // child goes wherever the parent does
         child[c]->draw();
      }
      ...
   }

I haven't tried this yet. It sounds good, but I'll update this if I implement it differently.

Visibility Determination

It's very important at rendering time to know if what you're about to draw is going to be at all visible - if not, you won't draw it in the first place :-), or take special measures if it's partially visible. These are the two types of measures: culling and clipping. If something's not visible, it will be discarded, but if it's partially visible, only parts of it will be discarded. This can be done at many levels: object, poly, vertex. Sometimes it's necessary for image quality, and often for speed improvement. I emphasize the speed improvement part and try to use it as much as possible. A lot of transformation can be avoided, more than 75%.

View Space Clipping/Culling

An obvious criteria an entity has to meet is to be in the field of view of the viewer. This field of view is represented by the view volume (frustrum/frustum/ whatever you wanna call it) in 3d space. It is defined by the projected planes from the screen's sides. Any more detail is more appropriate for the frustrum clipping tutorial [coming sometime, but till then here's a good one writtem by someone else]. Suffice it to say that a point can be tested for being outside the viewer's field of view, including behind him. Let's see, the objects behind the view plane of the viewer eliminate half the workload, with 180° of view left. I you consider a 90° fov or maybe 60°, that number's already under 25%

A good optimization for this is the use of bounding volumes [a separate doc to come on this too]. These are either cubes, spheres, or any other simplified object that accurately encompasses all of the object's vertices. This means that if any part of this bounding volume is out of the frustrum, it's fair to check if the object itself if out. If it's totally out, you can automatically ignore the whole object, and if the hierarchy is set up right, even its children. If it's totally in, you can be sre the whole object will have to be drawn, so no firther tests are necessary

But if the object is only partly out, all its vertices are checked too, to know exactly which ones stand where. If a vertex is out, I flag it and don't even transform it (more on how this is done later). This is not always correct, because that vertex might still be needed by a poly that's partially visible. For this case I have a fallback restore() function that gets all the data that's needed by a partially visible poly. If vertices weren't shared, all this could be done on a per-polygon basis in a more direct way, but I'm not about to abandon my object structure. (If you got a better idea plese tell me). Here's my pseudo code:

   #define IN 1
   #define PART 0
   #define OUT -1

   // 'clip' is the clipping status of the parent object
   // 'own_clip' is the status of the bounding volume encompassing only the current object's vertices

   void Object::draw(int clip=2)                 // the default, saying clipping status unknown - only happens for base objects
   {
      if(clip==2) {update_bound(); clip=PART;}   // get bounding volume 
      int own_clip=IN;

      if(clip==PART)                             // partial clipping of the parent object means uncertain status for the current object, so 'clip' has to be reevaluated
      {
         clip=MyFrustum.clip(bbox);              // clip now means the status of the bounding volume encompassing the object together with all its child objects
         own_clip=clip;                          // bbox is this all-encompassing volume
      }

      if(clip!=OUT)                              // if the whole object is not out
      {
         if(clip==PART)                          // if the whole object with children is partially in, 
            own_clip=MyFrustum.clip(own_bbox);   // there's still a chance for the main object (encompassed by own_bbox) to be totally in, eliminating unnecessary checks
   
         if(own_clip!=OUT)                       // as long as the main object is not out, try drawing it
         {
            for(int v=0; v < num_vertices; v++)  // go through all the vertices
            {
               if(own_clip==PART)                // if main object is partially out, check which parts of it are where
               {
                  if(MyFrustum.in(vertex[v]))    // checks if vertex is in the frustrum
                  {
                     vertex[v].wrld=vertex[v].obj * WrldMatrix;  // do transform and project
                     vertex[v].project();
                     vertex[v].out=0;            // flag as in
                  }
                  else
                  {
                     vertex[v].out=1;            // flag as out
                  }
               }
               else                              // if it's totally in, do everything automatically
               {
                  vertex[v].wrld=vertex[v].obj * WrldMatrix;
                  vertex[v].project();
                  vertex[v].out=0;               
               }
            }

            // if the object is not out, it's worth trying to draw its polys

            for(int p=0; p < num_polys; p++)
            {
               int p_in = poly[p] - > restore(WrldMatrix); // if restore says it makes sense
               if(p_in)
                  poly[p].draw();                          // then draw the poly
            }
         }

         // if the 'all-encompassing-volume' isn't out, it's worth trying to draw the children

         for(int c=0; c < num_children; c++)
         {
            child[c] - > draw(clip);                       // notice how I pass the current object's clipping status on to the children
         }
      }
   }


   int Poly::restore(Matrix &M)                       // accepts the WorldMatrix to restore the transformations
   {
      for(int i=0, ins=0; i < num_vertices; i++)
         if( !v[i] - > out )
            ins++;                                    // count the ins and outs
      
      if(!ins) return 0;                              // if it's all out don't even bother
   
      if(ins < num_vertices)                          // if some vertices are in, the rest have to be restored to have enough information to draw any poly at all
      {
         for(int i=0; i < num_vertices; i++)
            if(v[i]->out)
            {
               v[i]->wrld=v[i]->obj * M;
               v[i]->project();
               v[i]->out=0;
            }   
      }
   
      if(v[0]->clipped || v[1]->clipped || v[2]->clipped)  // if any of the vertices are out
      {
         MyFrustum.clip(this);                             // the poly has to be clipped
         return 0;                                         // the clipping routine manages the poly itself, so 0 is returned to not draw the old unclipped poly
      }
   
      return 1;
   }

Hmm, that's pretty complicated, it may take a while till you get why I do some things. If you don't it means I'm doing something stupid :-) It works, but I'm not saying it's perfect. I already see things to optimize. I welcome any suggestions about it, because this is one part of my engine that's truly my own creation.

The other thing I mentioned is clipping. You surely know that this is about cutting a polygon off at the side of the screen. It's easy to detect if a poly will be clipped by the status of its vertices' clipping flags. If they're all in or out it's easy. The actual clipping is done when one or more, but not all of the vertices are out. The actual polygon clipping can also be done using the frustrum planes, so it's described in the frustrum clipping tutorial. Of course clipping can also be done against the physical screen using any number of methods, but I don't consider that part of geometry processing.

Backface Culling

The other hidden surface removal technique you surely know is backface culling. Just as a refresher, polygons that are facing away from the viewer are not drawn. This reduces the number of faces to be processed by almost another half, so in the end you're not left with more than 15% of the work to do - that is poly filling and even tranformations. As you know, if a normal is facing away from the viewer, that vertex most likely won't be seen. This of course is determined by the dot product of the normal and view vector. That 'most likely' is a bit concerning, but I prepare for the eventuality in my Poly::restore() function that I mentioned before. It does the good'ol clockwise test, just to make sure the poly is front facing no matter what the normals say.

The reason vertex normals might be wrongly indicative of orientation is because they can be tilted for shading variation. Moreover they are usually not even parallel on the same poly, not to mention not being perpendicular to it. The reason for this is that vertex normals are shared by polys that are usually not parallel, so there's no way a normal could be paralled to all of them at the same time.

So I feel the double insurance is necessary in my case at least. Of course this can also be solved with the polys having their own normals. If the vertices are not shared, the whole poly can be discarded this way without worry. You wouldn't even need a backface test for every vertex. But I'm still not abandoning vertex sharing ;)

Ok, now to actually doing the backface test. Many people wrongly asume that if a normal is pointing more than 90° away from the view vector perpendicular to the view plane ((0, 0, 1) in view space), it's facing away. An even simpler test for this is if the normal in view space has a z coordinate larger than 0 - pointing into the screen. But all this is incorrect under perspective correction. Think about this: a plane perpendicular to your view plane, say x=0 - it stands edge on, its normal is (-1, 0, 0), the angle 90°. If that plane moves to x=1, you will actually be able to see its side, even though it's still exactly perpendicular to your view plane. In fact it could tilt quite a few degrees clockwise before it turned edge on and away.

So the thing to do is to get the vector that points from the view point (position of the viewer) straight to the polygon. This could be any point on the poly, or if you're doing the check on a per-vertex basis, it's even more straight forward. Now this is the vector you would check against the normal.

   for(int v=0; v < num_vertices; v++)
   {
      viewvector = vertex[v].world - viewpoint;
      if ( vertex[v].world_normal · viewvector < 0 )  
         vertex[v].out=1;
   }
Pretty simple, eh? I just wanted to get one considertation straight.
Here are the modifications necessary to the restore function to deal with culled vertices:

   int Poly::restore(Matrix &M)
   {
      for(int i=0, ins=0; i < num_vertices; i++)
      ...

      if(!ins) return 0;
      if(ins==num_vertices && cull()) return 0;  // even if the normals say everything's fine, this checks if the vertices are really clockwise; this check can only be done when all the vertex data is available

      if(ins < num_vertices)
      {
         ...
         if(cull()) return 0;                    // after reconstructing the vertex data, we can check for clockwiseness
      }
      ...
   }

Inverse Transformations

First of all, you gotta know that I'm obsessed with this stuff :-) I went through a lot of pain to get it working, but now I use it wherever I can. Ok, what it does is enable you to do things like visibility determination and shading without having to transform the object first - you can work with object coordinates.

The way to do this is to get the things you're comparing the object to (like the view point, light vector, frustrum, anything) into its own space. Since the object's WorldMatrix takes it into world space (or if multiplied with CameraMatrix, into view space), it means its inverse will put things from world or view space into object space. And view or world space is where you'll have most of these things.

Let's start with an easy example, the view point. This is always the origin of view space - (0,0,0). Since WorldMatrix, after being multiplied by CameraMatrix, will transform an object into view space, the inverse will put s point from view space into object space. The relation of this new point to the object's origin will be the same as if the object was in view space. So you cound say that object_space( viewpoint - (0,0,0) ) = view_space( (0,0,0) - transformed_origin ). This implies that viewpoint in object space will be -transformed_origin which equals -translation. So it's clear that the inverse transformation will (have to) create the effect of reverse motion. Same thing will happen with rotations. That was kinda obvious from the name, eh?

Ok, you can find out how to create the inverse of a matrix from my matrix doc [that will come someday, don't worry ;) If you need it urgently, just ask me]. Once you have the inverse matrix, you don't transform the objects, but whatever you wanted to compare them to. This creates obvious speed improvements, because you usually have more vertices than view points, for example :-). The other thing that's explained in the frustrum doc is how to inverse transform the frustrum too, so that you only compare object coordinates to it, only transfroming them when you know they're in.

Here's the drawing function with the inverse transformations this time:

   void Object::draw(int clip=2)
   {
      Matrix InverseWorldMatrix=WorldMatrix.inverse();
      Vector InverseLightVector=MyLight*InverseWorldMatrix;        // since the light is in world space, the world matrix' inverse has to be used
   
      // or
   
      Vector InverseLightVector=MyLight.rotate(InverseWorldMatrix) // if it's a directional light source, only the rotation should be applied; same goes to any directional vector
   
      transform(CameraMatrix);
      Matrix InverseWrldMatrix=WrldMatrix.inverse();
      MyFrustum.transform(InverseWrldMatrix);                      // the frustrum is defined in view space, so we have to use the whole view space transformation's inverse to get it into object space
   
      Vector InverseViewPoint=Vector(0, 0, 0)*InverseWorldMatrix;   // same with the view point
   
      ...
      if(MyFrustum.in(vertex[v]))                                    // this checks object coordinates against the above inverse transformed frustrum
      {
         if((vertex[v].obj_norm*(InverseViewPoint-vertex[0].obj))>0) // the object normal and point is used with the inverse transfromed view point
         {
             vertex[v].world=vertex[v].obj * WorldMatrix;
             vertex[v].project();
             vertex[v].out=0;
         }
         else
         {
             vertex[v].out=1;
         }
      }
      ...
   }

Polygon Sorting

Depth sorting is described in my earlier tutorial, so I'll only present optimizations here. Also, before you get into this, you should know that there are a lot of other methods for drawing polygons in right depth order, some of the even eliminating overdraw. Some of these are BSP trees, portal rendering, s- and z-buffering. You can check out some docs about 'em.

Right now I just use simple z-sorting. I create a buffer of polys to output, get their average z values, and sort them using any conventional sorting algorythm. The key in my opinion to sorting speed is what you're actually sorting. Do not, by any means, go sorting, switching full polygon structures.

What I do is store pointers to the polys in an array, as they are provided. The size of this array is determined at setup time by each object's constructor adding its number of polys to the size of the list, ensuring everything will fit. It's not quite dynamic enough, but it's not hard to initialize after all. The other choice would be linked lists which are very dynamic, but could be sorting slower, depending on the number of links, because you have to reassign more pointers. With the array of pointers, I just switch two pointers like I did with integers in my beginner's computer course. In the end I just go through the array with a for loop and draw all the polys in order.

There's a good doc by Voltaire/OTM on fast sorting algos useful for this kind of application, like many types of radix sort. It's definately worth checking out if you wanna use anything other than C's qsort or even better, a good ol' bubble sort like me :-)



Further References

  • Check out my collection of docs for all kinds of categories



    This page was last updated on 12/06/97

    Back to Steel's Programming Resources
    Click Here!