3D Programming Demos and Tutorials

Filling in
by Simon Brown

next part

This tutorial
Now we are going to cover how to take a set of three screen coordinates (x, y)'s and draw a solid flat-shaded triangle. This process varies depending on your programming environment. I'm going to cover the maths behind drawing filled triangles and leave the implementation to you. You won't be able to do anything with the maths unless you can plot pixels yourself, which varies depending on your target platform and programming language.

Horizontal lines
The easiest way to draw 2D triangles is to draw a series of horizontal lines to fill up the space occupied by the triangle as shown in Figure 2.1 below.

Figure 2.1 - Drawing a triangle using horizontal lines.

 

 

 

 


Types of triangles
Once you've put your 3D triangle through the 3D to 2D transformation, and got three screen (x, y) coordinates, you will have one of three types of triangle - flat-topped, flat-bottomed, or general triangles (which means neither the top nor bottom is flat). Figure 2.2 shows the three types of triangle.

Figure 2.2 - The three triangle types

 

 

 

 

 

 

 


Ok, so you can see above that two of the triangle types have flat horizontal edges. To draw a general triangle we split it in half horizontally to make one flat-topped and one flat-bottomed triangle. This means every triangle we draw has a flat horizontal edge and a point at the top or bottom.

Taking the Flat-topped triangle above as an example, basically, we start drawing at the point (x_3, y_3) and work out the gradient (change in x) of each the two outer edges as we decrease y (go upwards, in other words). So we will have a loop from y_3 to y_2. The reason that we use y_3 to y_2 and not y_3 to y_1 is that it also works for the flat-topped half of a general triangle, as you can see from Figure 2.2.

So the change in x value of the two edges per line are worked out like this-

change in left  edge per y = ( x_1 - x_3 ) / ( y_3 - y_2 )
change in right edge per y = ( x_2 - x_3 ) / ( y_3 - y_2 )

This is nothing more than simple linear interpolation. So we just have to keep track of the two x values of either edge of the line as we decrease y. We then feed the y value and the x values of the two ends of the line to a horizontal line drawing function (which we'll cover later).

Ok, next?
The first thing we need to do is to sort the three sets of coordinates in y order. This is fairly trivial.

float temp_x; // temporary variables, should be same type as x_1, x_2 etc.
float temp_y;

// Sort so y_1 is closest to the top of the screen, followed by y_2 and y_3


if ( y_2 < y_1 )   // Make sure point 2 is below (on screen) point 1
{

   temp_x = x_2;
   temp_y = y_2;
   x_2 = x_1;
   y_2 = y_1;
   x_1 = temp_x;
   y_1 = temp_y;
}

if ( y_3 < y_1 )   // Make sure point 3 is below (on screen) point 1
{

   temp_x = x_3;
   temp_y = y_3;
   x_3 = x_1;
   y_3 = y_1;
   x_1 = temp_x;
   y_1 = temp_y;
}

if ( y_3 < y_2 )   // Make sure point 3 is below (on screen) point 2
{

   temp_x = x_3;
   temp_y = y_3;
   x_3 = x_2;
   y_3 = y_2;
   x_2 = temp_x;
   y_2 = temp_y;
}

I didn't really have to show you that right? So now we've got our three (x, y)'s in ascending y order.

Horizontally splitting a "general" triangle
A general triangle must be split in half horizontally. This is a simple process. What we have to do is work out the x coordinate of the fourth point (x_4, y_2), shown in Figure 2.3 which is the only coordinate we haven't got if we are to make this triangle into one flat-topped and one flat-bottomed triangle. We already know the y value at this point, since it's the same as y_2.

Figure 2.3 - Splitting a "general" triangle in two horizontally.

 

 

 

 


So we now need to work out the x coordinate x_4 (shown above). We work out the ratio of the top height (from y_1 to y_2) divided by the total height (from y_1 to y_3). This tells us how far as a percentage the fourth point is along the edge it falls on. Since x_1 is always the top point and x_3 is always the bottom point (since we have sorted the three points by height), we know that x_3 to x_1 is the longest side, and the side which (x_4, y_2) must fall on.

So if we know the percentage along the line where x_4 falls and the x values at either end (x_3 and x_1) of the line then working out x_4 is straight forward. x_3 - x_1 gives us the length of the edge in x only, so multiply this by ratio will tell us how far along x_4 is (but not the actual coordinate) which we add to x_1 to give us the coordinate of x_4. Which looks like this in code-

float ratio = float ( y_2 - y_1 ) / float ( y_3 - y_1 ); // top height divided by total height

x_4 = ( ( x_3 - x_1 ) * ratio ) + x_1;

So there we go. That's all the preparation that's needed. We now have all the info we need to start drawing triangles. The process for drawing a flat-topped and flat-bottomed triangle is slightly different, so you will need two functions. For every triangle you call both the flat-topped and flat-bottomed triangle drawing functions, and the functions will work out for themselves whether they should be drawing anything. So if you've got only a flat-topped triangle, then when you call the flat-bottomed triangle function, it will work out that you are asking it to draw a zero height triangle and it will return without doing anything. You could equally well put this decision making prior to calling either function, and avoid a few function calls per frame, there's really no difference in speed, but there's less maths for me to explain when it's written this way.

This whole process is in no way 'set-in-stone', almost every software 3D engine you find will perform the same process but in a slightly different way.

Flat-Topped Triangle
As I described earlier, a flat-topped triangle involves a simple loop from y_3 to y_2. The starting x values for the end of the first horizontal line to be drawn are simply both x_3. We work out the gradient of both edges and then set the loop going.

// Draws a Flat-topped Triangle

void Draw_FT_Triangle ( void )
{

   float dx_left       = 0;   // left gradient
   float dx_right      = 0;   // right gradient
   float current_left  = 0;   // current left hand position
   float current_right = 0;   // current right hand position

   float height = y_3 - y_2;   // height of triangle

   if ( height < 0.5 && height > -0.5 ) return; // do not draw a zero height triangle

   dx_left  = ( x_4 - x_3 ) / height;
   dx_right = ( x_2 - x_3 ) / height;

   current_left = current_right = x_3; // both edges start at x_3

   for ( int i = y_3; i >= y_2; y-- ) // go from bottom to top
   {

      if ( i > -1 && i < 480 ) // Only draw line if on screen vertically
      {

         Draw_Line ( current_left, current_right, i );
      }

      current_left += dx_left;
      current_right += dx_right;

   }
}

As you can see, we keep track of the current x values of either edges of the line in the variables current_left and current_right, which, of course, must be floating point variables. We store the change in these variables per line (the gradient) in dx_left and dx_right. We return if we get a zero height triangle. Also we only draw a line if it is on the screen vertically, although these values change depending on your screen resolution. Since we use x_4 instead of x_1 to work out the first gradient we automatically take account of the fact that we may be drawing half of a general triangle. If we are not drawing half of a general triangle, then x_4 turns out identical to x_1 anyway, so the code works for both circumstances.

Flat-Bottomed Triangle
Drawing a flat-bottomed triangle is very similar. This time we will be looping from y_1 to y_2, so we are drawing from the top down this time. We are still drawing starting at the point and proceeding to the flat edge. Again by using y_1 to y_2 we take account of the fact that this may be either a flat-bottomed triangle, or the flat-bottomed half of a general triangle. Here's the code


// Draws a Flat-bottomed triangle

void Draw_FB_Triangle ( void )
{

   float dx_left       = 0; // left gradient
   float dx_right      = 0; // right gradient
   float current_left  = 0; // current left hand position
   float current_right = 0; // current right hand position

   float height = y_2 - y_1; // height of triangle

   if ( height < 0.5 && height > -0.5 ) return; // do not draw a zero height triangle

   dx_left  = ( x_4 - x_1 ) / height;
   dx_right = ( x_2 - x_1 ) / height;

   current_left = current_right = x_1; // both edges start at x_1

   for ( int i = y_1; i <= y_2; i++ ) // go from top to bottom
   {

      if ( i > -1 && y < 480 ) // Only draw line if it's on the screen vertically
      {

         Draw_Line ( current_left, current_right, i );
      }

      current_left += dx_left;
      current_right += dx_right;

   }
}

And that's how we draw a flat-bottomed triangle. This time we use x_4 instead of x_3 for the first gradient, again so our function will cope with being sent a flat-bottomed triangle, or the flat-bottomed half of a general triangle. It's also worth mentioning that although I've named my variables with left and right, this doesn't mean than left always refers to the left hand edge. For flat shaded triangles it doesn't matter which edge is which, the values are automatically swapped round if current_left happens to be greater than current_right when they are passed to the horizontal line drawing function. I've done it this way again to simplify the code.

Drawing a Line
Ok, this is probably the easiest bit, drawing a horizontal line. As you can see from the previous code, the line drawing function is passed the left hand edge position, the right hand edge position and the y value (called i in the previous code).

// This function draws a horizontal line

void Draw_Line ( int left, int right, int y )
{

   if ( !( left < 0 && right < 0 ) && !( left > 639 && right > 639 ) ) // line is visible
   {

      if ( left > right ) // make sure we are drawing left to right on the screen
      {
         int temp_x = left;
         left = right;
         right = temp_x;
      }

      if ( left < 0 ) // if left edge is off screen move it to left-hand edge of screen
         left = 0;

      if ( right > 639 ) // same for right edge of line
         right = 639;

      for ( int x_position = left; x_position <= right; x_position++ )
      {

         Plot ( x_position, y, colour );

      }
   }
}

And that's all there is to it. The only thing we have had to do here is make sure the line is on the screen horizontally and then make sure that left is actually the left hand of the two x values. Then we loop through the pixels in the line.

Implementation
As for how you implement Plot in your particular engine, it will depend on your target environment. In DOS and DirectDraw, you will have a pointer to the top-left pixel of your back-buffer and an offset to this pointer, which represents the current drawing position. So to draw a horizontal line you just keep incrementing the offset (the offset size must match the pixel size - 16 bit/32 bit etc) and setting the pixel colour. You will also need to know how much to increment the offset per y line as well, sometimes called the pitch. Say you are working in 16-bit colour, then the pitch is basically the difference between one point and the same point of the next y line in quantities of 16-bit's. Then to draw a horizontal line you would do-

offset = left + y * pitch;

for ( int x_position = left; x_position <= right; x_position++ )
{

   *(video_buffer + offset) = colour;
   offset++;

}

In DOS programming, the pitch will just be the screen width, since VGA memory is linear. In DirectDraw the pitch is not equal to the screen width. Instead it is the lPitch member of a DDSURFACEDESC2 struct, which is given in bytes, so divide by 2 for a 16-bit back-buffer. To plot pixels in DirectDraw you have to lock your back-buffer, and then you can request your video_buffer pointer and pitch form DirectDraw. If you don't know how to do that then chances are you don't know how to initialize DirectDraw anyway, so refer to the DirectX SDK under DirectDraw and Surfaces.

Note - although DirectX 8 doesn't have DirectDraw anymore, and it's 2D capabilities are limited, the capabilities needed for software 3D are still there, although the implementation has changed a little.

Conclusion
So, we now know the basics of taking some 3D points defined as (x, y, z)'s, transforming them into 2D screen coordinates, and then drawing a filled in triangle from these screen coordinates. You may have noticed that this engine will only work for objects that are straight ahead on the z-axis at the moment. This is deliberate and this is how the majority of 3D engines work. In the next tutorial I'm going to show you how to rotate and move your objects and the camera.

All content copyright © Simon Brown 1999-2005.
back to the introduction