【发布时间】:2009-07-29 15:42:44
【问题描述】:
我正在使用Bresenham's circle algorithm 快速绘制圆。但是,我也想(应用户的要求)画一个实心圆圈。
有没有一种快速有效的方法来做到这一点?与 Bresenham 类似的东西?
我使用的语言是 C。
【问题讨论】:
标签: c algorithm graphics geometry
我正在使用Bresenham's circle algorithm 快速绘制圆。但是,我也想(应用户的要求)画一个实心圆圈。
有没有一种快速有效的方法来做到这一点?与 Bresenham 类似的东西?
我使用的语言是 C。
【问题讨论】:
标签: c algorithm graphics geometry
读过the Wikipedia page on Bresenham's (also 'Midpoint') circle algorithm,看起来最简单的事情就是修改它的动作,而不是
setPixel(x0 + x, y0 + y);
setPixel(x0 - x, y0 + y);
和类似的,每次你都这样做
lineFrom(x0 - x, y0 + y, x0 + x, y0 + y);
也就是说,对于 Bresenham 的每一对点(具有相同的 y)绘图,你改为用一条线连接。。 p>
【讨论】:
只需使用蛮力。这个方法迭代了太多的像素,但它只使用整数乘法和加法。您完全避免了 Bresenham 的复杂性和 sqrt 可能存在的瓶颈。
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);
【讨论】:
if(x*x+y*y < radius*radius + radius) 也只是为了得到圆圈(环),你可以这样做if(x*x+y*y > radius*radius - radius && x*x+y*y < radius*radius + radius)
<= 替换为<,将radius * 0.8f 替换为+ radius 另一个if 语句不相关(当您只需要一个大纲时)。跨度>
这是一个 C# 粗略指南(应该不难为 C 找到正确的想法) - 这是没有使用 Bresenham 消除重复平方根的“原始”形式。
Bitmap bmp = new Bitmap(200, 200);
int r = 50; // radius
int ox = 100, oy = 100; // origin
for (int x = -r; x < r ; x++)
{
int height = (int)Math.Sqrt(r * r - x * x);
for (int y = -height; y < height; y++)
bmp.SetPixel(x + ox, y + oy, Color.Red);
}
bmp.Save(@"c:\users\dearwicker\Desktop\circle.bmp");
【讨论】:
你可以用这个:
void DrawFilledCircle(int x0, int y0, int radius)
{
int x = radius;
int y = 0;
int xChange = 1 - (radius << 1);
int yChange = 0;
int radiusError = 0;
while (x >= y)
{
for (int i = x0 - x; i <= x0 + x; i++)
{
SetPixel(i, y0 + y);
SetPixel(i, y0 - y);
}
for (int i = x0 - y; i <= x0 + y; i++)
{
SetPixel(i, y0 + x);
SetPixel(i, y0 - x);
}
y++;
radiusError += yChange;
yChange += 2;
if (((radiusError << 1) + xChange) > 0)
{
x--;
radiusError += xChange;
xChange += 2;
}
}
}
【讨论】:
我喜欢 palm3D 的回答。对于蛮力来说,这是一个非常快速的解决方案。没有平方根或三角函数可以减慢它的速度。它的一个弱点是嵌套循环。
将其转换为单个循环使该函数几乎快两倍。
int r2 = r * r;
int area = r2 << 2;
int rr = r << 1;
for (int i = 0; i < area; i++)
{
int tx = (i % rr) - r;
int ty = (i / rr) - r;
if (tx * tx + ty * ty <= r2)
SetPixel(x + tx, y + ty, c);
}
这种单循环解决方案可与线条绘制解决方案的效率相媲美。
int r2 = r * r;
for (int cy = -r; cy <= r; cy++)
{
int cx = (int)(Math.Sqrt(r2 - cy * cy) + 0.5);
int cyy = cy + y;
lineDDA(x - cx, cyy, x + cx, cyy, c);
}
【讨论】:
Intel(R) Core(TM) i7-4980HQ CPU @ 2.80GHz
这里有好主意! 由于我在一个需要绘制数千个圆的项目中,我在这里评估了所有建议(并通过预先计算半径的平方改进了一些):
http://quick-bench.com/mwTOodNOI81k1ddaTCGH_Cmn_Ag
Rev 变体只是交换了 x 和 y,因为我的网格/画布结构的工作方式使沿 y 轴的连续访问更快。
明显的赢家是 Daniel Earwicker 的方法 (DrawCircleBruteforcePrecalc),它预先计算 Y 值以避免不必要的半径检查。有点令人惊讶的是,它否定了 sqrt 调用引起的额外计算。
一些 cmets 建议使用单循环的 kmillen 变体 (DrawCircleSingleLoop) 应该非常快,但在这里它是最慢的。我认为这是因为所有的部门。但也许我已经将它错误地适应了该代码中的全局变量。如果有人看一下就好了。
编辑:自从大学时代以来第一次寻找一些汇编代码后,我设法发现圈子起源的最后添加是罪魁祸首。 预先计算这些,根据板凳数据,我将最快的方法提高了 3.7-3.9 倍! http://quick-bench.com/7ZYitwJIUgF_OkDUgnyMJY4lGlA 太棒了。
这是我的代码:
for (int x = -radius; x < radius ; x++)
{
int hh = (int)std::sqrt(radius_sqr - x * x);
int rx = center_x + x;
int ph = center_y + hh;
for (int y = center_y-hh; y < ph; y++)
canvas[rx][y] = 1;
}
【讨论】:
我发现palm3D 的蛮力算法是一个很好的起点。此方法使用相同的前提,但是它包含几种跳过检查大部分像素的方法。
首先,代码如下:
int largestX = circle.radius;
for (int y = 0; y <= radius; ++y) {
for (int x = largestX; x >= 0; --x) {
if ((x * x) + (y * y) <= (circle.radius * circle.radius)) {
drawLine(circle.center.x - x, circle.center.x + x, circle.center.y + y);
drawLine(circle.center.x - x, circle.center.x + x, circle.center.y - y);
largestX = x;
break; // go to next y coordinate
}
}
}
接下来是解释。
首先要注意的是,如果您找到给定水平线在圆内的最小 x 坐标,您会立即知道最大 x 坐标。 这是由于圆的对称性。如果最小 x 坐标在圆的边界框左侧前 10 个像素,则最大 x 是在圆形边界框的右侧后 10 个像素。
从高 x 值迭代到低 x 值的原因是,可以用更少的迭代次数找到最小 x 值。这是因为对于大多数线来说,最小 x 值比圆的中心 x 坐标更靠近边界框的左侧,因为圆向外弯曲,如 this image 所示 接下来要注意的是,由于圆也是垂直对称的,所以你找到的每条线都会给你一条免费的第二条线来绘制,每次你在圆的上半部分找到一条线,你会在下半部分得到一条线半径-y 坐标。因此,当找到任何一条线时,可以绘制两条,并且只需要迭代 y 值的上半部分。
最后要注意的是,如果你从位于圆心的y值开始,然后向顶部移动y,那么下一行的最小x值必须更靠近中心圆的 x 坐标比最后一行。这也是因为当你沿着圆圈向上时,圆圈会向中心 x 值弯曲。 Here is a visual on how that is the case.
总结:
您还可以存储(radius * radius) 和(y * y) 的值,而不是计算它们
多次。
【讨论】:
这是我的做法:
我正在使用具有两位精度的定点值(我们必须管理半点和半点的平方值)
正如前面的回答中提到的,我也使用平方值而不是平方根。
首先,我在圆的 1/8 部分检测到我的圆的边界限制。我正在使用这些点的对称来绘制圆的 4 个“边界”。然后我在圆圈内画正方形。
与中点圆算法不同,这个算法适用于偶数直径(也适用于实数直径,稍有变化)。
如果我的解释不清楚,请原谅我,我是法国人;)
void DrawFilledCircle(int circleDiameter, int circlePosX, int circlePosY)
{
const int FULL = (1 << 2);
const int HALF = (FULL >> 1);
int size = (circleDiameter << 2);// fixed point value for size
int ray = (size >> 1);
int dY2;
int ray2 = ray * ray;
int posmin,posmax;
int Y,X;
int x = ((circleDiameter&1)==1) ? ray : ray - HALF;
int y = HALF;
circlePosX -= (circleDiameter>>1);
circlePosY -= (circleDiameter>>1);
for (;; y+=FULL)
{
dY2 = (ray - y) * (ray - y);
for (;; x-=FULL)
{
if (dY2 + (ray - x) * (ray - x) <= ray2) continue;
if (x < y)
{
Y = (y >> 2);
posmin = Y;
posmax = circleDiameter - Y;
// Draw inside square and leave
while (Y < posmax)
{
for (X = posmin; X < posmax; X++)
setPixel(circlePosX+X, circlePosY+Y);
Y++;
}
// Just for a better understanding, the while loop does the same thing as:
// DrawSquare(circlePosX+Y, circlePosY+Y, circleDiameter - 2*Y);
return;
}
// Draw the 4 borders
X = (x >> 2) + 1;
Y = y >> 2;
posmax = circleDiameter - X;
int mirrorY = circleDiameter - Y - 1;
while (X < posmax)
{
setPixel(circlePosX+X, circlePosY+Y);
setPixel(circlePosX+X, circlePosY+mirrorY);
setPixel(circlePosX+Y, circlePosY+X);
setPixel(circlePosX+mirrorY, circlePosY+X);
X++;
}
// Just for a better understanding, the while loop does the same thing as:
// int lineSize = circleDiameter - X*2;
// Upper border:
// DrawHorizontalLine(circlePosX+X, circlePosY+Y, lineSize);
// Lower border:
// DrawHorizontalLine(circlePosX+X, circlePosY+mirrorY, lineSize);
// Left border:
// DrawVerticalLine(circlePosX+Y, circlePosY+X, lineSize);
// Right border:
// DrawVerticalLine(circlePosX+mirrorY, circlePosY+X, lineSize);
break;
}
}
}
void DrawSquare(int x, int y, int size)
{
for( int i=0 ; i<size ; i++ )
DrawHorizontalLine(x, y+i, size);
}
void DrawHorizontalLine(int x, int y, int width)
{
for(int i=0 ; i<width ; i++ )
SetPixel(x+i, y);
}
void DrawVerticalLine(int x, int y, int height)
{
for(int i=0 ; i<height ; i++ )
SetPixel(x, y+i);
}
要使用非整数直径,您可以提高定点精度或使用双精度值。 甚至应该可以根据 dY2 + (ray - x) * (ray - x) 和 ray2 (dx² + dy² and r²) 之间的差异来制作一种抗锯齿
【讨论】:
如果你想要一个快速的算法,考虑画一个有N条边的多边形,N越高,圆越精确。
【讨论】:
我会生成一个点列表,然后使用多边形绘制函数进行渲染。
【讨论】:
它可能不是您正在寻找的算法,也不是性能最高的算法,
但我总是这样做:
void fillCircle(int x, int y, int radius){
// fill a circle
for(int rad = radius; rad >= 0; rad--){
// stroke a circle
for(double i = 0; i <= PI * 2; i+=0.01){
int pX = x + rad * cos(i);
int pY = y + rad * sin(i);
drawPoint(pX, pY);
}
}
}
【讨论】:
以下两种方法通过一次绘制圆的多个部分来避免重复的平方根计算,因此应该相当快:
void circleFill(const size_t centerX, const size_t centerY, const size_t radius, color fill) {
if (centerX < radius || centerY < radius || centerX + radius > width || centerY + radius > height)
return;
const size_t signedRadius = radius * radius;
for (size_t y = 0; y < radius; y++) {
const size_t up = (centerY - y) * width;
const size_t down = (centerY + y) * width;
const size_t halfWidth = roundf(sqrtf(signedRadius - y * y));
for (size_t x = 0; x < halfWidth; x++) {
const size_t left = centerX - x;
const size_t right = centerX + x;
pixels[left + up] = fill;
pixels[right + up] = fill;
pixels[left + down] = fill;
pixels[right + down] = fill;
}
}
}
void circleContour(const size_t centerX, const size_t centerY, const size_t radius, color stroke) {
if (centerX < radius || centerY < radius || centerX + radius > width || centerY + radius > height)
return;
const size_t signedRadius = radius * radius;
const size_t maxSlopePoint = ceilf(radius * 0.707106781f); //ceilf(radius * cosf(TWO_PI/8));
for (size_t i = 0; i < maxSlopePoint; i++) {
const size_t depth = roundf(sqrtf(signedRadius - i * i));
size_t left = centerX - depth;
size_t right = centerX + depth;
size_t up = (centerY - i) * width;
size_t down = (centerY + i) * width;
pixels[left + up] = stroke;
pixels[right + up] = stroke;
pixels[left + down] = stroke;
pixels[right + down] = stroke;
left = centerX - i;
right = centerX + i;
up = (centerY - depth) * width;
down = (centerY + depth) * width;
pixels[left + up] = stroke;
pixels[right + up] = stroke;
pixels[left + down] = stroke;
pixels[right + down] = stroke;
}
}
【讨论】: