在SO上,找到了以下用于绘制实心圆的简单算法:
for(int y=-radius; y<=radius; y++)
for(int x=-radius; x<=radius; x++)
if(x*x+y*y <= radius*radius)
setpixel(origin.x+x, origin.y+y);
是否有同样简单的绘制填充椭圆的算法?
答案 0 :(得分:15)
更简单,没有double
且没有除法(但要注意整数溢出):
for(int y=-height; y<=height; y++) {
for(int x=-width; x<=width; x++) {
if(x*x*height*height+y*y*width*width <= height*height*width*width)
setpixel(origin.x+x, origin.y+y);
}
}
我们可以利用两个事实来显着优化这一点:
第一个事实是节省了四分之三的工作(几乎);第二个事实极大地减少了测试次数(我们只测试椭圆的边缘,即使在那里我们也不必测试每个点)。
int hh = height * height;
int ww = width * width;
int hhww = hh * ww;
int x0 = width;
int dx = 0;
// do the horizontal diameter
for (int x = -width; x <= width; x++)
setpixel(origin.x + x, origin.y);
// now do both halves at the same time, away from the diameter
for (int y = 1; y <= height; y++)
{
int x1 = x0 - (dx - 1); // try slopes of dx - 1 or more
for ( ; x1 > 0; x1--)
if (x1*x1*hh + y*y*ww <= hhww)
break;
dx = x0 - x1; // current approximation of the slope
x0 = x1;
for (int x = -x0; x <= x0; x++)
{
setpixel(origin.x + x, origin.y - y);
setpixel(origin.x + x, origin.y + y);
}
}
这是有效的,因为每条扫描线比前一条扫描线短,至少相同 因为那个比之前的那个短。由于四舍五入到整数像素坐标,这并不完全准确 - 线可以缩短一个像素。
换句话说,从最长的扫描线(水平直径)开始,每条线比前一条线短的数量,在代码中表示为dx
,减少最多一条,保持不变相同或增加。第一个内部for
找到当前扫描线比前一个扫描线更短的精确数量,从dx - 1
开始向上,直到我们落入椭圆内部。
| x1 dx x0
|###### |<-->|
current scan line --> |########### |<>|previous dx
previous scan line --> |################ |
two scan lines ago --> |###################
|#####################
|######################
|######################
+------------------------
要比较内椭圆测试的数量,每个星号是在天真版本中测试的一对坐标:
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
*********************************************
......并在改进版中:
*
**
****
***
***
***
**
**
答案 1 :(得分:7)
椭圆(关于原点)是沿 x 或 y 轴线性拉伸的圆。所以你可以修改你的循环:
for(int y=-height; y<=height; y++) {
for(int x=-width; x<=width; x++) {
double dx = (double)x / (double)width;
double dy = (double)y / (double)height;
if(dx*dx+dy*dy <= 1)
setpixel(origin.x+x, origin.y+y);
}
}
你可以看到,如果 width == height == radius ,那么这相当于你画圆圈的代码。
答案 2 :(得分:7)
替换
x*x+y*y <= radius*radius
带
Axx*x*x + 2*Axy*x*y + Ayy*y*y < radius*radius
你有三个常数,Axx,Axy,Ayy。当Axy = 0时,椭圆的轴将水平和垂直。 Axx = Ayy = 1成一个圆圈。 Axx越大,宽度越小。类似于Ayy和身高。对于以任何给定角度倾斜的任意椭圆,需要一些代数来计算常数。
Mathematically Axx,Axy,Ayy是一个“张量”,但也许你不想进入那些东西。
更新 - 详细的数学。我不认为S.O.可以像Math S.E.那样做很好的数学。所以这看起来很粗糙。 你想用x,y坐标中的椭圆绘制(或做任何事情)。椭圆是倾斜的。我们创建了一个与椭圆对齐的替代坐标系x',y'。显然,椭圆上的点满足
(x'/a)^2 + (y'/b)^2 = 1
通过考虑一些精心挑选的随机点,我们看到了
x' = C*x + S*y
y' = -S*x + C*y
其中S,C是sin(θ)和cos(θ),θ是x'轴w.r.t的角度。 x轴。我们可以使用符号 x =(x,y)来缩短这一点,并且对于引用的符号类似,并且R是一个涉及C和S的2x2矩阵:
x' = R x
可以写出椭圆方程
T( x')A'' x' = 1
其中'T'表示转置,并删除'^'以避免戳到眼中的每个人,因此“a2”实际上意味着^ 2,
A''=
1/a2 0
0 1/b2
使用 x' = R x ,我们找到了
T(R x )A''R x = 1
T( x )T(R)A''R x = 1
T( x )A x = 1
其中A,你需要知道的使你的倾斜绘图扫描线算法工作的事情是
A = T(R)A''R =
C2/a2+S2/b2 SC(1/a2-1/b2)
SC/(1/a2-1/b2) S2/a2 + C2/b2
根据T( x )A x 将这些乘以x和y乘以你得到它。
答案 3 :(得分:5)
由paper提出的快速Bresenham类型算法非常有效。这是我为同一篇文章写的OpenGL implementation。
基本前提是您在一个象限上绘制曲线,我们可以将其镜像到其他三个象限。这些顶点使用误差函数计算,类似于您在圆的中点圆算法中使用的顶点。我上面链接的论文对方程式有一个非常好的证明,并且算法提炼到仅检查给定顶点是否在椭圆内,只需通过在误差函数中替换它的值。该算法还跟踪我们在第一象限中绘制的曲线的切线斜率,并根据斜率值递增x或y - 这进一步提高了算法的性能。这是一张显示正在发生的事情的图片:
至于填充椭圆,一旦我们知道每个象限中的顶点(基本上是跨越x和y轴的镜像反射),我们为每个计算的顶点得到4个顶点 - 这足以绘制四边形(in无论如何OpenGL)。一旦我们为所有这些顶点绘制四边形,我们就得到一个填充的椭圆。由于性能原因,我给出的实现采用VBO,但您并不严格需要它。
该实现还向您展示了如何使用三角形和直线而不是绘制四边形来实现填充椭圆 - 尽管四边形明显更好,因为它是一个基元,我们只为4个顶点绘制一个四边形,而不是一个三角形三角形实现中的每个顶点。