# Lecture 2

## 2D Graphics

(includes text and images from "Computer Graphics: Principles and Practice" by Foley et all)

online, you can click here to see some images created by members of the Electronic Visualization Laboratory here at UIC. Each lecture has a different set of images (collect em all!)

### Progression of Topics

Last time we talked briefly about the hardware and in particular about the frame buffer and how it physically works.

Now we move up a level of abstraction to talking about how lines and polygons are drawn in the frame buffer.

Then we will discuss how 2D and 3D models are converted into forms suitable for drawing into the frame buffer.

Then, after the midterm, we will discuss how to make these 2D and 3D models more realistic looking to the viewer.

but all of this depends on being able to manipulate the frame buffer correctly.

### Mathematics VS Engineering

We like to think about a scene as mathematical primitives in a world-space. This scene is then rendered into the frame buffer. This allows a logical separation of the world from the view of that world.

mathematically, points are infinitely small
mathematically, line segments are infinitely thin

these mathematical elements need to be converted into discrete pixels

as usual, there is an obvious easy way of doing these conversions, and then there is the way it is actually done (for efficiency.)

### Scan Conversion (rasterization) of a line

take an analytic (continuous) function and convert (digitize) it so appropriate pixels can be illuminated in the frame buffer

In a language such as OpenGL a programmer can generate a 2D line segment in world-space with code like the following:

glBegin(GL_LINE_STRIP);
glVertex2f(1.5, 3.0);
glVertex2f(4.0, 4.0);

glEnd();

Polygons are single closed loops of line segments, usually drawn with their interiors filled.
In a language such as OpenGL polygons are very restricted to improve speed:

• edges can not intersect (simple polygons)
• polygon must be convex, not concave

To generate the outline of a triangular 2D polygon in world-space using OpenGL a programmer can write code like the following:

glBegin(GL_LINE_LOOP);
glVertex2f(1.5, 3.0);
glVertex2f(4.0, 4.0);
glVertex2f(4.0, 1.0);

glEnd();

To generate a filled triangular 2D polygon in world-space using OpenGL a programmer can write code like the following:

glBegin(GL_POLYGON);
glVertex2f(1.5, 3.0);
glVertex2f(4.0, 4.0);
glVertex2f(4.0, 1.0);

glEnd();

We will not limit ourselves to these 'easier' polygons.

How are line segments and polygons in world-space converted into illuminated pixels on the screen?

First these coordinates in world-space must be converted to coordinates in the viewport (ie pixel coordinates in the frame buffer.) This may involve the conversion from a 2D world to a 2D frame buffer (which we will study in a couple weeks), or the reduction from a 3D world to a 2D frame buffer (which we will study a couple weeks later.)

Then these coordinates in the viewport must be used to draw lines and polygons made up of individual pixels (rasterization.) This is the topic we will discuss now.

Most of the algorithms in Computer Graphics will follow the same pattern below. There is the simple (braindead) algorithm that works, but is too slow. Then that algorithm is repeatedly refined, making it more complicated to understand, but much faster for the computer to implement.

given a line segment from leftmost (Xo,Yo) to rightmost (X1,Y1):

Y=mX+B
m = deltaY / deltaX = (Y1 - Yo) / ( X1 - Xo)

Assuming |m| <= 1 we start at the leftmost edge of the line, and move right one pixel-column at a time illuminating the appropriate pixel in that column.

start = round(Xo)
stop = round(X1)
for (Xi = start; Xi <= stop; Xi++)

illuminate Xi, round(m * Xi + B);

Why is this bad? Each iteration has:

• comparison
• fractional multiplication
• call to round()

Addition is OK, fractional multiplication is bad, and a function call is very bad as this is done A LOT. So we need more complex algorithms which use simpler operations to decrease the speed.

Why is the slope (m) important?

if m=1 then each row and each column have a pixel filled in
if 0 <= m< 1 then each column has a pixel and each row has >= 1, so we increment X each iteration and compute Y.
if m > 1 then each row has a pixel and each column has >= 1, so we increment Y each iteration and compute X.

### Simple Incremental Algorithm

given a line segment from leftmost (Xo,Yo) to rightmost (X1,Y1):

Y=mX+B
m = deltaY / deltaX = (Y1 - Yo) / ( X1 - Xo)
if deltaX is 1 -> deltaY is m
so if Xi+1 = Xi + 1 -> Yi+1 = Yi + m

Assuming |m| <= 1 we start at the leftmost edge of the line, and move right one pixel-column at a time illuminating the pixel either in the current row or an adjacent row.

starting at the leftmost edge of the line:

X = round(Xo)
Y = Yo

while (X <= X1) repeatedly

illuminate X, round(Y)
add 1 to X (moving one pixel column to the right)

This guarantees there is one pixel illuminated in each column for the line

If |m| > 1 then we must reverse the roles of X and Y, incrementing Y by 1 and incrementing X by 1/m in each iteration.

Horizontal and vertical lines are subsets of the 2 cases given above.

need such a common, primitive function to be VERY fast.

features:

• + incremental
• - rounding is a time consuming operation(Y)
• - real variables have limited precision, can cause a cumulative error in long line segments (e.g. 1/3)
• - Y must be a floating point variables

### Midpoint Line Algorithm

given a line segment from leftmost (Xo,Yo) to rightmost (X1,Y1):

Y=mX+b
m = deltaY / deltaX = (Y1 - Yo) / ( X1 - Xo)
assuming Xo,X1,Yo,Y1 are integers

Assuming 0 <= m <= 1 we start at the leftmost edge of the line, and move right one pixel-column at a time illuminating the pixel either in the current row (the pixel to the EAST) or the next higher row (the pixel to the NORTHEAST.)

Y=mX+B
m = deltaY / deltaX = (Y1 - Yo) / ( X1 - Xo)

can rewrite the equation in the form: F(X,Y) = ax + by + c = 0

Y = (deltaY / deltaX) * X + B
0 = (deltaY / deltaX) * X - Y + B
0 = deltaY * X - deltaX * Y + deltaX * B

F(X,Y) = deltaY * X - deltaX * Y + deltaX * B

so for any point (Xi,Yi) we can plug Xi,Yi into the above equation and

F(Xi,Yi) = 0 -> (Xi,Yi) is on the line
F(Xi,Yi) > 0 -> (Xi,Yi) is below the line
F(Xi,Yi) < 0 -> (Xi,Yi) is above the line

Given that we have illuminated the pixel at (Xp,Yp) we will next either illuminate

the pixel to the EAST (Xp+ 1,Yp)
or the pixel to the NORTHEAST (Xp+ 1,Yp+ 1)

To decide we look at the Midpoint between the EAST and NORTHEAST pixel and see which side of the midpoint the line falls on.

line above the midpoint -> illuminate the NORTHEAST pixel
line below the midpoint -> illuminate the EAST pixel
line exactly on the midpoint -> CHOOSE TO illuminate the EAST pixel

We create a decision variable called d
We plug the Midpoint into the above F() for the line and see where the midpoint falls in relation to the line.
d = F(Xp+1,Yp+0.5)

d > 0 -> pick NORTHEAST pixel
d < 0 -> pick EAST pixel
d = 0 -> ***CHOOSE*** to pick EAST pixel

That tells us which pixel to illuminate next. Now we need to compute what Midpoint is for the next iteration.

if we pick the EAST pixel

Midpoint is incremented by 1 in X and 0 in Y

We want to compute the new d without recomputing d from the new Midpoint
We want to compute the new d only using current d

dcurrent= F(Xp + 1,Yp + 0.5)
using the function F(X,Y) = deltaY * X - deltaX * Y + deltaX * B we can expand this out ...
dcurrent= deltaY * (Xp + 1) - deltaX * (Yp + 0.5) + deltaX * B

dnew = F(Xp + 2, Yp + 0.5)
dnew = deltaY * (Xp + 2) - deltaX * (Yp + 0.5) + deltaX * B

when you simplify it you end up with: dnew = dcurrent + deltaY
so we create a new variable called deltaE where deltaE = deltaY

if we pick the NORTHEAST pixel

Midpoint is incremented by 1 in X and 1 in Y

We want to compute the new d without recomputing d from the new Midpoint, only using current d
We want to compute the new d only using current d

dcurrent= F(Xp + 1,Yp + 0.5)
using the function F(X,Y) = deltaY * X - deltaX * Y + deltaX * B we can expand this out ...
dcurrent= deltaY * (Xp + 1) - deltaX * (Yp + 0.5) + deltaX * B

dnew = F(Xp + 2, Yp + 1.5)
dnew = deltaY * (Xp + 2) - deltaX * (Yp + 1.5) + deltaX * B

when you simplify it you end up with: dnew = dcurrent + deltaY - deltaX
so we create a new variable called deltaNE where deltaNE = deltaY - deltaX

initial point (Xo,Yo) is known
so initial M is at (Xo + 1, Yo + 0.5)

so initial d = F(Xo + 1,Yo + 0.5)
using the function F(X,Y) = deltaY * X - deltaX * Y + deltaX * B we can expand this out ...

= deltaY * (Xo + 1) - deltaX * (Yo + 0.5) + deltaX * B
= (deltaY * Xo - deltaX * Yo + deltaX * B) + deltaY - 0.5 * deltaX
= F(Xo,Yo) + deltaY - 0.5 * deltaX

since (Xo,Yo) is on the line -> F(Xo,Yo) = 0
so initial d = deltaY - deltaX / 2

the division by 2 is still annoying, but we can remove it by being clever
we can avoid the division by 2 by multiplying F() by 2
this also multiplies d, deltaE, deltaNE by 2
but since d is only concerned with =0,< 0, or > 0 multiplication does not affect it

So now we can finally show the Midpoint Line algorithm

Assuming integral endpoints for the line segment (if not then make them integral)
starting at the leftmost edge of the line:

deltaX = X1 - Xo
deltaY = Y1 - Yo
d = deltaY * 2 - deltaX
deltaE = deltaY * 2
deltaNE = (deltaY - deltaX) * 2
X = Xo
Y = Yo
illuminate X, Y

while (X < X1) repeatedly

if ( d <= 0)

else

illuminate X, Y

The full algorithm is given (in C) in the red book as program 3.2 on p.75.
The full algorithm is given (in Pascal) in the white book as figure 3.8 on p. 78.

features:

• + incremental
• + only addition done in each iteration
• + all integer variables

but that's not all!
What if the line segment starts on the right and proceeds to the left?

with plain lines you could reverse the endpoints and draw the line from left to right as described above. If the line has a pattern this simplification will not work.

What about lines with slope < 0 or slope > 1? some modifications needed

### Filling a Polygon

want to avoid drawing pixels twice (not a problem with frame buffers but can be with other display technologies.)

pixels within the boundary of a polygon belong to the polygon
pixels on the left and bottom edges belong to a polygon, but not the pixels on the top and right edges

Want a polygon filling routine that handels convex, concave, intersecting polygons and poygons with interior holes.

overall algorithm:

moving from bottom to top up the polygon
starting at a left edge, fill pixels in spans until you come to a right edge

specific algorithm:

moving from bottom to top up the polygon
1. find intersections of the current scan line with all edges of polygon
2. sort intersections by increasing x coordinate
3. moving through list of increasing x intersections
parity bit = even (0)
each intersection inverts the parity bit
draw pixels when parity is odd (1)

Why do it horizontally, why not vertically?

Algorithm for step 1: scan-line algorithm

as usual there is the straightforward easy way and the convoluted efficient way.

an easy way:

• given that each line segment can be described using x = y/m + B
• each scan line covering the polygon has a unique integer Y value from ymin to ymax
• plugging each of these Y values into the equation gives the corresponding fractional X value
• these values could then be sorted and stored

### Coming Next Time

More Polygon Filling, Clipping

last revision 8/30/03