[计算机图形学]任意斜率的中点画线法
中点画线法简介
中点画线法是经典的画线算法,相较于DDA(数值微分法),中点画线法避免了使用浮点数进行运算,可以仅使用整数运算来获得像素坐标点。
但是目前大部分的教科书中仅介绍了斜率为 0 < = k < = 1 0<=k<=1 0<=k<=1的情况,对其他情况并没有说明。因此,下面我会对任意斜率的中点画线法进行延申。
中点画线法在斜率为 0 < k < = 1 0<k<=1 0<k<=1下的推导
首先介绍经典的斜率为 0 < k < = 1 0<k<=1 0<k<=1的情况:
如图,假设在当前迭代中离直线最近的像素已确定为 P = ( x p , y p ) P=(x_p,y_p) P=(xp,yp),由于 x x x为最大位移方向,因此接下来的每一次迭代, x x x方向上都将增加一个像素单位,而 y y y方向上将不增加像素单位或增加一个像素单位。
那么问题就是究竟选择像素 P 1 = ( x p + 1 , y p ) P1=(x_p + 1,y_p) P1=(xp+1,yp)还是像素 P 2 = ( x p + 1 , y p + 1 ) P2=(x_p+1,y_p+1) P2=(xp+1,yp+1)?这取决于哪一个像素会离直线会更接近。中点画线法给出了一个判断哪个像素更接近的规则:
考虑介于线段 P 1 P 2 P1P2 P1P2的中点 M = ( x p + 1 , y p + 0.5 ) M=(x_p+1,y_p+0.5) M=(xp+1,yp+0.5)和线段 P 1 P 2 P1P2 P1P2与直线的交点 Q Q Q,
- 如果点 M M M在点 Q Q Q的下方,说明点 P 2 P2 P2离 Q Q Q更近,也就是 P 2 P2 P2离直线更近,因此选择 P 2 P2 P2像素
- 如果点 M M M在点 Q Q Q的上方,说明点 P 1 P1 P1离 Q Q Q更近,也就是 P 1 P1 P1离直线更近,因此选择 P 1 P1 P1像素
- 如果点 M M M与点 Q Q Q重合,说明点 P 1 P1 P1和点 P 2 P2 P2离 Q Q Q一样近,也就是点 P 1 P1 P1和点 P 2 P2 P2离直线一样近,可取 P 1 P1 P1或 P 2 P2 P2中的任意一个,这里规定取 P 1 P1 P1像素
下面,我们用数学表达式来重新表达上述规则:
假设上图中的直线为 F ( x , y ) = a x + b y + c = 0 F(x,y)=ax+by+c=0 F(x,y)=ax+by+c=0,
- 点 M M M在点 Q Q Q的下方,等价于点 M M M在直线下方,也就是 F ( x p + 1 , y p + 0.5 ) < 0 F(x_p+1,y_p+0.5)<0 F(xp+1,yp+0.5)<0,此时选择 P 2 P2 P2像素
- 点 M M M在点 Q Q Q的上方,等价于点 M M M在直线上方,也就是 F ( x p + 1 , y p + 0.5 ) > 0 F(x_p+1,y_p+0.5)>0 F(xp+1,yp+0.5)>0,此时选择 P 1 P1 P1像素
- 点 M M M与点 Q Q Q的重合,等价于点 M M M刚好在直线上,也就是 F ( x p + 1 , y p + 0.5 ) = 0 F(x_p+1,y_p+0.5)=0 F(xp+1,yp+0.5)=0,此时规定选择 P 1 P1 P1像素
拓展延伸:
这里要求直线函数 F ( x , y ) F(x,y) F(x,y)中的 b ≥ 0 b\geq0 b≥0,因为这样才能确定使得 F ( x , y ) < 0 F(x,y) <0 F(x,y)<0的点对应在直线下方,使得 F ( x , y ) > 0 F(x,y)>0 F(x,y)>0的点对应在直线上方。也就是说, b b b的正负决定了点与直线的对应关系。类似的,其实 a a a的正负也决定着上述的对应关系,只不过 b b b决定了上下关系, a a a决定了左右关系。
进一步构造判别式: d = F ( x p + 1 , y p + 0.5 ) d=F(x_p+1,y_p+0.5) d=F(xp+1,yp+0.5),用判别式重述上述规则:
- 当 d < 0 d<0 d<0时,选择 P 2 P2 P2像素
- 当 d > = 0 d>=0 d>=0时,选择 P 1 P1 P1像素
有了这个方法后,我们就能由当前的像素点 P = ( x p , y p ) P=(x_p,y_p) P=(xp,yp)推断下一个像素点了。
在后续的迭代过程中,如果每一次迭代都类似于上述过程,将中点带入直线 F ( x , y ) F(x,y) F(x,y)计算一下,这会牺牲很多计算资源。为了提高计算效率,节省计算资源,我们得想出一个办法解决来带入计算的问题。因为函数 F ( x , y ) F(x,y) F(x,y)表示的是一条直线,具有线性性质,因此我们可以使用加法(也就是所谓的增量法)来代替带入计算。
-
假设像素点 P = ( x p , y p ) P=(x_p,y_p) P=(xp,yp)的下一个像素点为 P 1 = ( x p + 1 , y p ) P1=(x_p + 1,y_p) P1=(xp+1,yp),那么推断下一个像素点的判别式就可以写成
d 1 = F ( x p + 2 , y p + 0.5 ) = a ( x p + 2 ) + b ( y p + 0.5 ) + c = a ( x p + 1 ) + b ( y p + 0.5 ) + c + a = F ( x p + 1 , y p + 0.5 ) + a = d + a \begin{aligned} d_1 &=F(x_p+2,y_p+0.5) \\ &=a(x_p+2)+b(y_p+0.5)+c\\ &=a(x_p+1)+b(y_p+0.5)+c+a\\ &=F(x_p+1,y_p+0.5)+a\\ &=d+a \end{aligned} d1=F(xp+2,yp+0.5)=a(xp+2)+b(yp+0.5)+c=a(xp+1)+b(yp+0.5)+c+a=F(xp+1,yp+0.5)+a=d+a -
假设像素点 P = ( x p , y p ) P=(x_p,y_p) P=(xp,yp)的下一个像素点为 P 2 = ( x p + 1 , y p + 1 ) P2=(x_p+1,y_p+1) P2=(xp+1,yp+1),那么推断下一个像素点的判别式就可以写成
d 1 = F ( x p + 2 , y p + 1 + 0.5 ) = a ( x p + 2 ) + b ( y p + 1 + 0.5 ) + c = a ( x p + 1 ) + b ( y p + 0.5 ) + c + a + b = F ( x p + 1 , y p + 0.5 ) + a + b = d + a + b \begin{aligned} d_1 &=F(x_p+2,y_p+1+0.5) \\ &=a(x_p+2)+b(y_p+1+0.5)+c\\ &=a(x_p+1)+b(y_p+0.5)+c+a+b\\ &=F(x_p+1,y_p+0.5)+a+b\\ &=d+a+b \end{aligned} d1=F(xp+2,yp+1+0.5)=a(xp+2)+b(yp+1+0.5)+c=a(xp+1)+b(yp+0.5)+c+a+b=F(xp+1,yp+0.5)+a+b=d+a+b
也就是说,推断下一个像素点的判别式可以直接由上一个判别式加一个数(这个数被称为增量)就能得到,这样子计算效率就大大提高了。
特别的,假设像素点
P
=
(
x
p
,
y
p
)
P=(x_p,y_p)
P=(xp,yp)就是直线的起点(也就是
F
(
x
p
,
y
p
)
=
0
F(x_p,y_p)=0
F(xp,yp)=0),那么有:
d
=
F
(
x
p
+
1
,
y
p
+
0.5
)
=
a
(
x
p
+
1
)
+
b
(
y
p
+
0.5
)
+
c
=
a
x
p
+
b
y
p
+
c
+
a
+
0.5
b
=
F
(
x
p
,
y
p
)
+
a
+
0.5
b
=
a
+
0.5
b
\begin{aligned} d &=F(x_p+1,y_p+0.5)\\ &=a(x_p+1)+b(y_p+0.5)+c \\ &=ax_p+by_p+c+a+0.5b\\ &=F(x_p,y_p)+a+0.5b\\ &=a+0.5b \end{aligned}
d=F(xp+1,yp+0.5)=a(xp+1)+b(yp+0.5)+c=axp+byp+c+a+0.5b=F(xp,yp)+a+0.5b=a+0.5b
为了避免
0.5
0.5
0.5这个浮点数运算,所以我们用
2
d
=
2
a
+
b
2d=2a+b
2d=2a+b替换判别式,反正也只是和
0
0
0进行比对,乘多少都无所谓。
同样的,后续的判别式也变成了
2
d
1
=
2
d
+
2
a
2d_1=2d+2a
2d1=2d+2a或
2
d
1
=
2
d
+
2
(
a
+
b
)
2d_1=2d+2(a+b)
2d1=2d+2(a+b)。
最后,我们给出中点画线法在斜率为 0 < k < = 1 0<k<=1 0<k<=1下的完整步骤:
- 给起点 ( x 1 , y 1 ) (x1,y1) (x1,y1)和终点 ( x 2 , y 2 ) (x_2,y_2) (x2,y2),其中 x 1 < x 2 x_1<x_2 x1<x2且直线斜率为 0 < k < = 1 0<k<=1 0<k<=1。
- 初始化。令 a = y 1 − y 2 , b = x 2 − x 1 , d = 2 a + b , d e t a 1 = 2 a , d e t a 2 = 2 ( a + b ) , x = x 1 , y = y 1 a=y_1-y_2,b=x_2-x_1,d=2a+b,deta_1=2a,deta_2=2(a+b),x=x1,y=y1 a=y1−y2,b=x2−x1,d=2a+b,deta1=2a,deta2=2(a+b),x=x1,y=y1。
- 画像素点 ( x , y ) (x,y) (x,y)
- 判断 x x x是否小于 x 2 x_2 x2。如果 x < x 2 x<x_2 x<x2,则执行步骤5,否则流程结束。
- 如果 d < 0 d<0 d<0,则 x + + ; y + + ; d + = d e t a 2 x++;y++;d+=deta_2 x++;y++;d+=deta2;如果 d > = 0 d>=0 d>=0,则 x + + ; d + = d e t a 1 x++;d+=deta_1 x++;d+=deta1;画像素点 ( x , y ) (x,y) (x,y),转到步骤4。
任意斜率的中点画线法
任意其他斜率的情况也可以如同上述进行分析,但注意在斜率 ∣ k ∣ > 1 |k|>1 ∣k∣>1时,每一步的最大位移方向是 y y y方向,即每次迭代是在 y y y方向上前进一步,在 x x x方向上判断中点和直线关系。
具体推导过程不再说明,以下图为标准,假设起点为 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),终点在圆上时的初始判别式 d d d和判别式 d > 0 d>0 d>0、 d < 0 d<0 d<0时迭代情况。
特别的,当d = 0
时,应归属于只有最大位移方向移动的情况。
终点所在角度 | d | d>0 | d<0 | d = 0 d=0 d=0 |
---|---|---|---|---|
( 0 ° , 45 ° ] (0°,45°] (0°,45°] | 2a+b | x++;d+=2a | x++;y++;d+=2(a+b) | 归于 d > 0 d>0 d>0 |
( 45 ° , 90 ° ) (45°,90°) (45°,90°) | a+2b | y++;x++;d+=2(a+b) | y++;d+=2b | 归于 d < 0 d<0 d<0 |
( 90 ° , 135 ° ] (90°,135°] (90°,135°] | -a+2b | y++;x--;d+=2(b-a) | y++;d+=2b | 归于 d < 0 d<0 d<0 |
( 135 ° , 180 ° ) (135°,180°) (135°,180°) | -2a+b | x--;d-=2a | x--;y++;d+=2(b-a) | 归于 d > 0 d>0 d>0 |
( 180 ° , 225 ° ] (180°,225°] (180°,225°] | -2a-b | x--;y--;d-=2(a+b) | x--;d-=2a | 归于 d < 0 d<0 d<0 |
( 225 ° , 270 ° ) (225°,270°) (225°,270°) | -a-2b | y--;d-=2b | y--;x--;d-=2(a+b) | 归于 d > 0 d>0 d>0 |
( 270 ° , 315 ° ] (270°,315°] (270°,315°] | a-2b | y--;d-=2b | y--;x==;d+=2(a-b) | 归于 d > 0 d>0 d>0 |
( 315 ° , 360 ° ) (315°,360°) (315°,360°) | 2a-b | x++;y--;d+=2(a-b) | x++;d+=2a | 归于 d < 0 d < 0 d<0 |
备注:
-
水平直线和竖直直线易得,这里不再说明。
-
注意初始化时要令 b ≥ 0 b \geq 0 b≥0
int a = y1-y2; int b = x2-x1; if (b < 0){ a = -a; b = -b }
-
注意分清是
-=
还是+=
-
八个部分可以通过横、纵行进方向的正负号串起来,具体不再赘述,希望读者自己思考(嘻嘻🤭)
中点画线法代码(C语言)
#include <stdio.h>
#include <stdlib.h>
void vline(int x, int y1, int y2, FILE* f){
/*竖直的线*/
int s = y1 > y2 ? -1 : 1; // 迭代方向
for (int i = y1; i != y2; i += s) {
fprintf(f, "(%d,%d)\n", x, i);
}
fprintf(f, "(%d,%d)\n", x, y2);
}
void hline(int x1, int x2, int y, FILE* f){
/*水平的线*/
int s = x1 > x2 ? -1 : 1; // 迭代方向
for (int i = x1; i != x2; i += s){
fprintf(f, "(%d,%d)\n", i, y);
}
fprintf(f, "(%d,%d)\n", x2, y);
}
void midPointLine (int x1, int y1, int x2, int y2, FILE* f) {
/* 使用中点画线算法绘制起点为(x1,y1),终点为(x2,y2)的直线 */
if (x1 == x2) { // 直线为竖直线
vline(x1, y1, y2, f);
return;
}
if (y1 == y2) { // 直线为水平线
hline(x1, x2, y1, f);
return;
}
int a, b; // 所画直线的参数
a = y1 - y2;
b = x2 - x1;
if (b < 0){
a = -a;
b = -b;
}
int dx, dy; // 起终点之间的横、纵距离
dx = (b > 0) ? b : -b;
dy = (a > 0) ? a : -a;
int sx, sy; // 起点到终点的迭代方向
sx = (x1 > x2) ? -1 : 1;
sy = (y1 > y2) ? -1 : 1;
int x, y, d; // 迭代参数
x = x1;
y = y1;
d = (dx >= dy) ? 2 * sx * a + sy * b: sx * a + 2 * sy * b;
fprintf(f, "(%d,%d)\n", x1, y1);
// 中点画线法
if (dx >= dy){ // 直线斜率 0 < |k| <= 1 的情况
if (sy > 0){ // 终点位于起点的上面
while(x != x2){
if (d >= 0){
x += sx; d += 2 * sx * a;
fprintf(f, "(%d,%d)\n", x, y);
} else {
x += sx; y += sy; d += 2 * (sx * a + sy * b);
fprintf(f, "(%d,%d)\n", x, y);
}
} return;
} else { // 终点位于起点的下面
while(x != x2){
if (d > 0){
x += sx; y += sy; d += 2 * (sx * a + sy * b);
fprintf(f, "(%d,%d)\n", x, y);
} else {
x += sx; d += 2 * sx * a;
fprintf(f, "(%d,%d)\n", x, y);
}
} return;
}
} else { // 直线斜率 1 < |k| 的情况
if (sy > 0){ // 终点位于起点的上面
while(y != y2){
if (d > 0){
y += sy; x += sx; d += 2 * (sx * a + sy * b);
fprintf(f, "(%d,%d)\n", x, y);
} else {
y += sy; d += 2 * sy * b;
fprintf(f, "(%d,%d)\n", x, y);
}
} return;
} else { // 终点位于起点的下面
while(y != y2){
if (d >= 0){
y += sy; d += 2 * sy * b;
fprintf(f, "(%d,%d)\n", x, y);
} else {
y += sy; x += sx; d += 2 * (sx * a + sy * b);
fprintf(f, "(%d,%d)\n", x, y);
}
} return;
}
}
}
int main()
{
int x1,y1,x2,y2;
printf("请输入点(x1 y1): ");
scanf("%d %d", &x1, &y1);
printf("请输入点(x2 y2): ");
scanf("%d %d", &x2, &y2);
FILE* f;
f = fopen("midPointLineOutput.txt","w");
if (f == NULL) {
printf("无法建立输出文件,程序错误。");
return 0;
}
midPointLine(x1, y1, x2, y2, f);
fclose(f);
return 0;
}