CS184 Lecture 36 summary

Line drawing and polygon filling

Virtually all display devices these days are raster devices. When a polygon-oriented system like VRML tries to display a line or a polygon, it has to go from the geometry (e.g. point coordinates) to turning pixels on and off. This lecture, we look at that process.

We assume that perspective or orthographic projection has already been done, and so the task is to display 2D lines and polygons. This process is often called scan-conversion.

DDA

The simplest scan conversion procedure for lines is DDA (Digital Differential Analyser). Suppose we want to draw the line segment between points (x1, y1) and (x2, y2). The equation of the infinite line through the points (assuming its not vertical) can be written as

y = m x + b

where m = (y2 - y1) / (x2 - x1) and b = y1 - m x1.

Lets assume without loss of generality that x2 > x1 and that m < 1. If m is not < 1, we can reverse the roles of x and y (we'll explain that in a moment). Each pixel on the screen represents a certain length in x and y, let one pixel distance in x be dx, and for y its dy. Usually dx and dy are the same, but not always. We will assume that they are for this lecture, and they are both 1.

Let k be x1 rounded to the nearest integer. The x coordinate of the first pixel we draw is k. Its y coordinate is (m k) rounded to the nearest integer. Then we increment k, and draw the next point (x, y) = (k, round(m k)). To draw a point, we would use the integer point coordinates as indices into an array holding the screen contents, and set that element to the desired color.

We continue until the x coordinate reaches the rounded value of x2. Rather than computing mk at each step with a multiply, we can simply add m to its last value, since k is incrementing each time. Although if have many processors, we may want to compute each point in parallel, and then the multiplication scheme is best.

This scheme gives a scan-converted line that "covers" the x-axis. i.e. there is a pixel in every vertical column of the display between the endpoints. And there are no "gaps" vertically either. The largest step in y is m < 1 pixel, so every row of the display between the endpoints also has a pixel set in it.

As we said before, if m > 1, we just reverse the roles of x and y. We would take one-pixel steps in y instead, and compute the rounded value of x after each increment of 1/m in x.

Bresenham's Line Drawing Algorithm

A slight variation on DDA is Bresenham's algorithm. DDA has the disadvantage that you have to add fractional numbers (m or 1/m) at each step. That's not hard for a CPU but can be a lot of work for VLSI when you're trying to render 100s of thousands of polygons a second. Basically, assuming the points  (x1, y1) and (x2, y2) have integer coordinates, m = (y2 - y1) / (x2 - x1) will be a rational number. In effect, Bresenham does the same calculations as DDA using only integer arithmetic by keeping track of the numerator only of the product mk (the denominator is always the same).

The derivation is tedious (see Hearn and Baker) but the process is simple. Bresenham uses a "decision parameter" pk at each step. pk is really the fractional part of (mk - 0.5) with its denominator cleared. Here is some pseudo-code. We assume x2 > x1 and m < 1 as before:

  1. Plot  (x1, y1). Determine Dx =  (x2 - x1)   and Dy =  (y2 - y1).
  2. Initialize the decision parameter
    p0 = 2
    Dy - Dx
  3. If xk is the x-coordinate of the last point plotted, and pk is the last decision value, then:

    If pk < 0  the new point is (xk + 1, yk) and pk+1 = pk + 2 Dy

    Otherwise the new point is (xk + 1, yk + 1) and pk+1 = pk + 2 Dy - 2 Dx

  4. Repeat the last step Dx times.

Drawing Circles and Quadrics

Circles are a bit trickier but here is one simple approach. Suppose the circle is x2 + y2 = r2. First pick only the quadrant of the circle where |y| > |x|, which ensures that the slope of the circle |m| < 1. Start with y = r, x = 0 and plot it. Then increment x and compute  x2 + y2 for the old value of y and for y-1. Use whichever value of y gives a value closer to   r2. There is also a decision-function based method described in the book. Its derivation is pretty long and it works only for circles. Rather than describing generation procedures for all kinds of quadric curves, we give here a simple scheme which works for all of them. Its based on subdivision.

Recall that a unit circle has a parametric equation x = (1 - u2) / (1 + u2), y = 2u / (1 + u2). That makes it a rational quadratic B-spline. We know that you can compute rational B-splines using subdivision in one dimension higher. So lets figure out what the control points and weights are to generate the circle.

First of all, the blending functions for degree-2 B-splines in the u interval (0,1) are

B1(u) = 2/3 (1 - 2u + u2),
B2(u) = 2/3 (1 + 2u - 2u2),
B3(u) = 2/3 u2

We want to find 3 control points p1 through  p3 which generate the right curve. Those control points have 3 coordinates (x, y, z), and the third coordinate gives us the denominators. To get a denominator that is a multiple of (1 + u2) we can use 1 B1(u) + 1 B2(u) + 3 B3(u). So the z coordinates of the points are respectively 1, 1 and 3.

You can solve in a similar way for the x coordinates to get (1 - u2), and the weights are 1, 1, -1. Finally the y weights are -1, 1, 3. So the 3 control points in homogeneous coordinates are:

[ x ]      [  1 ]          [  1 ]           [ -1 ]
[ y ]  =  [ -1 ]         [  1 ]           [  3 ]
[ z ]      [  1 ]          [  1 ]           [  3 ]

And you can check that at u = 0, the point is (1, 0), which at u = 1, the point is (0,1). So in the range of these 3 control points, the curve is the first quadrant of the circle. The control points projected into 2D points are (1, -1), (1, 1) and (-1/3, 1). The first two points have weight 1 and the last has weight 3.

To draw the curve, you run the subdivision algorithm. That is, you add midpoints, then apply the averaging filter, which in this case is just another midpoint (Chaikin's algorithm). At each level of subdivision, the distance between consecutive control points decreases by two. When the points are closer than a pixel, you can compute their midpoints (which are on the final spline) and round them to the nearest pixel to draw.

To implement this scheme, up until the final projection (which requires a division), you need only do two additions and divides-by-two per point, per level of subdivision. The divides are just shifts (or nothing at all, just keep track of where the binary decimal point is). The final divisions are often supported in hardware because they correspond to perspective projection of 3D points. Put another way, you construct a 3D curve, and then perspective project it into the viewing plane to get a circle.

To draw a different quadric curve, you simply change the 3 control points.

Polygon Filling

We already discussed scan-line polygon filling in the context of Gouraud and Phong shading. Basically, you draw pixels along horizontal scan lines that intersect the polygon. For a line y = m x + b, you can determine which side of the line you are on (and therefore if you're inside or outside the polygon) by computing the sign of y - m x - b. If its positive, then the y coordinate of the point is above the line.

Rather than testing point by point, you can compute the minimum and maximum x values on the scan line. You start filling points at xmin and continue until xmax. Coherence can be used to update xmin and xmax for the next line because they will be offset by constants from their previous values.

Care must be taken if y = mx + b at a pixel. In other words, if a pixel is exactly on the polygon boundary. This is tricky because the point might be drawn twice (a second time in another polygon that shares the same edge). Its very desirable that polygons partition the plane. That is, if the polygons have zero-area of intersection, then no pixels should be drawn in both of them. Various "tie-breaking" schemes are used in these cases. One simple one is to "pretend" that the polygons are drawn with vertices pushed slightly off the grid. Instead of integer vertices (x, y), the vertices are (x + e, y + e2) where e is an infinitesimal value. The lines between those vertices can never pass through an integer point, so ties never happen.

Testing inside-outside

Deciding when you're inside or outside a polygon can be tricky if its not convex. One method is winding number. It assumes that the polygon has an directed boundary. That is, all the edges have a direction and material is assumed to be on one side (normally the left). You draw a ray from the point p to be tested in a vertical direction. Each time it crosses an edge that's pointing left, you add one to the winding number. Each time it crosses an edge that's pointing to the right, you subtract one from the winding number. If the number is positive, you're inside the polygon. If its zero, you're outside.

Flood Filling

To fill a more general shape (e.g. a circle or quadric, or a pen-drawn boundary), you can use flood filling. Flood filling starts from a point inside the boundary and recursively colors the neighbors of that point that are not on the boundary. This must be done carefully to make sure that filling doesnt cross the boundary. When we speak of neighbors, there are two common meanings. The 4-connected neighborhood of a point consists of the 4 neighbors to the N, S, W and E. The 8-connected neighborhood consists of these points plus the 4 diagonal neighbors. Generally, 4-connected neighborhoods are what you want for filling because the diagonal neighbors will cross most kinds of curves (including all the schemes we described above).