Intro Graphics Assignment 4
Lines, Circles, and Flood Fill
Due: October 21, 2003
Goals
In this project, you will build a set of routines for scan converting circles. There are lots of ways to scan convert a circle; you'll be implementing (only) three of them. The idea is to apply some of the principles you've learned in class, and get familiar with programming a simple graphics application. In addition, you'll write a flood-fill algorithm that lets you fill an entire region of the window with a solid color.
Useful stuff to get you started
We've provided a simple skeleton application that will take care of everything you need in terms of creating a window and handling the user interface. The user interface is kept to an absolute minimum to facilitate working on any platform you wish. All you have to do is fill in the functions in the files circle.c and flood.c. Look for the comment /* YOUR CODE HERE */ to figure out where to begin.
The skeleton code is available here.
A sample solution for Windows is available here.
Other than finding the functions you need to fill in, the only other thing you need to know is how to actually draw a point and read the color at a point! We have provided a function called DrawPixel(int x, int y, float r, float g, float b) that will draw a single pixel at coordinates (x,y) with color <r,g,b>. Note that the valid color range for each color component is [0,1] --- colors outside that range will simply be clamped.
It's not an error to call DrawPixel at a location outside the window border (even negative coordinates), but the pixel will be silently discarded.
The function to read the color at a point is ReadPixel(int x, int y, float *r, float *g, float *b). You pass pointers to the locations to store the red, green, and blue values at the pixel (x,y). So, for example, the code might look like:
void foo(int x, int y)
{
float r, g, b;
ReadPixel( x, y, &r, &g, &b );
printf ("The color at (%d, %d) is <%f,%f,%f>\n", x, y, r, g, b );
}
Assignment Specifications
Part I --- Bresenham's or Midpoint Algorithm
In class, we talked about the midpoint algorithm for drawing a circle directly using decision variables and finite differencing. Look for the function called MidpointCircle(int cx, int cy, int radius), and write your code there. The circle should be centered at (cx,cy), and have a radius of radius. All your circles should be drawn white, which is the color <1,1,1>.
Part II --- Polygon Approximation
You can also draw a circle by approximating it with a regular n-sided polygon. Obviously the smallest n you can use is 3, which would give you a triangle (not such a good approximation, unless your circle is really small). Look for the function NGonCircle(int n, int cx, int cy, int radius), and write your code there. n is the number of sides to use for the approximation, and the rest of the arguments are the same as for MidpointCircle. The circle should again be white.
But wait!
How do you draw a regular n-sided polygon? All we have is pixels! You will need to implement a line drawing algorithm as well, and call it repeatedly to draw the sides of the n-gon. Look for the function DrawLine(int x1, int y1, int x2, int y2), and write your code there. DrawLine draws a line from the point (x1, y1) to the point (x2, y2). You will need to handle all possible line directions, including horizontal and vertical lines to approximate the circle properly for any n.
Part III --- Fat and Smooth Circles
Here's one we didn't talk about in class, but it's pretty straightforward. Let's say you have some circle that's centered at (x,y), and has some thickness to it. To describe such a circle, we give it an inner and outer radius, say ri and ro. How are we going to draw a circle like that? We could try approximating it with a fat polygon, but when the number of sides goes up, the fat polygon looks pretty ugly. (besides, I didn't tell you how to draw thick lines).
A better solution is the following: We know that we're only going to touch pixels inside a square with center at (x,y) and with sides of length 2ro. Why not just look at every pixel in that square and ask ourselves whether or not it's inside the fat circle? All we have to do is compute the distance to the center, and see if it lies between ri and ro.
So what?
Why is this method of drawing circles any better than the others (other than letting us draw fat circles)? It sure seems slow (one square root per pixel?!), and it's tricky to draw a thin circle using this method because of precision problems.
First of all, you don't need to use the square root. We don't care what the actual distance from a point to the center is, just whether or not it lies between ri and ro. So why not compute the square of the distance, and ask if it lies between between ri2 and ro2. That should be a lot faster (try both ways if you don't believe me).
Second, we don't have to consider all the pixels in that square -- you certainly know that all the pixels in the inscribed square with center (x,y) and side length 2ri don't lie on the circle, so why test them? In fact, you can exploit the symmetry of the circle to look at even less pixels. Your solution should attempt to look at as few pixels as you think is reasonable. My solution looks at much less than one eighth of the pixels.
Third, and probably most importantly, this method allows us to smooth out the edges of the circle. What happens when the edge of the circle actually passes through a pixel in any of these algorithms? We see stair-step patterns, or "jaggies". These artifacts are a form of "aliasing", a result of the fact that pixels are discrete things, and a circle is a continuous thing. Why not chop up each pixel into, say, 16 equal parts, and see how many of those lie on the circle? That way, if none of them do, we still color the pixel black, and if all of them do, we still color the pixel white, but if, say, four of them do, we color the pixel 25% gray. This will have the visual effect of smoothing out the edges of the circle, and should give a much more pleasing effect. You can even try drawing a circle that's thinner than one pixel, and see what happens (you'll have to modify SampleCircle to accept floating point radii).
Okay, okay, I buy it.
Good. Look for the function SampleCircle( int outer, int inner, int samples, int cx, int cy ). outer and inner are the outer and inner radii, respectively, and samples is the of the number of pieces to chop each pixel into (so, if samples is 2, you should chop the pixel into 4 pieces, and if it's 3, you should copy the pixel into 9 pieces, etc.). You can divide the pixels regularly, or, if you're feeling really excited about this, try jittering the sub-pixel coordinates.
Congratulations, you've just anti-aliased something.
Part IV --- Fill 'er up!
The final part of this assignment is to write a flood-fill algorithm like the one we discussed in class. Look for the function Flood( int x, int y ), and write your code there. You should fill the region seeded at (x,y) with full red (<1,0,0>). Use the WindowWidth( ) and WindowHeight ( ) functions to get the width and height of the window so your flood fill searching routines don't leave the bounds of the window.
Using the interface
When you run the program, a window will appear on your screen. The controls for the program are almost all accessed by either pressing keys or clicking the mouse inside the window.
| Key | Meaning |
| r | Decrease radius |
| R | Increase radius |
| n | Decrease number of sides (n-gon only) |
| N | Increase number of sides (n-gon only) |
| i | Decrease inner radius (fat circles only) |
| I | Increase inner radius (fat circles only) |
| s | Decrease number of samples (fat circles only) |
| S | Increase number of samples (fat circles only) |
| 1 | Midpoint circle |
| 2 | N-gon circle |
| 3 | Fat circle |
You can also click anywhere in the window to select an initial seed for a run of the flood fill algorithm. If you're in doubt as to how something should look, you may always compare your program against the sample implementation.
Here's the art contest page.