图形学课本, 按规矩介绍完矩阵行列式, 第一个算法肯定就是Bresenham画线算法了.
來我们來看看算法
Bresenham是用来画一些不反走样的线段的. 都说了线段肯定有起点和终点, 假设我们:
(xf, yf) ==[LINETO]==> (xt, yt)
按照一些初中(好像是初中吧忘了)的几何, 这条直线的方程是:
yt - yf
y - yf = --------- (x - xf)
xt - xf
好我们來变一下型. 首先恶心的除号就拿掉. 然后令dy = yt - yf
, dx = xt - xf
:
(y - yf)(xt - xf) = (x - xf)(yt - yf)
y dx - yf dx = x dy - xf dy
到这里我们可以想想这个直线可以怎样画了. 对于一条斜率在1以下的直线, 相邻两点要么是同一行, 要么是斜角. 也就是说, 当前点是(x, y)
的话, 下一个点要么是(x+1, y)
, 要么是(x+1, y+1)
.
^ ,------ This is the CURRENTLY drawn pixel.
| V _
| _|_| <----- Which is the next
| |_|_| <----- pixel to draw ?
|
+-------->
设一个判别式 f(x, y) = x dy - y dx + yf dx - xf dy
, 留意到 dx > 0
且dy > 0
. 所以, 如果f(x, y) < 0
, 说明y太大了, (x, y)
在直线上方. 反之f(x, y) > 0
, 说明x太大了.
^
|
|####/:::
|###/:::: #. Area where f(x, y) < 0
|##/::::: :. Area where f(x, y) > 0
|#/:::::: /. Points on the line meet f(x, y) = 0
+/-------->
/
接下来的就耳熟能详了, 比较右边的点和右上的点的f(x, y)
, 画小的那个. 包含进上一次 image.h 那个头文件, 然后就写出第一个画线函数:
cpp
void bresenham_1(int xf, int yf, int xt, int yt, const color& c, image& img) { auto f = [&](int x, int y) { int dx = xt - xf; int dy = yt - yf; return x*dy - y*dx + yf*dx - xf*dy; }; for(int x = xf, y = yf; x <= xt; x++) { if(abs(f(x, y)) - abs(f(x, y+1)) < 0) img(x, y) = c; else img(x, ++y) = c; } }
优化时间
事情说到这里, 算法已经能工作了, 接下来就是优化. 能在什么地方优化呢? Bresenham卖广告总是在说"全程只用加减法, 效率超高~". 但是一个看上去必须要用乘法的直线公式, 怎么都没办法优化到只用加减法吧...
其实乘法是在重复计算 :-)小学时候学乘法, 不都说4x3是四个3加起来吗, 所以乘法本身就是加法组成的啊. 而重复的部分, 就是你已经算了3x3, 你只需要再加上一个3就可以得到4x3了. 同样地, 当前判别式的结果, 我们只需要在上一个判别式的结果基础上加上一点什么就行了. 至于是什么, 就要看推导了:
For x = xf, y = yf: f(xf, yf) = 0
f(x+1, y) = (x+1) dy - y dx + yf dx - xf dy
= f(x, y) + dy * >= 0 *
f(x+1, y+1) = (x+1) dy - (y+1) dx + yf dx - xf dy
= f(x, y) + dy - dx * <= 0 *
这个递推式就是我们需要的. 写下的代码是这样的:
cpp
void bresenham_2(int xf, int yf, int xt, int yt, const color& c, image& img) { int f = 0; int dx = xt - xf; int dy = yt - yf; for(int x = xf, y = yf; x <= xt; x++) { img(x, y) = c; if((f + dy) + (f + dy - dx) < 0) f += dy; else { f += dy - dx; y++; } } }
注: 有的同学发现我这个结果跟Computer Graphics 2nd Edition, D. Hearn & M.P. Baker的公式有点不太一样. 其实仔细看一下是一样的, 只不过变了个型. 那本书的公式写出的程序是这个样子的:
cpp
void bresenham_3(int xf, int yf, int xt, int yt, const color& c, image& img) { int dx = xt - xf; int dy = yt - yf; int f = dy + dy - dx; for(int x = xf, y = yf; x <= xt; x++) { img(x, y) = c; if(f < 0) f += dy + dy; else { f += dy + dy - dx - dx; y++; } } }
补充完整
void bresenham_4(int xf, int yf, int xt, int yt, const color& c, image& img)
{
if(xf > xt) {
int t;
t = xf; xf = xt; xt = t;
t = yf; yf = yt; yt = t;
}
int dx = xt - xf;
int dy = yt - yf;
int f = 0;
if(dy >= 0 && dy <= dx) {
for(int x = xf, y = yf; x <= xt; x++) {
img(x, y) = c;
if((f + dy) + (f + dy - dx) < 0)
f += dy;
else { f += dy - dx; y++; }
}
} else if(dy >= 0 && dy > dx) {
for(int x = xf, y = yf; y <= yt; y++) {
img(x, y) = c;
if((f - dx) + (f - dx + dy) > 0)
f += -dx;
else { f += -dx + dy; x++; }
}
} else if(dy <= 0 && -dy <= dx) {
for(int x = xf, y = yf; x <= xt; x++) {
img(x, y) = c;
if((f + dy) + (f + dy + dx) > 0)
f += dy;
else { f += dy + dx; y--; }
}
} else if(dy <= 0 && -dy > dx) {
for(int x = xf, y = yf; y >= yt; y--) {
img(x, y) = c;
if((f + dx) + (f + dy + dx) < 0)
f += dx;
else { f += dy + dx; x++; }
}
}
}
测试一下 :-)
cpp
int main() { image img(800, 600); bresenham_1(20, 20, 500, 50, color(uint32_t(0x00000000)), img); bresenham_2(20, 30, 500, 60, color(uint32_t(0x00000000)), img); bresenham_3(20, 40, 500, 70, color(uint32_t(0x00000000)), img); // completed line algorithm for(int i = 0; i < 50; i++) { bresenham_4(rand() % 800, rand() % 600, rand() % 800, rand() % 600, color(uint32_t(0x00000000)), img); } ofstream f("3-1-line.ppm"); f << img; f.close(); }
好了有人该警告我不要偷懒用
來画图了, 老老实实贴上来吧 (压缩成jpg之后那种漂亮的Minecraft风格的直线变得很模糊糟透了)