PyOpenGL实现Bresenham算法
- 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 -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()
注意: 为了让像素坐标与窗口坐标对应,我们需要在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, 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,其中 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 += 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方向变化

浙公网安备 33010602011771号