DDA直线绘制算法
DDA算法即数值微分法,是根据直线的微分方程来计算 Δ x \Delta x Δx或 Δ y \Delta y Δy生成直线的扫描转化算法。
设过端点
P
0
(
x
0
,
y
0
)
P_0(x_0, y_0)
P0(x0,y0)、
P
1
(
x
1
,
y
1
)
P_1(x_1, y_1)
P1(x1,y1)的直线为 L ,则直线段 L 的斜率为:
k
=
y
1
−
y
0
x
1
−
x
0
k=\frac{y_1-y_0}{x_1-x_0}
k=x1−x0y1−y0
直线的微分方程为:
y
1
−
y
0
x
1
−
x
0
=
Δ
y
Δ
x
=
k
\frac{y_1-y_0}{x_1-x_0} = \frac{\Delta y}{\Delta x} = k
x1−x0y1−y0=ΔxΔy=k
要在显示器显示直线 L ,必须确定最佳逼近直线 L 的像素集合。我们采用步进的方式来获得构成直线的顶点,如下图所示:
注:图片来源于:黄静.计算机图形学及其实践教程[M].北京:机械工业出版社,2015.5:39
即当 ∣ k ∣ ≤ 1 |k| \leq 1 ∣k∣≤1时,直线横向距离大于纵向距离,当 Δ x \Delta x Δx每增加1, Δ y \Delta y Δy增加k(即 d y d x \frac{dy}{dx} dxdy);当 ∣ k ∣ ≥ 1 |k| \geq 1 ∣k∣≥1时,直线纵向距离大于横向距离,当 Δ y \Delta y Δy每增加1, Δ x \Delta x Δx增加 1 k \frac{1}{k} k1(即 d x d y \frac{dx}{dy} dydx)。这样可以得到尽可能多的顶点来构造直线。下面是DDA算法的具体实现:
def gl_draw_line_by_dda(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
mmx = x1
mmy = y1
if abs(dx) > abs(dy):
steps = abs(dx)
else:
steps = abs(dy)
xincrement = float(dx/steps)
yincrement = float(dy/steps)
glBegin(GL_POINTS)
for k in range(steps):
glVertex2f(round(mmx)*0.005, round(mmy)*0.005)
mmx += xincrement
mmy += yincrement
glVertex2f 函数用于绘制顶点,具体请看下文 OpenGL初步 部分。
Bresenham画线法
以
0
≤
k
≤
1
0 \leq k \leq 1
0≤k≤1 为例,假设直线 A交下一列扫描线于M(如下图),Bresenham算法计算
M
P
2
MP_2
MP2即
Δ
i
\Delta_i
Δi的大小。如果
Δ
i
≥
P
1
P
2
2
\Delta_i \geq \frac{P_1P_2}{2}
Δi≥2P1P2,即
Δ
i
≥
0.5
\Delta_i \geq 0.5
Δi≥0.5,则交点M点更靠近上面一个点,选择
P
1
P_1
P1作为直线的下一个顶点,此时
P
1
P_1
P1点的横纵坐标均在前一点的横纵坐标的基础上增1;反之,选择
P
2
P_2
P2作为直线的下一个顶点,此时
P
2
P_2
P2的横坐标在前一点横纵标的基础上增1,纵坐标不变。
注:图片来源于:黄静.计算机图形学及其实践教程[M].北京:机械工业出版社,2015.5:40
下面进行算法公式的推演(以上图的直线AB为例):
初始值:A点的坐标
A
(
x
1
,
y
1
)
A(x_1,y_1)
A(x1,y1), B点的坐标
B
(
x
2
,
y
2
)
B(x_2,y_2)
B(x2,y2),用
x
0
x_0
x0、
y
0
y_0
y0来表示正要绘制的顶点的横纵坐标,用
w
w
w作选择哪一个顶点的判别式。
x
0
=
x
1
,
y
0
=
y
1
d
x
=
x
2
−
x
1
d
y
=
y
2
−
y
1
k
=
d
y
d
x
w
=
k
=
d
y
d
x
x_0=x_1,y_0=y_1\\ dx=x_2-x_1 \\ dy=y_2-y_1 \\ k=\frac{dy}{dx}\\ w=k=\frac{dy}{dx}
x0=x1,y0=y1dx=x2−x1dy=y2−y1k=dxdyw=k=dxdy
当遍历下一点时
x
0
=
x
0
+
1
x_0=x_0+1 \\
x0=x0+1
如果
w
>
0.5
w > 0.5
w>0.5,则上面一个顶点更接近真实直线上的顶点:
y
0
=
y
0
+
1
w
=
w
+
k
−
1
=
w
+
d
y
d
x
−
1
y_0=y_0+1 \\ w=w+k-1=w+\frac{dy}{dx}-1
y0=y0+1w=w+k−1=w+dxdy−1
因为此时对于下一个顶点来说,它的上下顶点的中点纵坐标增加了1,因此
w
w
w要减1。
如果
w
≤
0.5
w \leq 0.5
w≤0.5,则下面一个顶点更接近真实直线上的顶点,
y
0
y_0
y0保持不变,因此不对
y
0
y_0
y0做任何操作:
w
=
w
+
d
y
d
x
w=w+\frac{dy}{dx}
w=w+dxdy
循环整个遍历过程,直到 x 0 = = x 2 x_0 == x_2 x0==x2 ,整个绘制过程结束。
以下是示例代码:
'''此段代码仅为方便理解所作,可以将其视为伪代码'''
def gl_draw_line_by_bresenham(x1, y1, x2, y2):
x0 = x1
y0 = y1
dx = x2 - x1
dy = y2 - y1
k = dy/dx
w = k
glPointSize(1)
glBegin(GL_POINTS)
while x0 <= x2:
glVertex2f(x0 * 0.005, y0 * 0.005)
x0 += 1
if w > 0.5:
y0 += 1 # 选择上面一个顶点
w = w + k - 1
else:
w = w + k # 选择下面一个顶点
为了避免浮点运算带来的运算速度的降低,以及增加判断的速度,将判别式作了优化:
原判别式:
w
>
0.5
w > 0.5
w>0.5
根据不等式的性质有:
(
w
−
0.5
)
∗
2
d
x
>
0
(w-0.5)*2dx > 0
(w−0.5)∗2dx>0
乘以
2
d
x
2dx
2dx 是为了避免计算斜率,因此,改进的Bresenham直线绘制算法的流程如下:
初始值:A点的坐标
A
(
x
1
,
y
1
)
A(x_1,y_1)
A(x1,y1) , B点的坐标
B
(
x
2
,
y
2
)
B(x_2,y_2)
B(x2,y2) ,用
x
0
x_0
x0、
y
0
y_0
y0来表示正要绘制的顶点的横纵坐标,用
w
w
w 作选择哪一个顶点的判别式。
x
0
=
x
1
,
y
0
=
y
1
d
x
=
x
2
−
x
1
d
y
=
y
2
−
y
1
w
=
(
d
y
d
x
−
0.5
)
∗
2
d
x
=
2
d
y
−
d
x
x_0=x_1,y_0=y_1\\ dx=x_2-x_1 \\ dy=y_2-y_1 \\ w=(\frac{dy}{dx} - 0.5)*2dx = 2dy - dx
x0=x1,y0=y1dx=x2−x1dy=y2−y1w=(dxdy−0.5)∗2dx=2dy−dx
当遍历下一点时
x
0
=
x
0
+
1
x_0=x_0+1 \\
x0=x0+1
如果
w
>
0
w > 0
w>0,则上面一个顶点更接近真实直线上的顶点:
y
0
=
y
0
+
1
w
=
w
+
(
k
−
1
)
∗
2
d
x
=
w
+
2
d
y
−
2
d
x
y_0=y_0+1 \\ w=w+(k-1)*2dx=w+2dy-2dx
y0=y0+1w=w+(k−1)∗2dx=w+2dy−2dx
如果
w
≤
0.5
w \leq 0.5
w≤0.5:
w
=
w
+
2
d
y
w=w+2dy
w=w+2dy
循环整个遍历过程,直到 x 0 = = x 2 x_0 == x_2 x0==x2 ,整个绘制过程结束。
其他斜率的情况类似,下面是Bresenham画线法的代码:
# Bresenham画线法
def gl_draw_line_by_bresenham(x1, y1, x2, y2):
"""
这个函数用于绘制给定两个端点的线段
:param x1: 第一个点横坐标
:param y1: 第一个点纵坐标
:param x2: 第二个点横坐标
:param y2: 第二个点纵坐标
:return: None
"""
if x2 < x1:
temp1 = x1
x1 = x2
x2 = temp1
temp1 = y1
y1 = y2
y2 = temp1
glPointSize(1)
glBegin(GL_POINTS)
dx = abs(x2 - x1)
dy = abs(y2 - y1)
x0 = x1
y0 = y1
# 当 k<0 时
if y1 <= y2:
# 当 k<1 时
if dy <= dx:
w = 2 * dy - dx
while x0 < x2:
glVertex2f(x0 * 0.005, y0 * 0.005)
x0 += 1
if w > 0:
y0 += 1
w = w + 2 * dy - 2 * dx
else:
w = w + 2 * dy
else:
w = 2 * dx - dy
while y0 < y2:
glVertex2f(x0 * 0.005, y0 * 0.005)
y0 += 1
if w > 0:
x0 += 1
w = w + 2 * dx - 2 * dy
else:
w = w + 2 * dx
else:
# 当 k<1 时
if dy <= dx:
w = 2 * dy - dx
while x0 < x2:
glVertex2f(x0 * 0.005, y0 * 0.005)
x0 += 1
if w > 0:
y0 -= 1
w = w + 2 * dy - 2 * dx
else:
w = w + 2 * dy
else:
w = 2 * dx - dy
while y0 > y2:
glVertex2f(x0 * 0.005, y0 * 0.005)
y0 -= 1
if w > 0:
x0 += 1
w = w + 2 * dx - 2 * dy
else:
w = w + 2 * dx
中点画线法
中点画线法的思路和Bresenham画线法十分类似。假定直线的斜率
0
<
k
<
1
0 < k < 1
0<k<1 ,且已确定当前处理过的像素点
P
(
x
p
,
y
p
)
P(x_p,y_p)
P(xp,yp) ,则下一个与直线最接近的像素只能是
P
1
P_1
P1 或
P
2
P_2
P2。设
M
M
M 为
P
1
P_1
P1 和
P
2
P_2
P2 的中点,
Q
Q
Q 为交点,如下图所示:
注:图片来源于:徐文鹏.计算机图形学基础(OpenGL版)[M].北京:清华大学出版社,2016.6:48
中点画线法判断 Q Q Q 点和 M M M 点的位置关系,如果 Q Q Q 点在 M M M 点的上方,则 Q Q Q 点离 P 2 P_2 P2 点较近,下一个顶点选择 P 2 P_2 P2 点;如果 Q Q Q 点在 M M M 点的下方,则 Q Q Q 点离 P 1 P_1 P1 点较近,下一个顶点选择 P 1 P_1 P1 点。
设起点A点的坐标
A
(
x
1
,
y
1
)
A(x_1,y_1)
A(x1,y1) , 终点B点的坐标
B
(
x
2
,
y
2
)
B(x_2,y_2)
B(x2,y2)
则直线的方程为:
y
−
y
1
x
−
x
1
=
y
2
−
y
1
x
2
−
x
1
\frac{y-y_1}{x-x_1} = \frac{y_2-y_1}{x_2-x_1}
x−x1y−y1=x2−x1y2−y1
化为一般式有:
(
y
1
−
y
2
)
∗
x
+
(
x
2
−
x
1
)
∗
y
+
x
1
y
2
−
x
2
y
1
=
0
(y_1-y_2)*x+(x_2-x_1)*y+x_1y_2-x_2y_1=0
(y1−y2)∗x+(x2−x1)∗y+x1y2−x2y1=0
令:
a
=
y
1
−
y
2
b
=
x
2
−
x
1
c
=
x
1
y
2
−
x
2
y
1
a=y_1-y_2\\ b=x_2-x_1\\ c=x_1y_2-x_2y_1
a=y1−y2b=x2−x1c=x1y2−x2y1
则直线方程为:
F
(
x
,
y
)
=
a
x
+
b
y
+
c
=
0
F(x,y)=ax+by+c=0
F(x,y)=ax+by+c=0
由数学知识可知有下面的关系:
(
1
)
F
(
x
,
y
)
=
0
(1) F(x,y)=0
(1)F(x,y)=0,点在直线上;
(
2
)
F
(
x
,
y
)
>
0
(2) F(x,y)>0
(2)F(x,y)>0,点在直线上方;
(
1
)
F
(
x
,
y
)
<
0
(1) F(x,y)<0
(1)F(x,y)<0,点在直线下方。
所以,如果需要判断 M M M点在 Q Q Q点的上方还是下方,只需要把中点 M M M代入 F ( x , y ) F(x,y) F(x,y),并检查它的符号即可。
构造判别式:
d
=
F
(
M
)
=
F
(
x
p
+
1
,
y
p
+
0.5
)
=
a
(
x
p
+
1
)
+
b
(
y
p
+
0.5
)
+
c
\begin{align*} d&=F(M)\\ &=F(x_p+1,y_p+0.5)\\ &=a(x_p+1)+b(y_p+0.5)+c \end{align*}
d=F(M)=F(xp+1,yp+0.5)=a(xp+1)+b(yp+0.5)+c
画线从点
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1) 开始,因此判别式的初始值为:
d
=
F
(
x
1
+
1
,
y
1
+
0.5
)
=
a
(
x
0
+
1
)
+
b
(
y
0
+
0.5
)
+
c
=
a
x
0
+
b
y
0
+
c
+
a
+
0.5
b
=
F
(
x
1
,
y
1
)
+
a
+
0.5
b
=
0
+
a
+
0.5
b
=
a
+
0.5
b
\begin{align*} d&=F(x_1+1,y_1+0.5)\\ &=a(x_0+1)+b(y_0+0.5)+c\\ &=ax_0+by_0+c+a+0.5b\\ &=F(x_1,y_1)+a+0.5b\\ &=0+a+0.5b\\ &=a+0.5b \end{align*}
d=F(x1+1,y1+0.5)=a(x0+1)+b(y0+0.5)+c=ax0+by0+c+a+0.5b=F(x1,y1)+a+0.5b=0+a+0.5b=a+0.5b
当
d
<
0
d<0
d<0 时,
M
M
M在
Q
Q
Q的下方,取上面一个顶点
P
2
P_2
P2,此时下一个中点的坐标为
(
x
p
+
2
,
y
p
+
1.5
)
(x_p+2,y_p+1.5)
(xp+2,yp+1.5),新的判别式为:
d
=
F
(
x
p
+
2
,
y
p
+
1.5
)
=
a
(
x
p
+
2
)
+
b
(
y
p
+
1.5
)
+
c
=
a
(
x
p
+
1
)
+
b
(
y
p
+
0.5
)
+
c
+
a
+
b
=
d
+
a
+
b
\begin{align*} d &= F(x_p+2,y_p+1.5)\\ &=a(x_p+2)+b(y_p+1.5)+c\\ &=a(x_p+1)+b(y_p+0.5)+c+a+b\\ &=d+a+b \end{align*}
d=F(xp+2,yp+1.5)=a(xp+2)+b(yp+1.5)+c=a(xp+1)+b(yp+0.5)+c+a+b=d+a+b
当
d
≥
0
d\geq0
d≥0 时,
M
M
M在
Q
Q
Q的上方,取下面一个顶点
P
1
P_1
P1,此时下一个中点的坐标为
(
x
p
+
2
,
y
p
+
0.5
)
(x_p+2,y_p+0.5)
(xp+2,yp+0.5),新的判别式为。
d
=
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
=
d
+
a
\begin{align*} d=&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\\ &=d+a \end{align*}
d=F(xp+2,yp+0.5)=a(xp+2)+b(yp+0.5)+c=a(xp+1)+b(yp+0.5)+c+a=d+a
然后循环这个判别选择的过程,直到所有点都绘制完毕。
将推理过程略去,设起点A点的坐标
A
(
x
1
,
y
1
)
A(x_1,y_1)
A(x1,y1) , 终点B点的坐标
B
(
x
2
,
y
2
)
B(x_2,y_2)
B(x2,y2),
(
x
0
,
y
0
)
(x_0,y_0)
(x0,y0) 表示正要绘制的顶点,整个算法流程为:
a
=
y
1
−
y
2
b
=
x
2
−
x
1
d
=
a
+
0.5
b
x
=
x
1
,
y
=
y
1
a=y_1-y_2\\ b=x_2-x_1\\ d=a+0.5b\\ x=x_1,y=y_1
a=y1−y2b=x2−x1d=a+0.5bx=x1,y=y1
当
d
<
0
d<0
d<0 时,
M
M
M在
Q
Q
Q的下方,取上面一个顶点
P
2
P_2
P2:
x
=
x
+
1
y
=
y
+
1
d
=
d
+
a
+
b
x=x+1\\ y=y+1\\ d=d+a+b
x=x+1y=y+1d=d+a+b
当
d
≥
0
d\geq0
d≥0 时,
M
M
M在
Q
Q
Q的上方,取下面一个顶点
P
1
P_1
P1:
x
=
x
+
1
d
=
d
+
a
x=x+1\\ d=d+a
x=x+1d=d+a
当
x
=
=
x
2
x==x_2
x==x2 时,绘制结束。
其他斜率情况的推导过程类似,下面是中点画线法的完整代码:
# 中点画线法
def gl_draw_line(x1, y1, x2, y2):
"""
这个函数用于绘制给定两个端点的线段
:param x1: 第一个点横坐标
:param y1: 第一个点纵坐标
:param x2: 第二个点横坐标
:param y2: 第二个点纵坐标
:return: None
"""
glBegin(GL_POINTS)
tag = 0
if abs(x2 - x1) < abs(y2 - y1):
temp = x1
x1 = y1
y1 = temp
temp = x2
x2 = y2
y2 = temp
tag = 1
if x1 > x2:
temp = x1
x1 = x2
x2 = temp
temp = y1
y1 = y2
y2 = temp
a = y1 - y2
b = x2 - x1
d = a+b/2
if y1 < y2:
x = x1
y = y1
glVertex2f(x*0.005, y*0.005)
while x < x2:
if d < 0:
x += 1
y += 1
d = d + a + b
else:
x += 1
d += a
if tag == 1:
glVertex2f(y*0.005, x*0.005)
else:
glVertex2f(x*0.005, y*0.005)
else:
x = x2
y = y2
glVertex2f(x*0.005, y*0.005)
while x > x1:
if d < 0:
x -= 1
y += 1
d = d - a + b
else:
x -= 1
d -= a
if tag == 1:
glVertex2f(y * 0.005, x * 0.005)
else:
glVertex2f(x * 0.005, y * 0.005)
OpenGL初步
这里主要简单介绍一下python环境下的OpenGL的一些常见的函数及其主要的用途。
main函数部分。
glutInit() # 对GLUT初始化
对GLUT初始化,写在主函数调用OpenGL之前。
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA) # 设置显示方式
设置OpenGL的显示方式,前面一个参数为缓存的方式,主要有单缓存和双缓存两种,单缓存的参数为 GLUT_SINGLE ,当程序不需要表现实时状态的显示时就可以用它,而当要实时显示代绘制图像的状态时就需要使用双缓存,双缓存的参数为 GLUT_DOUBLE 。本文采用双缓存实时将代绘制图像显示出来方便绘制者观察。后面一个参数为显示的颜色,GLUT_RGBA 表示带有alpha通道(即透明通道)的颜色模式, GLUT_RGB 表示不带alpha通道的颜色模式, GLUT_INDEX 表示颜色索引模式,其他的参数详情请查阅官方文档。
glutInitWindowSize(windowsizex, windowsizey) # 设置窗口大小
设置窗口大小,第一个参数是窗口的宽度,第二个参数是窗口的高度,它们的单位都是像素。
text = glutCreateWindow("MYTEXT") # 创建窗口
创建窗口,窗口被创建后,需要调用glutMainLoop()才能看到,参数为所创建的窗口的名称。
glutDisplayFunc(mydisplay) # 调用函数绘制图像,参数即为所调用的绘制函数的函数名称
调用函数绘制图像,参数即为所调用的绘制函数的函数名称。
glutMouseFunc(mymouse) # 获取鼠标点击事件,参数即为响应鼠标点击事件的函数
获取鼠标点击事件,参数即为响应鼠标点击事件的函数名称。
glutPassiveMotionFunc(mymousemotion) # 鼠标按钮松开时移动相应函数
鼠标按钮松开时移动相应函数,参数即为该函数名称。
glutMainLoop()
用于启动 GLUT 的事件处理循环,直到程序显式地退出,窗口被创建后,需要调用glutMainLoop()才能看到。
main函数部分的代码:
if __name__ == "__main__":
glutInit() # 对GLUT初始化
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA) # 设置显示方式
glutInitWindowSize(windowsizex, windowsizey) # 设置窗口大小
text = glutCreateWindow("MYTEXT") # 创建窗口,窗口被创建后,需要调用glutMainLoop()才能看到,参数为所创建的窗口的名称
glutDisplayFunc(mydisplay) # 调用函数绘制图像,参数即为所调用的绘制函数的函数名称
glutMouseFunc(mymouse) # 获取鼠标点击事件,参数即为响应鼠标点击事件的函数
glutPassiveMotionFunc(mymousemotion) # 鼠标按钮松开时移动相应函数
glutMainLoop() # 事件处理循环
glClearColor(0.0, 0.0, 0.0, 0.0) # 将清空颜色为黑色
glClear(GL_COLOR_BUFFER_BIT) # 将窗口的背景设置为当前颜色
绘制图像部分
在绘制图像的函数mydisplay中,会用到几个函数:
glClear(GL_COLOR_BUFFER_BIT)
用于绘制函数的开头,用于清空当前的图像缓存,之后的代码便是绘制新图像的代码
glutSwapBuffers() # 双缓存的刷新模式
glFlush() # 单缓存的刷新模式
用于绘制函数的结尾,用于将已经绘制好的图像刷新到窗口里去,当是 GLUT_DOUBLE 模式用上面一个函数, GLUT_SINGLE 模式用下面一个函数。
glBegin(GL_POINTS)
glEnd()
用于绘制函数的中间,表示开始绘制顶点,glBegin里的参数不同,表示在这中间绘制的图形也就不同,由于我们这里主要实现的是绘制具体的算法,所以只需要 GL_POINTS, 直线、多边形等其他图形的绘制方法请参考官方文档。
glVertex2f(x, y)
绘制一个顶点,其参数是两个浮点数 x 和 y,它们分别表示顶点在二维空间中的 x 和 y 坐标。
glPointSize(1)
表示绘制图像时所用的每一个点的大小,默认为1。
glutPostRedisplay() # 重画,相当于重新调用display
重画,相当于重新调用display绘制函数。
display部分的代码:
def mydisplay():
# print("-----")
glClear(GL_COLOR_BUFFER_BIT)
global is_end_draw
global is_cur_begin_draw
global lines_list
if len(lines_list) != 0:
for item in lines_list:
gl_draw_line_by_bresenham(item[0], item[1], item[2], item[3])
glEnd()
if is_end_draw == 1:
lines_list.append([(mx - int(windowsizex / 2)), -(my - int(windowsizey / 2)),
(ex - int(windowsizex / 2)), -(ey - int(windowsizey / 2))])
glutPostRedisplay()
else:
if is_cur_begin_draw == 1:
gl_draw_line_by_bresenham((mx - int(windowsizex / 2)), -(my - int(windowsizey / 2)),
(cur_ex - int(windowsizex / 2)), -(cur_ey - int(windowsizey / 2)))
glEnd()
glutSwapBuffers() # 双缓存的刷新模式
这里使用的是Bresenham直线绘制法,如果想用其他方法绘制,需要将gl_draw_line_by_bresenham 改为对应的函数。
鼠标点击事件部分
鼠标点击事件mymouse的参数有四个。
button 参数获取的是当前点击的是鼠标的哪一个键,当其等于 GLUT_LEFT_BUTTON 时,表示鼠标左键被按下;当其等于 GLUT_RIGHT_BUTTON 时,表示鼠标右键被按下;当其等于 GLUT_MIDDLE_BUTTON 时,表示鼠标中键被按下。
state 参数获取的是鼠标鼠标的状态,如果其等于 GLUT_DOWN 表示鼠标按钮被按下,如果其等于 GLUT_UP 表示鼠标按钮被释放。
mousex 和 mousey:这两个整数分别表示鼠标在窗口中的 x 和 y 坐标,在本文中,其坐标原点在窗口的左上角,x轴正方向向右,y轴正方向向下,和光栅扫描显示器的扫描线扫描方向一致。
这里我使用全局变量 mx 和 my 来存储所绘制直线的起点坐标,ex 和 ey 来存储所绘制直线的终点坐标。first_left_button_down 是用来判断当前点击是绘制的直线的起点还是终点,is_end_draw 是用来判断是否一条直线的绘制是否结束。
def mymouse(button, state, mousex, mousey):
global mx
global my
if button == GLUT_RIGHT_BUTTON and state == GLUT_DOWN:
print("right_button down")
print("x: ", mousex, " y: ", mousey)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glClearColor(0.0, 0.0, 0.0, 0.0) # 将清空颜色为黑色
if button == GLUT_LEFT_BUTTON:
if state == GLUT_DOWN:
global is_end_draw
global first_left_button_down
global ex
global ey
if first_left_button_down == 0:
mx = mousex
my = mousey
print("left_button down")
print("mx: ", mx, " my: ", my)
is_end_draw = 0
first_left_button_down = 1
glutPostRedisplay() # 重画,相当于重新调用display
elif first_left_button_down == 1:
is_end_draw = 1
first_left_button_down = 0
ex = mousex
ey = mousey
glutPostRedisplay() # 重画,相当于重新调用display
鼠标移动事件部分
鼠标移动事件部分传递两个参数,用来表示鼠标在窗口中的实时位置。
这里我用 cur_ex 和 cur_ey 两个全局变量来存储鼠标移动过程中的实时位置。is_cur_begin_draw 用于判断是否正在绘制直线。
def mymousemotion(x1, y1):
global is_end_draw
global cur_ex, cur_ey
global is_cur_begin_draw
if is_end_draw == 0:
cur_ex = x1
cur_ey = y1
is_cur_begin_draw = 1
glutPostRedisplay() # 重画,相当于重新调用display
else:
is_cur_begin_draw = 0
坐标位置的转化
在本文中,由于获取的鼠标位置时的参考系和绘制顶点时用到的参考系不同,所以要进行顶点坐标位置的转化。获取的鼠标位置时的参考系坐标原点在窗口的左上角,x轴正方向向右,y轴正方向向下,单位长度是像素;绘制顶点时用到的参考系坐标原点在窗口的正中央,x轴正方向向右,y轴正方向向上,横纵坐标最大长度为1,表示窗口的边缘。
在mydisplay中的坐标位置转化:
gl_draw_line_by_bresenham((mx - int(windowsizex / 2)), -(my - int(windowsizey / 2)),
(cur_ex - int(windowsizex / 2)), -(cur_ey - int(windowsizey / 2)))
这里的坐标长度还是像素,我在绘制直线的代码中才将其标准化:
glVertex2f(x0 * 0.005, y0 * 0.005)
这里乘以0.005是因为我定义的窗口长宽均为400,那么按x、y的最大值为200,要变为1就要乘以1/200。
效果展示
完整代码用 lines_list 来存储已经画好了的线,并使用控制变量来控制绘制直线的状态向量机,使用双缓存进行刷新,因此可以看到实时的绘制效果,也能够绘制多条直线,下面是实现的效果展示图:
完整代码
#!/usr/bin/python
# -*- coding: utf-8 -*-
from OpenGL.GL import *
# from OpenGL.GLU import *
from OpenGL.GLUT import *
# import math
mx = 200
my = 200
ex = 200
ey = 200
cur_ex = 200
cur_ey = 200
windowsizex = 400
windowsizey = 400
is_cur_begin_draw = 0 # 判断实时绘画线段是否开始绘制
is_begin_draw = 0 # 判断是否开始绘画
is_end_draw = -1 # 判断是否停止绘画
first_left_button_down = 0
lines_list = [] # 保存已经画好了的线
# DDA画线法
def gl_draw_line_by_dda(x1, y1, x2, y2):
dx = x2 - x1
dy = y2 - y1
mmx = x1
mmy = y1
if abs(dx) > abs(dy):
steps = abs(dx)
else:
steps = abs(dy)
xincrement = float(dx/steps)
yincrement = float(dy/steps)
glBegin(GL_POINTS)
for k in range(steps):
glVertex2f(round(mmx)*0.005, round(mmy)*0.005)
mmx += xincrement
mmy += yincrement
# Bresenham画线法
def gl_draw_line_by_bresenham(x1, y1, x2, y2):
"""
这个函数用于绘制给定两个端点的线段
:param x1: 第一个点横坐标
:param y1: 第一个点纵坐标
:param x2: 第二个点横坐标
:param y2: 第二个点纵坐标
:return: None
"""
if x2 < x1:
temp1 = x1
x1 = x2
x2 = temp1
temp1 = y1
y1 = y2
y2 = temp1
glPointSize(1)
glBegin(GL_POINTS)
dx = abs(x2 - x1)
dy = abs(y2 - y1)
x0 = x1
y0 = y1
# 当 k<0 时
if y1 <= y2:
# 当 k<1 时
if dy <= dx:
w = 2 * dy - dx
while x0 < x2:
glVertex2f(x0 * 0.005, y0 * 0.005)
x0 += 1
if w > 0:
y0 += 1
w = w + 2 * dy - 2 * dx
else:
w = w + 2 * dy
else:
w = 2 * dx - dy
while y0 < y2:
glVertex2f(x0 * 0.005, y0 * 0.005)
y0 += 1
if w > 0:
x0 += 1
w = w + 2 * dx - 2 * dy
else:
w = w + 2 * dx
else:
# 当 k<1 时
if dy <= dx:
w = 2 * dy - dx
while x0 < x2:
glVertex2f(x0 * 0.005, y0 * 0.005)
x0 += 1
if w > 0:
y0 -= 1
w = w + 2 * dy - 2 * dx
else:
w = w + 2 * dy
else:
w = 2 * dx - dy
while y0 > y2:
glVertex2f(x0 * 0.005, y0 * 0.005)
y0 -= 1
if w > 0:
x0 += 1
w = w + 2 * dx - 2 * dy
else:
w = w + 2 * dx
# 中点画线法
def gl_draw_line(x1, y1, x2, y2):
"""
这个函数用于绘制给定两个端点的线段
:param x1: 第一个点横坐标
:param y1: 第一个点纵坐标
:param x2: 第二个点横坐标
:param y2: 第二个点纵坐标
:return: None
"""
glBegin(GL_POINTS)
tag = 0
if abs(x2 - x1) < abs(y2 - y1):
temp = x1
x1 = y1
y1 = temp
temp = x2
x2 = y2
y2 = temp
tag = 1
if x1 > x2:
temp = x1
x1 = x2
x2 = temp
temp = y1
y1 = y2
y2 = temp
a = y1 - y2
b = x2 - x1
d = a+b/2
if y1 < y2:
x = x1
y = y1
glVertex2f(x*0.005, y*0.005)
while x < x2:
if d < 0:
x += 1
y += 1
d = d + a + b
else:
x += 1
d += a
if tag == 1:
glVertex2f(y*0.005, x*0.005)
else:
glVertex2f(x*0.005, y*0.005)
else:
x = x2
y = y2
glVertex2f(x*0.005, y*0.005)
while x > x1:
if d < 0:
x -= 1
y += 1
d = d - a + b
else:
x -= 1
d -= a
if tag == 1:
glVertex2f(y * 0.005, x * 0.005)
else:
glVertex2f(x * 0.005, y * 0.005)
def draw_point(x1, y1):
glPointSize(1)
glBegin(GL_POINTS)
glVertex2f((x1 - int(windowsizex / 2)) * 0.005, -(y1 - int(windowsizey / 2)) * 0.005)
glEnd()
def mydisplay():
# print("-----")
glClear(GL_COLOR_BUFFER_BIT)
global is_end_draw
global is_cur_begin_draw
global lines_list
if len(lines_list) != 0:
for item in lines_list:
gl_draw_line_by_bresenham(item[0], item[1], item[2], item[3])
glEnd()
if is_end_draw == 1:
lines_list.append([(mx - int(windowsizex / 2)), -(my - int(windowsizey / 2)),
(ex - int(windowsizex / 2)), -(ey - int(windowsizey / 2))])
glutPostRedisplay()
else:
if is_cur_begin_draw == 1:
gl_draw_line_by_bresenham((mx - int(windowsizex / 2)), -(my - int(windowsizey / 2)),
(cur_ex - int(windowsizex / 2)), -(cur_ey - int(windowsizey / 2)))
glEnd()
glutSwapBuffers() # 双缓存的刷新模式
def mymouse(button, state, mousex, mousey):
global mx
global my
if button == GLUT_RIGHT_BUTTON and state == GLUT_DOWN:
print("right_button down")
print("x: ", mousex, " y: ", mousey)
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
glClearColor(0.0, 0.0, 0.0, 0.0) # 将清空颜色为黑色
if button == GLUT_LEFT_BUTTON:
if state == GLUT_DOWN:
global is_end_draw
global first_left_button_down
global ex
global ey
if first_left_button_down == 0:
mx = mousex
my = mousey
print("left_button down")
print("mx: ", mx, " my: ", my)
is_end_draw = 0
first_left_button_down = 1
glutPostRedisplay() # 重画,相当于重新调用display
elif first_left_button_down == 1:
is_end_draw = 1
first_left_button_down = 0
ex = mousex
ey = mousey
glutPostRedisplay() # 重画,相当于重新调用display
def mymousemotion(x1, y1):
global is_end_draw
global cur_ex, cur_ey
global is_cur_begin_draw
if is_end_draw == 0:
cur_ex = x1
cur_ey = y1
is_cur_begin_draw = 1
glutPostRedisplay() # 重画,相当于重新调用display
else:
is_cur_begin_draw = 0
if __name__ == "__main__":
glutInit() # 对GLUT初始化
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA) # 设置显示方式
glutInitWindowSize(windowsizex, windowsizey)
text = glutCreateWindow("MYTEXT") # 创建窗口,窗口被创建后,需要调用glutMainLoop()才能看到
glutDisplayFunc(mydisplay)
glutMouseFunc(mymouse)
glutPassiveMotionFunc(mymousemotion) # 鼠标按钮松开时移动相应函数
glutMainLoop()
glClearColor(0.0, 0.0, 0.0, 0.0) # 将清空颜色为黑色
glClear(GL_COLOR_BUFFER_BIT) # 将窗口的背景设置为当前颜色
结语
本文算法推演部分部分来自于参考文献的摘要,实现的代码未经优化,仅供学习与参考使用,如有错误,敬请谅解。
参考文献
[1] 黄静.计算机图形学及其实践教程[M].北京:机械工业出版社,2015.5:38-41
[2] 徐文鹏.计算机图形学基础(OpenGL版)[M].北京:清华大学出版社,2016.6:40-50