数值微分(DDA)算法
首先从最直观的DDA算法说起,已知过端点P0 (x0, y0), P1(x1, y1)的直线段L:y = kx + b,容易得知直线斜率为:k = (y1-y0)/(x1-x0),(假设x1 ≠ x0)。
我们假设|k| ≤ 1,这样x每增加1,y将增加k,并且保证x每增加1,y的增量不能大于1;如果|k| > 1,则应该将x和y互换。由于k是浮点数,因此算法中需要将y舍入为int型,并圆整到最接近的位置。
算法本身也很简单,C++形式的算法实现如下:
void drawLineDDA(int x0, int y0, int x1, int y1, long color)
{
int dx = x1- x0;
itn dy = y1- y0;
float k = dy/dx;
float y = y0;
for (int x = x0; x <= x1; x++){
setpixel (x, int(y+0.5), color);
y=y+k;
}
}
很明显DDA算法在每次迭代中的x, y值是上一步的值加上一个增量获得的,因此它是一个增量算法。
但是这种方法直观,但效率太低,因为每一步需要一次浮点乘法和一次舍入运算。
Bresenham画线算法
Bresenham画线算法是使用最广泛的直线生成算法,DDA算法确定下一个点坐标的方式是根据直接斜率k得到,由于k是浮点数,因此将不得不使用浮点运算,并将浮点数转换整数。而Bresenham算法的思想则是根据误差项来决定下一个像素是取正右方还是右上方(|k| < 1的情况),因此就不需要求斜率k的除法运算和浮点数运算,提高了效率。
假设以确定了第i个点Pi(xi, yi),接下来需要确定第i+1个点P(x, y),那么根据直线方程有:
y = yi + k;
如果delta(i+1) = y – (yi + 0.5f) < 0,那么表明P距C点较近,取C点,否则取D点。左图中P将取为D点。当i=0时有,delta(1) = y1 – (0+0.5) = -0.5 (k < 1)。由此我们可以得到画线的方法。
1) 求出k = (y1 – y0)/(x1 – x0),初始化delta = -0.5;
2) 迭代x,每次delta += k,如果delta ≥ 0则表明需要取右上方的点,使y增1,同时使e减1;
算法代码如下:
int dx = x1- x0;
itn dy = y1- y0;
float k = dy/dx, delta = -0.5f;
for (int x = x0, y = y0; x <= x1; x++){
setpixel (x, y, color);
delta +=k;
if(delta >= 0){delta -= 1; y++;}
}
}
考察上面的方法,依然需要计算k和使用浮点数;好像比起DDA没有什么改进嘛,只不过是将浮点运算搬到了误差项delta上而已,其实我们距目标已经非常近了,从算法中可以看到只用到误差项delta的符号来判断,因此可以做如下的简单替换:
e = delta*dx*2
现在一切水落石出了,我们已经可以消除浮点数了,也不必计算k了。好了,下面是完整的Bresenham算法程序:
void drawLineBresenham(int x0, int y0, int x1, int y1, long color)
{
int dx = x1- x0;
itn dy = y1- y0;
if(dx >= dy){ // |k| <= 1
int e = -dx;
for (int x = x0, y = y0; x <= x1; x++){
setpixel (x, y, color);
e += 2*dy;
if(e >= 0){e -= 2*dx; y++;}
}
}
else{ // |k| > 1
int e = -dy;
for (int x = x0, y = y0; y <= y1; y++){
setpixel (x, y, color);
e += 2*dx;
if(e >= 0){e -= 2*dy; x++;}
}
}
}