- Bresenham直线算法
- Bresenham画圆算法
- 中点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坐标有两种选择:y 或 y+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_0为2Δ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 -1sy = 1 if y0 < y1 else -1err = dx - dywhile True:# 使用 glBegin/glEnd 包裹点绘制glBegin(GL_POINTS)glVertex2i(x0, y0)glEnd()if x0 == x1 and y0 == y1:breake2 = 2 * errif e2 > -dy:err -= dyx0 += sxif e2 < dx:err += dxy0 += 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, 400, (0.0, 1.0, 0.0)) # 绿色,斜率中等draw_line_bresenham(250, 50, 250, 450, (0.0, 0.0, 1.0)) # 蓝色,垂直线glutSwapBuffers()
注意: 为了让像素坐标与窗口坐标对应,我们需要在init或reshape函数中设置正交投影,将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, rp = 1 - rplot_circle_points(cx, cy, x, y, color)while x < y:x += 1if p < 0:p += 2 * x + 1else:y -= 1p += 2 * (x - y) + 1plot_circle_points(cx, cy, x, y, color)
# 在 display 函数中调用
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,其中 a 和 b 分别是长轴和短轴半径。
中点画椭圆算法是画圆算法的直接推广。但椭圆的曲率是变化的,因此决策过程需要分为两个区域:
- 区域1: 从
(0, b)开始,此时椭圆的斜率|dy/dx| > 1。我们主要在y方向递减。 - 区域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。
在每个区域内,我们都使用中点决策来选择下一个像素,并维护一个增量计算的决策参数,其形式与画圆算法类似,但系数会因a和b的不同而变化。
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 += 1if p1 < 0:p1 += (2 * b * b * x) + (b * b)else:y -= 1p1 += (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 -= 1if p2 > 0:p2 -= (2 * a * a * y) + (a * a)else:x += 1p2 += (2 * b * b * x) - (2 * a * a * y) + (a * a)plot_ellipse_points(cx, cy, x, y, color)
# 在 display 函数中调用
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方向变化