public void circleSimple(int xCenter, int yCenter, int radius, Color c) { int pix = c.getRGB(); int x, y, r2; r2 = radius * radius; for (x = -radius; x <= radius; x++) { y = (int) (Math.sqrt(r2 - x*x) + 0.5); raster.setPixel(pix, xCenter + x, yCenter + y); raster.setPixel(pix, xCenter + x, yCenter - y); } }And it would result in circles that look like:
The above applet demonstrates the circleSimple() algorithm. Click and drag the left button to specify the circle's center and a point on its radius. Selecting the right button will clear the drawing. The circle approximation generated by the algorithm is overlaid with an ideal circle for comparison.
As you can see the circles look fine in areas where only one pixel is required for each column, but in areas of the circle where the local slope is greater the one the circle appears discontinuous (where have we seen this before?).
We could take the approach of computing the derivative (i.e. the local slope) of the function at each point and then make a decision whether to step in the x direction or the y direction. But, we will explore a different tact here.
A circle exhibits a great deal of symmetry. We've already exploited this somewhat by plotting two pixels for each function evaluation; one for each possible sign of the square-root function. This symmetry was about the x-axis. The reason that a square-root function brings out this symmetry results from our predilection that the x-axis should be used as an independent variable in function evaluations while the y-axis is dependent. Thus, since a function can yield only one value for member of the domain, we are forced to make a choice between positive and negative square-roots. The net result is that our simple circle-drawing algorithm exploits 2-way symmetry about the x-axis.
Obviously, a circle has a great deal more symmetry. Just as every point above an x-axis drawn through a circle's center has a symmetric point an equal distance from, but on the other side of the x-axis, each point also has a symmetric point on the opposite side of a y-axis drawn through the circle's center.
public void circleSym4(int xCenter, int yCenter, int radius, Color c) { int pix = c.getRGB(); int x, y, r2; r2 = radius * radius; raster.setPixel(pix, xCenter, yCenter + radius); raster.setPixel(pix, xCenter, yCenter - radius); for (x = 1; x <= radius; x++) { y = (int) (Math.sqrt(r2 - x*x) + 0.5); raster.setPixel(pix, xCenter + x, yCenter + y); raster.setPixel(pix, xCenter + x, yCenter - y); raster.setPixel(pix, xCenter - x, yCenter + y); raster.setPixel(pix, xCenter - x, yCenter - y); } }This circle-drawing algorithm uses 4-way symmetry.
The above applet demonstrates the circleSym4() algorithm. Click and drag the left button to specify the circle's center and a point on its radius. Selecting the right button will clear the drawing. The circle approximation generated by the algorithm is overlaid with an ideal circle for comparison.
This algorithm has all the problems of our previous algorithm, but, it gives the same result with half as many function evaluations. So much for "making it work first" before optimizing. But, we're on a roll so let's push this symmetry thing as far as it will take us.
Notice also that a circle exhibits symmetry about the pair of lines with slopes of one and minus one, as shown below.
The following routine takes advantage of this 8-way symmetry.
public void circleSym8(int xCenter, int yCenter, int radius, Color c) { int pix = c.getRGB(); int x, y, r2; r2 = radius * radius; raster.setPixel(pix, xCenter, yCenter + radius); raster.setPixel(pix, xCenter, yCenter - radius); raster.setPixel(pix, xCenter + radius, yCenter); raster.setPixel(pix, xCenter - radius, yCenter); y = radius; x = 1; y = (int) (Math.sqrt(r2 - 1) + 0.5); while (x < y) { raster.setPixel(pix, xCenter + x, yCenter + y); raster.setPixel(pix, xCenter + x, yCenter - y); raster.setPixel(pix, xCenter - x, yCenter + y); raster.setPixel(pix, xCenter - x, yCenter - y); raster.setPixel(pix, xCenter + y, yCenter + x); raster.setPixel(pix, xCenter + y, yCenter - x); raster.setPixel(pix, xCenter - y, yCenter + x); raster.setPixel(pix, xCenter - y, yCenter - x); x += 1; y = (int) (Math.sqrt(r2 - x*x) + 0.5); } if (x == y) { raster.setPixel(pix, xCenter + x, yCenter + y); raster.setPixel(pix, xCenter + x, yCenter - y); raster.setPixel(pix, xCenter - x, yCenter + y); raster.setPixel(pix, xCenter - x, yCenter - y); } }So now we get 8 points for every function evaluation, and this routine should be approximately 4-times faster than our initial circle-drawing algorithm. What's going on with the four pixels that are set outside the loop (both at the top and bottom)? Didn't I say that every point determines 7 others?
The above applet demonstrates the circleSym4() algorithm. Click and drag the left button to specify the circle's center and a point on its radius. Selecting the right button will clear the drawing. The circle approximation generated by the algorithm is overlaid with an ideal circle for comparison.
It seems suddenly that our circle's appear continuous, and we added no special code to test for the slope. Symmetry has come to our rescue (actually, symmetry is also what saved us on lines... think about it).
So our next objective is to simplify the function evaluation that takes place on each iteration of our circle-drawing algorithm. All those multiplies and square-root evaluations are expensive. We can do better.
One approach is to manipulate of circle equation slightly. First, we translate our coordinate system so that the circle's center is at the origin (the book leaves out this step), giving:
What we'd like to do is to use this discriminating function to maintain our trajectory of drawn pixels as close as possible to the desired circle. Luckily, we can start with a point on the circle, (x0, y0+r) (or (0, r) in our adjusted coordinate system). As we move along in steps of x we note that the slope is less than zero and greater than negative one at points in the direction we're heading that are near our known point on a circle. Thus we need only to figure out at each step whether to step down in y or maintain y at each step.
Consider what the next value of the discriminating function is in the case that there is no change in y.
Thus when we are inside the circle with a negative discriminant we can incrementally update the discriminant's value by incrementing by 2x + 1.
Suppose instead that we find ourselves outside of the circle, in this case the next value of the descriminant should be:
All that remains is to compute the initial value for our discriminating function. Our initial point, (0,r), was on the circle. The very next point, (1, r), will be outside if we continue without changing y (why?). However, we'd like to adjust our equation so that we don't make a change in y unless we are more than half way to it's next value. This can be accomplished by initialing the discriminating function to the value at y - 1/2, a point slightly inside of the circle. This is similar to the initial offset that we added to the DDA line to avoid rounding at every step.
private final void circlePoints(int cx, int cy, int x, int y, int pix) { int act = Color.red.getRGB(); if (x == 0) { raster.setPixel(act, cx, cy + y); raster.setPixel(pix, cx, cy - y); raster.setPixel(pix, cx + y, cy); raster.setPixel(pix, cx - y, cy); } else if (x == y) { raster.setPixel(act, cx + x, cy + y); raster.setPixel(pix, cx - x, cy + y); raster.setPixel(pix, cx + x, cy - y); raster.setPixel(pix, cx - x, cy - y); } else if (x < y) { raster.setPixel(act, cx + x, cy + y); raster.setPixel(pix, cx - x, cy + y); raster.setPixel(pix, cx + x, cy - y); raster.setPixel(pix, cx - x, cy - y); raster.setPixel(pix, cx + y, cy + x); raster.setPixel(pix, cx - y, cy + x); raster.setPixel(pix, cx + y, cy - x); raster.setPixel(pix, cx - y, cy - x); } } public void circleMidpoint(int xCenter, int yCenter, int radius, Color c) { int pix = c.getRGB(); int x = 0; int y = radius; int p = (5 - radius*4)/4; circlePoints(xCenter, yCenter, x, y, pix); while (x < y) { x++; if (p < 0) { p += 2*x+1; } else { y--; p += 2*(x-y)+1; } circlePoints(xCenter, yCenter, x, y, pix); } }