计算机图形学学习python实现1(绘制直线)(DDA、Bresenham画线法、中点画线法、OpenGL初步)(完整代码为鼠标实时绘制)

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=x1x0y1y0
直线的微分方程为:
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 x1x0y1y0=ΔxΔy=k
要在显示器显示直线 L ,必须确定最佳逼近直线 L 的像素集合。我们采用步进的方式来获得构成直线的顶点,如下图所示:

在这里插入图片描述
注:图片来源于:黄静.计算机图形学及其实践教程[M].北京:机械工业出版社,2015.5:39

即当 ∣ k ∣ ≤ 1 |k| \leq 1 k1时,直线横向距离大于纵向距离,当 Δ x \Delta x Δx每增加1, Δ y \Delta y Δy增加k(即 d y d x \frac{dy}{dx} dxdy);当 ∣ k ∣ ≥ 1 |k| \geq 1 k1时,直线纵向距离大于横向距离,当 Δ 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 0k1 为例,假设直线 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} Δi2P1P2,即 Δ i ≥ 0.5 \Delta_i \geq 0.5 Δi0.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=x2x1dy=y2y1k=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+k1=w+dxdy1
因为此时对于下一个顶点来说,它的上下顶点的中点纵坐标增加了1,因此 w w w要减1。

如果 w ≤ 0.5 w \leq 0.5 w0.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 (w0.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=x2x1dy=y2y1w=dxdy0.52dx=2dydx
当遍历下一点时
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+(k1)2dx=w+2dy2dx

如果 w ≤ 0.5 w \leq 0.5 w0.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} xx1yy1=x2x1y2y1
化为一般式有:
( 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 (y1y2)x+(x2x1)y+x1y2x2y1=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=y1y2b=x2x1c=x1y2x2y1
则直线方程为:
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 d0 时, 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=y1y2b=x2x1d=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 d0 时, 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 表示鼠标按钮被释放。

mousexmousey:这两个整数分别表示鼠标在窗口中的 x 和 y 坐标,在本文中,其坐标原点在窗口的左上角,x轴正方向向右,y轴正方向向下,和光栅扫描显示器的扫描线扫描方向一致。

这里我使用全局变量 mxmy 来存储所绘制直线的起点坐标,exey 来存储所绘制直线的终点坐标。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_excur_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

  • 27
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值