目前常见的显示设备为光栅化显示器。此类设备采用像素的方式显示图像,也就意味着要在光栅化显示器上画出完美的直线,倾角必须是45k度(k为整数)。
如果不满足角度为45的倍数,需要采用近似的方法,比如,可以选择像素点到实际直线的距离最短者。先考虑斜率介于0和1之间的情况:如上图所示,当点被选择为直线的一部分时,只可能是右侧或者右上侧的点。此时,可以由两点到直线的距离之差()的符号判定选择何者。根据相似三角形原理,其符号必然与相同。
又由于,所以上式符号与相同。令此变量为“误差值”,记为。Bresenham算法即是通过此误差值的正负来决定下一像素点的选取。但是算法不是每一步都重新计算此值,而是通过递推式更新。这是Bresenham算法高效的原因。
误差值的递推式如下:
递推关系很容易可以证明,如果需要,可以参考Bresenham的原论文,证明非常详细。
现在,我们需要将算法推广到整个二维平面。首先,忽略递推关系的变化,当分别为负和为正时,所对应的下一个像素点不再固定是右侧和右上侧(也就是x加一或者x、y都加一)。但是,x坐标和y坐标的增量可以简单地由线段起始矢量的符号确定。试看下图:
另外,当直线斜率的绝对值大于1时,算法每一步都保证y加一,而不是x加一。这种情况下需要将 和调换,并修改相应的递推式。
说这么多空话可能不容易理解,试结合C++实现与注释理解:
void bresenham(int x1, int y1, int x2, int y2) { // (x1, y1) to (x2, y2)
int deltax = std::abs(x1 - x2);
int deltay = std::abs(y1 - y2);
const int stepx = x1 < x2 ? 1 : -1; // 根据矢量方向,确定增量大小
const int stepy = y1 < y2 ? 1 : -1;
bool steep = false;
if (deltay > deltax) { // 如果斜率绝对值大于1,交换deltax和deltay
std::swap(deltax, deltay);
steep = true;
}
int deviation = (deltay << 1) + deltax;
for (int i = 0; i < deltax; ++i) {
setpixel(x1, y1); // 选定像素点(x1, y1)
if (deviation > 0) {
if (steep) {
x1 += stepx;
} else {
y1 += stepy;
}
deviation -= deltax << 1;
}
if (steep) {
y1 += stepy;
} else {
x1 += stepx;
}
deviation += deltay << 1;
}
}
以上代码效率可能不足。经过简化后,可以在几行内完成,但可读性下降很多1:
void bresenham(int x0, int y0, int x1, int y1) {
int dx = abs(x1 - x0), sx = x0 < x1 ? 1 : -1;
int dy = abs(y1 - y0), sy = y0 < y1 ? 1 : -1;
int err = (dx > dy ? dx : -dy) / 2;
while (setpixel(x0, y0), x0 != x1 || y0 != y1) {
int e2 = err;
if (e2 > -dx) { err -= dy; x0 += sx; }
if (e2 < dy) { err += dx; y0 += sy; }
}
}
Footnotes
-
用C语言画直线, https://zhuanlan.zhihu.com/p/30553006 ↩