PyOpenGL实现Bresenham算法

  1. Bresenham直线算法
  2. Bresenham画圆算法
  3. 中点Bresenham画椭圆算法

1. Bresenham直线生成算法

1.1 理论基础

绘制直线的最直观想法是使用直线方程 y = mx + b,并对x的每个整数值计算y
这涉及到大量的浮点数乘法和舍入运算,计算成本高昂,尤其是在早期的硬件上
Bresenham算法的核心思想是增量计算误差累积。它避免了浮点运算,仅通过整数加减法来决定下一个像素点的位置。
考虑一条从 (x0, y0)(x1, y1) 的直线,我们首先通过对称性将问题简化到第一象限且斜率 0 ≤ m ≤ 1 的情况
算法从起点 (x0, y0) 开始,每次向x方向前进一个像素(x = x + 1)。此时,下一个像素点的y坐标有两种选择:yy+1。决策依据是理想直线在 x+1 处的真实y坐标更接近哪个整数点。
设决策参数为 p_k。它定义为:
p_k = 2Δx * (y_{real} - y_k) - Δx
其中 Δx = x1 - x0, Δy = y1 - y0

  • 如果 p_k < 0,表示理想直线更靠近 y_k,因此选择像素 (x_k+1, y_k)
  • 如果 p_k ≥ 0,表示理想直线更靠近或穿过 y_k+1,因此选择像素 (x_k+1, y_k+1)
    关键在于,p_{k+1} 可以从 p_k 增量计算得到,避免了重复计算:
  • 若选择 y_k,则 p_{k+1} = p_k + 2Δy
  • 若选择 y_k+1,则 p_{k+1} = p_k + 2Δy - 2Δx
    初始决策参数 p_02Δy - Δx
    整个算法流程仅包含整数比较和加法,效率极高。

1.2 PyOpenGL实现

实现一个通用的Bresenham直线函数
GL_POINTS来绘制计算出的像素点。

def draw_line_bresenham(x0, y0, x1, y1, color):
    """通用的Bresenham直线算法,处理所有象限"""
    glColor3f(*color)
    dx = abs(x1 - x0)
    dy = abs(y1 - y0)
    sx = 1 if x0 < x1 else -1
    sy = 1 if y0 < y1 else -1
    err = dx - dy
    while True:
        # 使用 glBegin/glEnd 包裹点绘制
        glBegin(GL_POINTS)
        glVertex2i(x0, y0)
        glEnd()
        if x0 == x1 and y0 == y1:
            break
        
        e2 = 2 * err
        if e2 > -dy:
            err -= dy
            x0 += sx
        if e2 < dx:
            err += dx
            y0 += sy
# 在 display 函数中调用
def display():
    glClear(GL_COLOR_BUFFER_BIT)
    
    # 绘制三条不同斜率的直线
    draw_line_bresenham(50, 50, 450, 200, (1.0, 0.0, 0.0)) # 红色,斜率较小
    draw_line_bresenham(50, 250, 450, 600, (0.0, 1.0, 0.0)) # 绿色,斜率中等
    draw_line_bresenham(250, 50, 250, 450, (0.0, 0.0, 1.0)) # 蓝色,垂直线
    
    glutSwapBuffers()

注意: 为了让像素坐标与窗口坐标对应,我们需要在initreshape函数中设置正交投影,将OpenGL的坐标系从[-1, 1]映射到窗口的像素范围。

def reshape(width, height):
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity()
    # 设置投影范围为窗口大小,原点在左下角
    gluOrtho2D(0, width, 0, height)
    glMatrixMode(GL_MODELVIEW)

1.3 结果观察

运行程序,我们可以清晰地看到三条不同颜色的直线。通过观察,可以验证Bresenham算法在不同倾角下的表现:

  • 小斜率直线:像素点在x方向上连续分布,y方向上偶尔跳跃
  • 大斜率直线:通过算法的对称性处理,像素点在y方向上连续分布,x方向上偶尔跳跃
  • 垂直/水平线:算法退化为简单的轴向填充

2. Bresenham画圆算法:八分对称性的应用

2.1 理论基础

与直线类似,直接使用圆的方程 x² + y² = r² 同样涉及大量浮点运算。Bresenham画圆算法利用了圆的八分对称性。我们只需计算圆的八分之一(例如,从 (0, r)(r/√2, r/√2) 的第二象限八分圆),然后通过对称变换绘制出完整的圆。
算法同样基于中点决策。在 (x_k, y_k) 已经被选中的情况下,下一个候选像素点要么是 E(x_k+1, y_k),要么是 SE(x_k+1, y_k-1)
我们定义一个圆函数 F(x, y) = x² + y² - r²

  • F(x, y) < 0 表示点在圆内。
  • F(x, y) > 0 表示点在圆外。
    决策参数 p_k 是中点 M(x_k+1, y_k-0.5) 的函数值:
    p_k = F(x_k+1, y_k-0.5) = (x_k+1)² + (y_k-0.5)² - r²
  • 如果 p_k < 0,中点在圆内,选择像素 E,下一个中点为 (x_k+2, y_k-0.5)
  • 如果 p_k ≥ 0,中点在圆外或圆上,选择像素 SE,下一个中点为 (x_k+2, y_k-1.5)
    通过增量计算,p_{k+1} 也可由 p_k 推导:
  • 若选 E,则 p_{k+1} = p_k + 2x_k + 3
  • 若选 SE,则 p_{k+1} = p_k + 2x_k - 2y_k + 5
    初始决策参数 p_0 = F(1, r-0.5) = 1 + (r-0.5)² - r² = 5/4 - r。为避免浮点数,可取 p_0 = 1 - r

2.2 PyOpenGL实现

def plot_circle_points(cx, cy, x, y, color):
    """利用八分对称性绘制圆上的8个点"""
    glColor3f(*color)
    points = [
        (cx + x, cy + y), (cx - x, cy + y), (cx + x, cy - y), (cx - x, cy - y),
        (cx + y, cy + x), (cx - y, cy + x), (cx + y, cy - x), (cx - y, cy - x)
    ]
    glBegin(GL_POINTS)
    for px, py in points:
        glVertex2i(px, py)
    glEnd()
def draw_circle_bresenham(cx, cy, r, color):
    """Bresenham画圆算法"""
    x, y = 0, r
    p = 1 - r
    plot_circle_points(cx, cy, x, y, color)
    
    while x < y:
        x += 1
        if p < 0:
            p += 2 * x + 1
        else:
            y -= 1
            p += 2 * (x - y) + 1
        plot_circle_points(cx, cy, x, y, color)

def display():
    glClear(GL_COLOR_BUFFER_BIT)
    # 绘制一个圆
    draw_circle_bresenham(400, 300, 80, (1.0, 1.0, 0.0)) # 黄色圆
    
    glutSwapBuffers()

2.3 结果观察

屏幕上会出现一个由离散像素点构成的黄色圆形

3. 中点Bresenham画椭圆算法:圆的推广

3.1 理论基础

椭圆可以看作是圆在两个轴向上进行不等比缩放的结果。其标准方程为 (x/a)² + (y/b)² = 1,其中 ab 分别是长轴和短轴半径。
中点画椭圆算法是画圆算法的直接推广。但椭圆的曲率是变化的,因此决策过程需要分为两个区域:

  1. 区域1: 从 (0, b) 开始,此时椭圆的斜率 |dy/dx| > 1。我们主要在y方向递减。
  2. 区域2: 当斜率变为 |dy/dx| < 1 时,切换到区域2。我们主要在x方向递增。
    分界点发生在 dy/dx = -1 时。通过对椭圆方程隐函数求导可得:
    2x/a² + 2y(dy/dx)/b² = 0 => dy/dx = - (b²x) / (a²y)
    |dy/dx| = 1 时,分界点条件为 b²x = a²y
    在每个区域内,我们都使用中点决策来选择下一个像素,并维护一个增量计算的决策参数,其形式与画圆算法类似,但系数会因 ab 的不同而变化。

3.2 PyOpenGL实现

def plot_ellipse_points(cx, cy, x, y, color):
    """利用四分对称性绘制椭圆上的4个点"""
    glColor3f(*color)
    glBegin(GL_POINTS)
    glVertex2i(cx + x, cy + y)
    glVertex2i(cx - x, cy + y)
    glVertex2i(cx + x, cy - y)
    glVertex2i(cx - x, cy - y)
    glEnd()
def draw_ellipse_midpoint(cx, cy, a, b, color):
    """中点Bresenham画椭圆算法"""
    x, y = 0, b
    
    # 区域1的决策参数
    p1 = (b * b) - (a * a * b) + (0.25 * a * a)
    
    # 绘制初始点
    plot_ellipse_points(cx, cy, x, y, color)
    
    # --- 区域1 ---
    while (2 * b * b * x) < (2 * a * a * y):
        x += 1
        if p1 < 0:
            p1 += (2 * b * b * x) + (b * b)
        else:
            y -= 1
            p1 += (2 * b * b * x) - (2 * a * a * y) + (b * b)
        plot_ellipse_points(cx, cy, x, y, color)
        
    # --- 区域2 ---
    # 区域2的决策参数
    p2 = (b * b) * (x + 0.5) * (x + 0.5) + (a * a) * (y - 1) * (y - 1) - (a * a * b * b)
    
    while y > 0:
        y -= 1
        if p2 > 0:
            p2 -= (2 * a * a * y) + (a * a)
        else:
            x += 1
            p2 += (2 * b * b * x) - (2 * a * a * y) + (a * a)
        plot_ellipse_points(cx, cy, x, y, color)
def display():
    glClear(GL_COLOR_BUFFER_BIT)
    # 绘制一个椭圆
    draw_ellipse_midpoint(150, 350, 100, 60, (0.0, 1.0, 1.0)) # 青色椭圆
    
    glutSwapBuffers()

3.3 结果观察

屏幕上会出现一个青色的椭圆
可以清晰地看到,在长轴两端,像素点更密集地沿y方向变化;在短轴两端,则更密集地沿x方向变化

posted @ 2025-11-21 21:40  lumiere_cloud  阅读(24)  评论(0)    收藏  举报