首先,您的代码中有一些错误,因为sin()
and cos()
函数是弧度函数,而不是基于度数的函数。这将使您的圆出现在伪随机选择的点上,因为每一步都会绘制一个与前一步相距约 57 度的点。这不会产生任何影响,因为现代图形工作是缓冲的,您将看到最终结果,而不是工作原理。
话虽如此,今天没有人使用您公开的算法来画圆。看看Bresenham 中点算法 https://en.wikipedia.org/wiki/Midpoint_circle_algorithm,它基本上尝试按八分圆绘制一个圆,但速度要快几倍。
这些算法背后的想法是考虑R^2 = x^2 + y^2
公式并在一个轴上逐个像素地绘制,并考虑何时必须遵循另一个轴(您按八分圆绘制,因为这样您就不会处理大于一个导数,您只需决定是否你是否升职)。该例程还考虑了圆对称性,仅计算一个八分圆,然后在每次通过时绘制八个点。
当我年轻时从头开始开发该算法时(以前没有见过布雷森汉姆的算法),我对解决方案的推理可能会对您有所帮助。
第一个尝试是考虑到小圆圈的分辨率(粒度与角度无关),您必须比大圆圈绘制更少的像素,并且您遵循的单度方法必须重新设计以适应更精细或粗略的分辨率。这个想法是,一个像素一个像素地进行,而不是逐度进行,直到绘制出完整的东西。我们将仅绘制第一个八分圆,并根据图形的对称性绘制其余部分。我们从(0, -R)
点并将逐个像素地移动,直到我们到达(sqrt(2)*R, R - sqrt(2)*R)
point.
我们要做的第一件事是尝试保存我们必须执行的所有操作。我们可以节省运算的第一个地方是计算平方......我们将使用R^2 = x^2 + y^2
方程及其上,R
仅用作R^2
一直以来,所以,假设我们想要绘制一个十像素半径的圆,我们将其平方为100
(即 10 像素半径的平方)。
接下来,我们将看到正方形的一个属性,即它们从一个正方形到下一个正方形的增长是奇数(0 -> 1(delta is 1) -> 4(delta is 3) -> 9(delta is 5) -> 16(delta is 7) ...
)因此,如果我们可以安排 x 增长 1,我们可以轻松计算 x^2,只需将 2 添加到odd
变量,然后添加odd
到最后一个平方数,所以我们将使用两个数字:x
and x2
。我们将两者初始化为0
,第一个增长为x += 1;
,而第二个则随着关系而增长x2 += dx2; dx2 += 2;
(我们初始化dx2 = 1;
)这让我们允许x
and x2
只通过求和来增长,根本不做乘法。
如果有人认为我们需要y2 = 100 - x2
然后我们被迫计算y = sqrt(y2)
你是almost是的,但这里的技巧是能够管理y
and y2
向后序列与 x 对应序列相同。嗯,对了,y
and y2
可以以相同的方式进行相反的管理x
and x2
但这一次我们必须倒退,将奇数从(什么?)减少到1
where dy2 -= 2; y2 -= dy2;
并最终达到0
。为此,请检查两个连续方格之间的差值是否恰好是相加的两个数字,因此,例如,13^2 = 169
and 14^2 = 196
is 13 + 14 = 27
,如果我们从R = 14
to 0
in y
.
让事情变得如此复杂的原因是,这样我们只用整数进行加法,而不需要进行乘法(好吧,乘法并不那么昂贵,但曾经有一段时间)好吧,我们最初对平方进行乘法半径R
,但我们在开始时只计算一次R^2
.
现在的想法是将原点设置在出发点(0, -R)
然后向右,逐个像素,添加(和修改)x
, x2
, and sum
(我们一直减去总和)直到我们到达下一个方格y
,然后更新所有 y 轴值(我们确实递减 y,我们必须在那一刻向上移动一个像素)y
, y2
, dy2
,然后绘制像素(或者之前绘制它,就像我们在例程中所做的那样),直到...什么? (嗯,重点是直到我们在 45 度点相遇,八分圆就完成了,x
and y
坐标相等)到此为止很重要,因为从这一点开始,一步可能使 y 坐标增加超过一个像素(导数大于一),这会使整体算法变得复杂(并且无论如何,我们正在绘制其他对称的八个点,因此我们正在绘制图形的其他部分)
因此,假设我们的半径为 100,并开始:
x=0, x2= 0, dx2= 1, y=10, y2=100, dy2=19, sum=100 *
x=1, x2= 1, dx2= 3, y= 9, y2= 81, dy2=17, sum= 99
x=2, x2= 4, dx2= 5, y= 9, y2= 81, dy2=17, sum= 96
x=3, x2= 9, dx2= 7, y= 9, y2= 81, dy2=17, sum= 91
x=4, x2=16, dx2= 9, y= 9, y2= 81, dy2=17, sum= 84 *
x=5, x2=25, dx2=11, y= 8, y2= 64, dy2=15, sum= 75 *
x=6, x2=36, dx2=13, y= 7, y2= 49, dy2=13, sum= 64 *
x=7, x2=49, dx2=15, y= 7, y2= 49, dy2=13, sum= 51
标有星号的点是sum
值跨越下一个y2
值,使得y
要递减的值并且必须移动我们绘制的像素。最终的例程是:
int bh_onlyonepoint(int r, int cx, int cy)
{
int r2 = r*r;
int x = 0, x2 = 0, dx2 = 1;
int y = r, y2 = y*y, dy2 = 2*y - 1;
int sum = r2;
while(x <= y) {
draw(cx + x, cy + y); /* draw the point, see below */
sum -= dx2;
x2 += dx2;
x++;
dx2 += 2;
if (sum <= y2) {
y--; y2 -= dy2; dy2 -= 2;
}
} /* while */
return x; /* number of points drawn */
}
如果你愿意,我写了一个简单的例子,在屏幕上用 ascii 画一个圆,给定半径作为命令参数。在绘制单个星号之前,它使用 ANSI 转义序列将光标定位在屏幕中。 X 方向上的比例加倍,以补偿字符高度(ascii 中的“像素”不是方形的)我已经包含了一个新的绘图函数指针参数来回调点绘图和一个main
从命令行获取参数的例程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void draw(int x, int y)
{
/* move to position (2*x, y) and plot an asterisk */
printf("\033[%d;%dH*", y, x<<1);
}
int bh(int r, int cx, int cy, void(*draw)(int, int))
{
/* the variables mentioned in the text */
int r2 = r*r;
int x = 0, x2 = 0, dx2 = 1;
int y = r, y2 = y*y, dy2 = 2*y - 1;
int sum = r2;
while(x <= y) {
/* draw the eight points */
draw(cx + x, cy + y);
draw(cx + x, cy - y);
draw(cx - x, cy + y);
draw(cx - x, cy - y);
draw(cx + y, cy + x);
draw(cx + y, cy - x);
draw(cx - y, cy + x);
draw(cx - y, cy - x);
sum -= dx2;
x2 += dx2;
x++;
dx2 += 2;
if (sum <= y2) {
y--; y2 -= dy2; dy2 -= 2;
}
} /* while */
return x; /* number of points drawn */
}
int main(int argc, char **argv)
{
int i;
char *cols = getenv("COLUMNS");
char *lines = getenv("LINES");
int cx, cy;
if (!cols) cols = "80";
if (!lines) lines = "24";
cx = atoi(cols)/4;
cy = atoi(lines)/2;
printf("\033[2J"); /* erase screen */
for (i = 1; i < argc; i++) {
bh(atoi(argv[i]), cx, cy, draw);
}
fflush(stdout);
sleep(10);
puts(""); /* force a new line */
}
最终结果是:
*
* * * * * * * * * *
* * * *
* *
* * * *
* * *
* * * * * * * * * *
* * * *
* * * * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * *
* * * * * *
* * * *
* * * * * * * * * *
* * *
* * * *
* *
* * * *
* * * * * * * * * *
*
最后,如果您想要更好的结果(不是那些由精确的半径值产生的峰值,使它们仅在 x 或 y 为零时接触点),您可以直接向例程传递半径的平方值(这允许使带分数半径的整数计算)
填充一个圆圈
如果要填充圆,只需绘制一对计算点之间的所有点,如下所示:
lineFromTo(cx - x, cy - y, cx + x, cy - y);
lineFromTo(cx - y, cy + x, cx + y, cy + x);
lineFromTo(cx - y, cy - x, cx + y, cy - x);
lineFromTo(cx - x, cy + y, cx + x, cy + y);
这些都是水平线,所以也许您可以通过以下方式获得改进:
/* X1 X2 Y */
HorizLineX1X2Y(cx - x, cx + x, cy - y);
HorizLineX1X2Y(cx - y, cx + y, cy + x);
HorizLineX1X2Y(cx - y, cx + y, cy - x);
HorizLineX1X2Y(cx - x, cx + x, cy + y);
已创建一个新的 git 存储库,其中包含允许填充、绘制或跟踪算法运行的最终程序github https://github.com/mojadita/bresenham.git