Tuesday, April 16, 2024 | Toby Opferman
 

2D Primatives

Toby Opferman
http://www.opferman.net
toby@opferman.net


				2D Primatives

	In this section we introduce some code and we introduce some
more theory on 2D.  Then in the next section, we introduce 3D using the
2D primatives.


	Lines, Circles, Rectangles, Triangles, and pixels are all graphics
primatives.  Everything can be made up of these primatives, even 3D graphics.


	Let us start out with Lines.  A line in computer graphics is really
a line segment.  It has a start point, an end point and a slope.  Now,
we need a way to draw a line on the screen.  Well, if the line is horizontal,
that is from (X1, Y1) to (X2, Y2)  Y1 = Y2, we have a horizontal line.


We can determine the length of this line is X2 - X1 and draw it such that:

FOR X = X1 to X2
  PutPixel(X, Y1)
END FOR

In a general pesudo code.

If a line is Verticle, that is X1 = X2 for (X1, Y1) - (X2, Y2)  we can
draw it such that

FOR Y = Y1 to Y2
  PutPixel(X1, Y)
END FOR


Now, when we work with graphics, we are going to be dealing with linear
memory.  That is, Y is defined as Y*MaxX.  This is because in order
to get to the line we need, we have to Multiply Y by the length of the width
of the screen.  Then, we add X.

Let's say we have this screen:
8x5: (All screens mxn are indexed 0 to (m-1), 0 to (n-1))

  0  1  2  3  4  5  6  7 
0 
1
2
3
4

And let's say it's actualy a linear screen.  That is, the values to get
to a position are:


   0  1  2  3  4  5  6  7  
0  0  1  2  3  4  5  6  7  
1  8  9 10 11 12 13 14 15
2 16 17 18 19 20 21 22 23
3 24 25 26 27 28 29 30 31
4 32 33 34 35 36 37 38 39


Now, to get a location on the screen, you have "Screen".
You need to add 1 number to it go to go to the location.
That is, this matrix is just imaginary, arbitrary, we only like to picture
it as a matrix, really, it's just a straight buffer from 0 to 39.

If Y = 0 and X = 0 then Screen + Y + X = 0

Now, the max X is 8.  So,

Screen + Y*8 + X  = Location.  Let's see:

Y = 3, X = 4   Y*8 + X = (3)*8 + 4 = 24 + 4 = 28


Look back above or at this lower simplified graph.

   0  1  2  3  4  5  6  7  
0 
1 
2 
3             28
4 

That is the correct linear combination.  

8 = 2^3  So Y (Bit Shift Left 3)  is the same thing.  

Let us look at that way:

Y = 3, X = 4

Now, instead of multiplying 8, we will bitshift.

Y<<3 + X

3 in Binary = 00000011

Shift if left 3 places

00011000
2^4+2^3 = 16 + 8 = 24 
24 + X = 24 + 4 = 28

there's your answer.  Why show you this?  Well, graphics is usually very
computational and processor consuming.  Bit shifting is faster than multiplication
on a computer especially.  This tutorial is NOT going to teach you how to do this,
I am just showing you that it is your best interest to look at things in different
lights and learn certain optimizations.  If you don't know about binary, you should
learn.  There is a tutorial under DOX that has Base Conversions on it (Assembly
tutorial 1) that you should read.


Second thing I want to mention is a Double Buffer.  Video memory is slow
and plus you don't want to be drawing on it while someone is viewing it.
You want to have an offscreen linear buffer the same size as the screen.
That way, you can draw on that and just blast that to the screen all at once.
And it's faster, video memory is always being accessed by the video card.

Ok, back to drawing lines.  If we want to draw a diaginal line, it's a bit
of work, but a guy named Bresenham came up with an algorithm to draw lines.

To draw a line, you want to get the change in X and the change in Y.

Here is the theory behind the algorithm.

You have a line (X1, Y1) to (X2, Y2)
For our example we will use 

(10, 4) to (20, 10)

We get a Delta X and a Delta Y (Delta means change, so Change in Y and Change in X)

Delta X = 20 - 10 = 10
Delta Y = 10 - 4 = 6

The Delta X is greater than Delta Y so we will plot along the X axis.
That way, we have more room to adjust the error for Y's descent.

So, we want a loop from 0 to 10.

At first, we just plot at (10, 4), the starting point.

  10 11 12 13 14 15 16 17 18 19 20
4  *
5
6
7
8
9
10

Now, we have an error counter. Which we will start off at 0, but now every time
we plot we will add the change of the variable that is NOT being plotted against.
Hence, we are moving along the X acess.  What we want to do is divide up
Y so that it is evenly divided amonst the delta of X.

Error = Error + Delta Y

Error is now 6.

We now increment X1 by 1 (Positive since we are going in the positive direction)

X1 is now 11.

Second time thru the loop we are at
(11, 4)

We plot:

  10 11 12 13 14 15 16 17 18 19 20
4  *  *
5
6
7
8
9
10

We add the error
Error = Error + Delta Y
Error = 6 + 6 = 12

Now, Error > Delta X.

So, we need to do some adjusting of the line.
We want to subtract DeltaX FROM the error to reset it
to a correct error position.
Error = Error - Delta X
Error = 12 - 10 = 2
Error = 2

Now, we want to increment Y1 by it's direction (Which is Positive) so we
add 1.

Y1 = Y1 + 1

And again, we add 1 to X1.

Now, we have
(12, 5)
We plot

  10 11 12 13 14 15 16 17 18 19 20
4  *  *  
5        *
6
7
8
9
10


We add the error
Error = Error + Delta Y
Error = 2 + 6 = 8

Error is less than Delta X

Add 1 to X1


Now we have
(13, 5)

  10 11 12 13 14 15 16 17 18 19 20
4  *  *  
5        *  *
6
7
8
9
10

We add the error
Error = Error + Delta Y
Error = 8 + 6 = 14

Now, Error > Delta X.

So, we need to do some adjusting of the line.

Error = Error - Delta X
Error = 14 - 10 = 4
Error = 4

Now, we want to increment Y1 by it's direction (Which is Positive) so we
add 1.

Y1 = Y1 + 1

And again, we add 1 to X1.


Now we have
(14, 6)

  10 11 12 13 14 15 16 17 18 19 20
4  *  *  
5        *  *
6              *
7
8
9
10


We add the error
Error = Error + Delta Y
Error = 4 + 6 = 10

Error is equal to Delta X

So, we need to do some adjusting of the line.

Error = Error - Delta X
Error = 10 - 10 = 0
Error = 0

Add 1 to Y1

Add 1 to X1


Now we have
(15, 7)

  10 11 12 13 14 15 16 17 18 19 20
4  *  *  
5        *  *
6              *
7                 *
8
9
10



We add the error
Error = Error + Delta Y
Error = 0 + 6 = 6

And again, we add 1 to X1.

Now we have
(16, 7)

  10 11 12 13 14 15 16 17 18 19 20
4  *  *  
5        *  *
6              *
7                 *  *
8
9
10



We add the error
Error = Error + Delta Y
Error = 6 + 6 = 12

Error is greater than Delta X
Error = Error - Delta X = 2
Y1 = Y1 + 1

Add 1 to X1


Now we have
(17, 8)

  10 11 12 13 14 15 16 17 18 19 20
4  *  *  
5        *  *
6              *
7                 *  *
8                       *
9
10

We add the error
Error = Error + Delta Y
Error = 2 + 6 = 8
+++

And again, we add 1 to X1.


Now we have
(18, 8)

  10 11 12 13 14 15 16 17 18 19 20
4  *  *  
5        *  *
6              *
7                 *  *
8                       *  *
9                          
10

We add the error
Error = Error + Delta Y
Error = 8 + 6 = 14

Error > Delta X 

Error = Error - Delta X = 4
Y1 = Y1 + 1


And again, we add 1 to X1.

Now we have
(19, 9)

  10 11 12 13 14 15 16 17 18 19 20
4  *  *  
5        *  *
6              *
7                 *  *
8                       *  *
9                             *
10



We add the error
Error = Error + Delta Y
Error = 4 + 6 = 10

Error = Delta X 
Error = Error - Delta X = 0
Y1 = Y1 + 1


And again, we add 1 to X1.

Now we have
(20, 10)

  10 11 12 13 14 15 16 17 18 19 20
4  *  *  
5        *  *
6              *
7                 *  *
8                       *  *
9                             *
10                               *


And now X = 10, so the loop is done and so is the line.

As you can see, it's a pretty good algorithm.


Here is the Pseudo Code:



Delta X = X2 - X1
Delta Y = Y2 - Y1

IF Delta X < 0 THEN
   Sign Delta X = -1
ELSE
   Sign Delta X = 1
END IF

IF Delta Y < 0 THEN
   Sign Delta Y = -1
ELSE
   Sign Delta Y = 1
END IF

Delta X = Sign Delta X * Delta X + 1
Delta Y = Sign Delta Y * Delta Y + 1

ERROR = 0

IF Delta X >= Delta Y THEN
   FOR X = 0 TO Delta X - 1
       Plot(X1, Y1)

       ERROR = ERROR + Delta Y

       IF ERROR >= Delta X THEN
          ERROR = ERROR - Delta X
          Y1 = Y1 + Sign Delta Y
       END IF
  
       X1 = X1 + Sign Delta X
   END FOR
ELSE
   FOR Y = 0 TO Delta Y - 1
       Plot(X1, Y1)

       ERROR = ERROR + Delta X

       IF ERROR >= Delta Y THEN
          ERROR = ERROR - Delta Y
          X1 = X1 + Sign Delta X
       END IF
  
       Y1 = Y1 + Sign Delta Y
   END FOR
END IF



The next thing you will want to learn is how to make a triangle.
Since any polygon is just a bunch of triangles formed together, you
get a fast triangle drawing function you can draw them all.


Now, obviously when I say draw a triangle I'm not talking just connecting
3 lines.  I am talking about drawing the real solid triangle.


You will laugh at this, but not only do we have to split polygons up into
triangles, but we have to split triangles up into triangles!

See, we want to be able to draw horizontal lines very easily.
So, we make that we have 2 types of triangles. Flat Top and Flat bottom.
If a triangle isn't one of those two, we make it one.

Exmaple:

Take this triangle:

|\
| \
|  \
|   \
|    \
|     \
|     /
|    /
|   /
|  /
| /    
|/



Divide it like so:
|\
| \
|  \
|   \
|    \
|_____\
|     /
|    /
|   /
|  /
| /    
|/



Now you have 2 seperate triangles:

|\
| \
|  \
|   \
|    \
|_____\



 _____
|     /
|    /
|   /
|  /
| /    
|/


A flat bottom and a flat top.


How do you split it? You just need 1 more point.

First thing, you want to sort the points.

You have 3 points.  (X1, Y1), (X2, Y2), (X3, Y3)


This is how you sort them:

IF Y1 > Y2 THEN
  Swap(Y1, Y2)
  Swap(X1, X2)
END IF

IF Y1 > Y3 THEN
  Swap(Y1, Y3)
  Swap(X1, X3)
END IF

IF Y2 > Y3 THEN
  Swap(Y2, Y3)
  Swap(X2, X3)
END IF


To test for a Flat Top Triangle:

IF Y1 = Y2 THEN
  FlatTop
END IF

To Test For A Flat Bottom Triangle:

IF Y2 = Y3 THEN
  FlatBottom
END IF

Also, Remeber:

IF Y1 = Y2 AND Y2 = Y3 THEN
  This is a line
END IF

IF X1 = X2 AND X2 = X3 THEN
  This is a line
END IF

Also, if Any 2 Vertexs are Equal, Then that is a line as well.


IF X1 = X2 AND Y1 = Y2 THEN
  This is a line
END IF

IF X2 = X3 AND Y3 = Y2 THEN
  This is a line
END IF

Y3 Cannot Equal Y1 unless Y2 Equals Y1 since you sorted them.



Now, you have them sorted, say you have these triangles:

(5, 10), (5, 15), (10, 15) Flat Bottom
(5, 10), (5, 10), (10, 15) Flat Top
(5, 8), (6, 12), (15, 15) Irregular

Well, on an irregular triangle, the middle point, (X2, Y2) has to create
the new point to split the triangle.

We will call this point (NewX, NewY)

For Obvious reasons, NewY = Y2.

We are splitting these Horizontally.  So, NewY will be the same as Y2.

Now, we need to computer NewX.

Here is our triangle:

   5  6  7  8  9 10 11 12 13 14 15
 8 *  
 9     
10  
11  
12    * 
13      
14     
15                                *

I can't put the lines in so you are going to have to imagine.

Now, you know that the NewX should be on line 12 and on the line between
X1 and X3.


   5  6  7  8  9 10 11 12 13 14 15
 8 *  
 9     
10  
11  
12    *         
13      
14     
15                                *

(5, 8), (6, 12), (15, 15) Irregular

(X3 - X1) = Delta X (10)
Now, you can divide 10 by 15.

(Delta X)/Y3

10/15 = 0.666666666666666666666666666666667

That is the change in X from X1 to X3

So, .6666 * Y2 = NewX

That's .6666 * 12 = 8

NewX = 8



   5  6  7  8  9 10 11 12 13 14 15
 8 *  
 9     
10  
11  
12    *     *    
13      
14     
15                                *



Now, the triangle is split.

Split Triangles are drawn like so:
FlatBottom (X1, Y1), (X2, Y2), (NewX, Y2)
FlatTop    (X2, Y2), (NewX, Y2), (X3, Y3)

Now, you want to draw a triangle with a flat bottom.

What we need to do is the same thing as the line algorithm did.


             (X1, Y1)


(X2, Y2)                    (NewX, Y2)


We start at (X1, Y1).  We plot a dot.  Now, we need to change the Length
of our drawing gradually so the  Right side will end up at (NewX, Y2).
AND, we also need to change the start position of our drawing so the Left side
ends up at (X2, Y2).

NewX and X2 in Ascending order.

IF X2 > NewX THEN
  Swap(X2, NewX)
END IF

Next, you want to find out Delta X2 and Delta NewX
Delta Y = (Y2 - Y1)
Delta X2 = (X1 - X2)/(Delta Y)
Delta NewX = (NewX - X1)/(Delta Y)

Just like the line.  We make it porportional.  We get the distance from X1 to X2 (And X1 to
NewX)  And we make it porportional. i.e., every New Y, it goes up a portion.  This is what
we need.  (Now, triangles NEED to work with floats.  But, the Line aglorithm I showed you
works fine with integers.)

(5, 8), (6, 12), (15, 15) Was our triangle.
We now have:

(5, 8), (6, 12), (8, 12) Flat Bottom
(6, 12), (8, 12), (15, 15) Flat Top

Delta Y = (12 - 8) = 4
Delta X2 = (6 - 5)/4 = 1/4 = .25
Delta NewX = (8 - 5)/4 = 3/4 = .75
Get an End X.
EndX = Delta NewX + X1 + .50 = .75 + 5.50 = 6.25
Add on .50 to round up.
Length = 1

Now, we start the loop.
Now, as you can see, if we every line do the following:

Plot(X1, Y1) Length 1

Y1 = Y1 + 1 = 9
X1 = X1 + Delta X2 = 5.25
Length = EndX - X1 = 6.25 - 5.25 = 1 (Add on 1 so you plot 2 pixels) +1 = 2
EndX = EndX + Delta NewX = 5.75 + .75 = 6.50


Next round:

Plot(X1, Y1) Length 2
These are rounded to integers so,
Plot(5, 9)

Y1 = Y1 + 1 = 10
X1 = X1 + Delta X2 = 5.25 + .25 = 5.50
Length = EndX - X1 = 6.50 - 5.50 = 1 (Add on 1 so you plot 2 pixels) + 1 = 2
EndX = EndX + Delta NewX = 6.50 + .75 = 7.25


Next Round:

Plot(6, 10) Length 2


Y1 = Y1 + 1 = 11
X1 = X1 + Delta X2 = 5.50 + .25 = 5.75
Length = EndX - X1 = 7.25 - 5.75 = 2 (Add on 1 so you plot 2 pixels) + 1 = 3
EndX = EndX + Delta NewX = 7.25 + .75 = 8


Next Round:

Plot(6, 11) Length 3

Y1 = Y1 + 1 = 12
X1 = X1 + Delta X2 = 5.75 + .25 = 6
Length = EndX - X1 = 8 - 6 = 2 (Add on 1 so you plot 2 pixels) + 1 = 3
EndX = EndX + Delta NewX = 8 + .75 = 8.75

Plot(6, 12) Length 3

And it looks like this:

   5  6  7  8  9 10 11 12 13 14 15
 8 *  
 9 *  *      
10    *  *  
11    *  *  *      
12    *  *  *    
13      
14     
15                                *


Here is pseudo code:

IF X2 > X3 THEN
  Swap(X2, X3)
END IF

Delta X = (X2 - X1)/(Y2 - Y1)
NewDelta = (X3 - X1)/(Y2 - Y1)
BegX = X1
EndX = NewDelta + X1 + .5
Length = 1
    
FOR Y = Y1 TO Y2
  Plot(BegX, Y, Length)
  BegX = BegX + Delta X
  Length = (EndX - BegX) + 1  
  EndX = EndX + NewDelta
END FOR



Seems simple enough.  But something is missing.  If you do run that, you do see that
some bigger triangles look ok and some triangles look a little messed up.  We need
to perfect this so the triangle will be more perfect.  Can you think of any ideas?
Sure, we can try the .5 to .75 with a little bit of difference, but not what we want.
These triangles are pretty rigid, see if you can figure out a way to make them more
accurate.

These triangles need to be pretty decent together because, we are supposed to
be able to fit triangles together to make polygons.  If they aren't working
propertly, you will see holes in them, pixels missing.  


The next would be to do the flat top triangles and as you figured it's pretty much
the same as the flat bottom triangles.

Well, we have A line from (X1, Y1) to (X2, Y2).

We want these to merge to (X3, Y3).


But, instead of wasting my time telling you how to do this, I will now go into another
method.



First thing first, like always, get deltas.  The change of X as Y changes by 1.
That is how we work things.  Just like I stated in the line drawing and in 
the flat top.  We want how much X changes as we change Y by 1 so we can
appropriately increment X.  Now, if it is not 1:1 (That is, Delta X = 1,
which means for every Y + 1 you have X + 1 (A perfect situation!)), we don't have
.5 of a pixel, so we must approximate and round.  That is why the .5 was added onto DeltaX
of the one above.  We need to figure out a good approximation.


But, I am not going to show you bottom triangle code.  Instead, we are going to step
up a bit and try to fix the problem.  I only made the crap code above for better
visualization.  What if we forget about creating a middle X.  What if we forget
about checking for bottom and top triangles?  Well, let's see this better Pseudo Code.


* First, like above, we sort.

IF Y1 > Y2 THEN
  Swap(Y1, Y2)
  Swap(X1, X2)
END IF

IF Y1 > Y3 THEN
  Swap(Y1, Y3)
  Swap(X1, X3)
END IF

IF Y2 > Y3 THEN
  Swap(Y2, Y3)
  Swap(X2, X3)
END IF

* And again, we need the deltas, but instead of splitting the triangle, let us
* get the deltas for each line.


Delta X1 = X2 - X1
Delta X2 = X3 - X2
Delta X3 = X3 - X1
Delta Y1 = Y2 - Y1
Delta Y2 = Y3 - Y2
Delta Y3 = Y3 - Y1

* Now, Let use create the Deltas to be used in the loops

IF Delta Y1 IS NOT 0 THEN
   D1 = Delta X1 / Delta Y1
ELSE
   D1 = 0
END IF

* Now, you can't divide by 0, so we need a check in.

IF Delta Y2 IS NOT 0 THEN
   D2 = Delta X2 / Delta Y2
ELSE
   D2 = 0
END IF

IF Delta Y3 IS NOT 0 THEN
   D3 = Delta X3 / Delta Y3
ELSE
   D3 = 0
END IF

* Now, we have the loops.  We want to loop to where we WOULD have split, then
* change how we create the lines and loop again.

* The Flat Bottom Triangle
FOR V = Y1 TO Y2

*  You start at X1 and you Add the current Y value subtracted by Y1.  Then you multiply
*  The change in X as Y approcahes the maximum. 
   
  PlotX1 =  X1 + (V - Y1)*D1;
  PlotX2 =  X1 + (V - Y1)*D3;  

  HorizontalLine(PlotX1, PlotX2, V) 
END FOR

* The Flat Top Triangle
FOR V = Y2 TO Y3

*  You start at X2 and you Add the current Y value subtracted by Y2.  Then you multiply
*  The change in X as Y approcahes the maximum. 

  PlotX1 =  X2 + (V - Y2)*D2;
  PlotX2 =  X1 + (V - Y1)*D3;  

  HorizonalLine(PlotX, V)
END FOR



NOTE: The parameters for this arbitrary Horizontal Line Function are:

HorizontalLine(X1, X2, Y)

If you don't FULLY understand this, it is ok.  With modern APIs you don't have to anyway.
And even with no APIs, you can just use the psuedo code I gave.  I made the first one,
with the shitty right side, in ten minutes for this tutorial.  But, the above version
I did not make myself, but it is a better version.  I leave optimizations up to you
now however.  The above works on the same principal as the one I came up with, but it
works better.

For primative 3D beginnings a circle function is not needed.  If you want to draw a circle,
A SIMPLE function would just be:

FOR THETHA = 0 TO 360
  PlotPixel(Radius*COS(THETA), Radius*SIN(THETA))
END FOR


(Notice in my examples I ignore a "color" parameter, these are arbitrary functions,
so they are omitted.)


There is a more optimized way to do a circle, with integers.  As you also could tell,
that will only plot 360 pixels.  If your circle has a radius that on a computer screen
would need > 360 pixels, you would see start to see spaces between the pixels of the circle.

This tutorial has served it's purpose and theory on drawing a circle is irrevevant and
is left up to your own research if you are interested.  Or, you could think of how to do
it on your own.

For the basics of 3D, A line drawing function and a triangle drawing function are definately
what you need.  You need to get really familar with the triangle function.  Since, not
only is it used to draw a solid surface, but texture mapping needs to be drawn in combination
with it, and so does lighting.  It's pretty much the most important primative.  Now, yes,
once you get to graphics APIs like DirectX and OpenGL you don't have to worry about it anymore
since most things are done for you.  This is behind the scenes tho, you should know how things
work so you have a better understanding even if you just use an API.  

Also remeber, these routines are NOT optimized, they are even just pseudo code.
Another important aspect of 3D and game programming alone is speed.  I would always
look for faster and better methods.
 
About Toby Opferman

Professional software engineer with over 15 years...

Learn more »
Codeproject Articles

Programming related articles...

Articles »
Resume

Resume »
Contact

Email: codeproject(at)opferman(dot)com