Journal: Dr. Dobb's Journal Feb 1991 v16 n2 p153(7) ----------------------------------------------------------------------------- Title: The polygon primeval. (filling polygons) (column) Author: Abrash, Michael. AttFile: Program: GP-FEB91.ASC Source code listing. Summary: The first in a series of columns develops routines to draw filled polygons. Filled polygons, which are essentially any closed form filled with pixels in a consistent color or pattern, are useful for a variety of graphics constructs. Polygons are divisible into convex, nonconvex and complex shapes. Filling a polygon is fundamentally a rasterization process involving drawing all of the horizontal lines within the polygon's boundaries. Problems that must be resolved include defining which pixels are within each polygon and ensuring that multiple polygons can fit together without overlapping. A line-tracing algorithm is modified so that it contains only pixels that are truly within the polygon. Rules and routines are developed to fit polygons together without overlapping and fill them. ----------------------------------------------------------------------------- Descriptors.. Topic: Graphic Forms Computer Graphics Mathematics of Computing Methods Painting Algorithms Programming Instruction Geometry. Feature: illustration chart program. Caption: A standard line-drawing algorithm can select polygon-boundary pixels. (chart) Overlaying of filled polygons. (chart) Routine to color-fill a convex polygon. (program) ----------------------------------------------------------------------------- Full Text: "Give me but one firm spot on which to stand, and I will move the Earth. " -Archimedes Were Archimedes alive today, he might say, "Give me but one fast polygon-fill routine on which to call, and I will draw the Earth." Programmers often think of pixel drawing as being the basic graphics primitive, but filled polygons are equally fundamental and far more useful. Filled polygons can be used for constructs as diverse as a single pixel or a 3-D surface, and virtually everything in between. I'll spend some time in this column developing routines to draw filled polygons and building more sophisticated graphics operations atop those routines, Sooner, rather than later, I'll get to 2-D manipulation and animation of polygonbased entities (with occasional diversions to Other graphics topics of interest), leading uP to an exploration of 3-D graphics. You can't get there from here without laying some groundwork, though, so this month I'll begin with the basics of filling a polygon. Next month, we'll see how to draw a polygon considerably faster. That will set the tone for this column: High-level exploration of a graphics topic first, followed by a speedy hardware-specific implementation for the IBM PCNGA combination, the most widely used graphics system around. Abstract, machine-independent graphics is a thing of beauty, but only by understanding graphics at all levels, including the hardware, can YOu boost performance into the realm of the sublime. And slow computer graphics is scarcely worth the bother. Filled Polygons A polygon is simply a shape formed by lines laid end to end to form a continuous, closed path. A polygon is filled by setting afl pixels within the polygon's boundaries to a color or pattern. For now, we'll work only with polygons filled with solid colors. You can divide polygons into three categories: convex, nonconvex, and complex, as shown in Convex polygons include what you'd normally thingk of as " convex" and mole; as far as we're concerned, a convex polygon is on, for which any horizontal line drawn through the polygon encounters the right edge exactly once and the left edge exactly once, excluding horizontal and zero-length edge segments. Put another way, neither the right nor left edge of a convex polygon ever reverses direction from uP to down, or vice-versa. Also, the right and left edges of a convex polygon may not cross each other, although they may touch so long as the right edge never crosses over to the left side of the left edge. (Check out the second polygon drawn in Listing Three, page 166, which certainly isn't convex in the normal sense.) The boundaries of nonconvex polygons, on the other hand, can go in whatever directions they please, so long as they never cross. Complex polygons can have any boundaries you might imagine, which makes for interesting problems in deciding which interior spaces to fill and which not. Each category is a superset of the previous one. Why bother to distinguish between convex, nonconvex, and complex polygons? For peformance, especially when it comes to filling convex polygons. It's with filled convex polygons that we're going to start; they're widely useful and will serve well to introduce some of the subtler complexities of polygon drawing, not the least of which is the slippery concept of "inside." Which Side is inside? The basic principle of polygon filling is decomposing each polygon into a series of horizontal fines, one for each horizontal row of pixels, or scan line, within the polygon (a process I'll call scan conversion), and drawing the horizontal lines. I'll refer to the entire process as rasterization. Rasterization of convex polygons is easily done by starting at the top of the polygon and tracing down the left and right sides, one scan line (one vertical pixel) at a time, filling the extent between the two edges on each scan line, until the bottom of the polygon is reached. At first glance, rasterization does not seem to be particularly complicated, although it should be apparent that this simple approach is inadequate for nonconvex polygons. There are a couple of complications, however. The lesser complication is how to rasterize the polygon efficiently, given that it's difficult to write fast code that simultaneously traces two edges and flus the space between them. The solution is decoupling the process of scan-converting the polygon into a list of horizontal lines from that of drawing the horizontal lines. One device-independent routine can trace along the two edges and build a list of the beginning and end coordinates of the polygon on each raster line. Then a second, device-specific routine can draw from the list after the entire polygon has been scanned. We'll see this in action shortly. The second, greater complication arises because the definition of which pixels are "within" a polygon is a more complicated matter than you might imagine. You might think that scan-converting an edge of a polygon is analogous to drawing a line from one vertex to the next, but this is not so. A line by itself is a one-dimensional construct, and as such is approximated on a display by drawing the pixels nearest to the line on either side of the true line. A line serving as a polygon boundary, on the other hand, is part of a two-dimensional object. When filling a polygon, we want to draw the pixels within the polygon, but a standard vertex-to-vertex linedrawing algorithm will draw many pixels outside the polygon, as shown in Figure 2. It's no crime to use standard lines to trace out a polygon, rather than drawing only interior pixels. In fact, there are certain advantages: For example, the edges of a filled polygon will match the edges of the same polygon drawn unfilled. Such polygons will look pretty much as they're supposed to, and all drawing on raster displays is, after all, only an approximation of an ideal. There's one great drawback to tracing polygons with standard lines, however: Adjacent polygons won't fit together properly, as shown in Figure 3. If you use six equilateral triangles to make a hexagon, for example, the edges of the triangles will overlap when traced with standard lines, and more recently drawn triangles will wipe out portions of their predecessors. Worse still, odd color effects will show up along the polygon boundaries if exclusive-or drawing is used. Consequently, filling out to the boundary lines just won't do for drawing images composed of fitted together polygons. And because fitting polygons together is exactly what I have in mind, we need a different approach. How Do You Fit Polygons Together? How, then, do you fit polygons together? Very carefully. First, the line-tracing algorithm must be adjusted so that it selects only those pixels that are truly inside the polygon. This basically requires shifting a standard line-drawing algorithm horizontally by one half-pixel toward the polygon's interior. That leaves the issue of how to handle points that are exactly on the boundary, and points that lie at vertices, so that those points are drawn once and only once. To deal with that, we're going to adopt the following rules: Points located exactly on nonhorizontal edges are drawn only if the interior of the polygon is directly to the right (left edges are drawn, right edges aren't). 9 Points located exactly on horizontal edges are drawn only if the interior of the polygon is directly below them (horizontal top edges are drawn, horizontal bottom edges aren't). A vertex is drawn only if afl lines ending at that point meet the above conditions (no right or bottom edges end at that point). All edges of a polygon except those that are flat tops or flat bottoms will be considered either right edges or left edges, regardless of slope. The left edge is the one that starts with the leftmost line down from the top of the polygon. These rules ensure that no pixel is drawn more than once when adjacent polygons are fined, and that if polygons cover the full 360-degree range around a pixel, then that pixel will be drawn once and only once - just what we need in order to be able to fit filled polygons together seamlessly. This sort of non-overlapping polygon filling isn't ideal for all purposes. Polygons are skewed toward the top and left edges, which not only introduces drawing error relative to the ideal polygon but also means that a filled polygon won't match the same polygon drawn unfilled. Narrow wedges and one-pixel-wide polygons will show up spottily. All in all, the choice of polygon-filling approach depends entirely on the ways in which the filled polygons will be used. For our purposes, nonoverlapping polygons are the way to go, so let's have at them. Non-overlapping Convex Polygons Made Easy Without further ado, Listing One (page 164) contains a function, FillConvexPolygon, that accepts a list of points that describe a convex polygon, with the last point assumed to connect to the first, and scans it into a list of lines to flU, then passes that list to the function DrawHorizontalLineList Listing Two (page 165). Listing Three (page 166) is a sample program that calls FillConvexPolygon to draw polygons of various sorts, and Listing Four (page 166) is a header file included by the other listings. Listing Two isn't particularly interesting; it merely draws each horizontal line in the passed-in list in the simplest possible way, one pixel at a time. NO, that doesn't make the pixel the fundamental primitive; next month I'll replace Listing Two with a much faster version that doesn't bother with individual pixels at all.) Listing One is where the action is this month. Our goal is to scan out the left and right edges of each polygon so that all points inside and no points outside the polygon are drawn, and so that all points located exactly on the boundary are drawn only if they are not on right or bottom edges. That's precisely what Listing One does; here's how. Listing one first finds the top and bottom of the polygon, then works out from the top point to find the two ends of the top edge. If the ends are at different locations, the top is flat, which has two implications. Firstly, it's easy to find the starting vertices and directions through the vertex list for the left and right edges. (To scan-convert them properly, we must first determine which edge is which.) Secondly, the top scan line of the polygon should be drawn without the rightmost pixel, because only the rightmost pixel of the horizontal edge that makes up the top scan line is part of a right edge. if, on the other hand, the ends of the top edge are at the same location, then the top is pointed. In that case, the top scan line of the polygon isn't drawn; it's part of the right-edge line that starts at the top vertex. it's part of a left-edge line, too, but the right edge overrides.) When the top isn't flat, it's more difficult to tell in which direction through the vertex list the right and left edges go, because both edges start at the top vertex. The solution is to compare the slopes from the top vertex to ends of the two lines coming out of it in order to see which is leftmost. The calculations in Listing One involving the various deltas do this, using a slightly rearranged form of the equation: DeltaYN/DeltaXN > DeltaYP/DeltaXP Once we know where the left edge starts in the vertex list, we can scan-convert it a line at a time until the bottom vertex is reached. Each point is stored as the starting X coordinate for the corresponding scan line in the list we'll pass to DrawHorizontalLineList. The nearest X coordinate on each scan line that's on or to the right of the left edge is selected. The last point of each line making up the left edge isn't scan-converted, producing two desirable effects. First, it avoids drawing each vertex twice; two lines come into every vertex, but we want to scan-convert each vertex only once. Second, not scan-converting the last point of each line causes the bottom scan line of the polygon not to be drawn, as required by our rules. The first scan line of the polygon is also skipped if the top isn't flat. Now we need to scan-convert the right edge into the ending X coordinate fields of the line list. This is performed in the same manner as for the left edge, except that every line in the right edge is moved one pixel to the left before being scan-converted. Why? We want the nearest point to the left of but not on the right edge, so that the right edge itself isn't drawn. As it happens, drawing the nearest point on or to the right of a line moved one pixel to the left is exactly the same as drawing the nearest point to the left of but not on that line in its original location. Sketch it out and you'll see what I mean. Once the two edges are scan-converted, the whole line list is passed to DrawHorizontalLineList, and the polygon is drawn. Finis. Oddball Cases Listing One handles zero-length segments (multiple vertices at the same location) by ignoring them, which will be useful down the road because scaled down polygons can end up with nearby vertices moved to the same location. Horizontal line segments are fine anywhere in a polygon, too. Basically, Listing One scan-converts between active edges (the edges that define the extent of the polygon on each scan line) and both horizontal and zero-length lines are non-active; neither advances to another scan line, so they don't affect the edges being scanned. Book of the Month The book of the month is the second edition of Foley and van Dam's classic Fundamentals of Interactive Computer Graphics, the inspiration and primary reference for much of the nonmachinespecific material I'll present in this column. The almost entirely rewritten new version, retitled Computer Graphics: Principles and Practice (AddisonWesley, 1990, $64.50), nearly doubles the size of the first tome, to a total of 1174 pages. You'll wish it were longer, too, because computer graphics has become such a broad field that even this massive book often merely touches on an area, providing the fundamental concepts, equations, and algorithms, and moves on. Still, just about everything you could want to know is in there somewhere. Truly a book to lose yourself in, and highly recommended. Coming Up Next This month's code merely demonstrates the principles of filling convex polygons, and is by no means fast. Next month, we'll spice things up by eliminating the floating point calculations and pixel-at-a-time drawing and tossing a little assembly language into the mix.