ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

图形学实验一:任意斜率直线段绘制与DDA/Bresenham算法对比

图形学实验一:任意斜率直线段绘制与DDA/Bresenham算法对比

简介:这份资源是计算机图形学实验设计报告,面向高校计算机相关专业学生及图形学初学者,聚焦任意斜率直线段的绘制问题。报告围绕中点Bresenham扫描转换算法展开,通过设计CLine直线类,实现MoveTo()与LineTo()两个成员函数,重点解决斜率在[-1,0]区间内直线段的像素级绘制。压缩包内仅含1个doc文档,约60KB,完整收录实验目的、实验要求、类设计思路、核心代码实现及运行结果分析,代码部分涵盖Line.h头文件定义、Line.cpp成员函数实现以及OnDraw中调用绘制的完整流程。读者可从中获得可直接参考的实验报告框架、中点Bresenham算法的推导与判别条件、CLine类的封装方法,以及坐标映射与SetPixel绘点的具体实现细节,便于快速完成同类图形学实验并理解直线扫描转换的核心原理。目前已有411人学习下载,适合需要提交实验报告或夯实光栅化算法基础的学习者参考。

1. 图形学绘制任意斜率的直线段:为什么你的 DDA 在陡峭斜率下会画成虚线

很多同学做计算机图形学实验一,第一次在 CDC 上画任意斜率直线段时都会遇到一个诡异现象:斜率接近 1 或者大于 1 的时候,线看起来是断断续续的虚线,或者干脆只画了几个孤立的像素点。这不是 MFC 的锅,也不是 CDC 的锅,而是光栅化算法本身对斜率没有做对称处理。图形学里绘制任意斜率的直线段,核心要解决的就是把连续直线映射到离散像素网格时,如何保证每一步都落在正确的像素上,且不出现断裂、不重复、不偏移。这个实验通常出现在深圳大学、电子科技大学等高校的图形学课程第一次上机,要求用 CLine 类封装,基于 CDC 的 SetPixel 逐点绘制。适合正在做图形学实验一、需要理解 Bresenham 和 DDA 差异、并且想一次把任意斜率都跑通的人。

2. 从 DDA 到 Bresenham:任意斜率直线段的光栅化选型

2.1 DDA 的浮点累加为什么在陡峭斜率下翻车

DDA 的思路很直接:取 x 和 y 方向跨度较大的那个作为步进方向,然后按 1 个单位步进,另一个方向按斜率累加浮点数,最后四舍五入取整。听起来没问题,但问题出在浮点累加的误差累积上。

假设起点 (0,0),终点 (3,10),斜率 k = 10/3 ≈ 3.333。如果按 x 步进,x 每次加 1,y 每次加 3.333,取整后得到 y = 0, 3, 6, 10。像素点是 (0,0), (1,3), (2,6), (3,10)。中间 y 从 3 跳到 6,跳过了 4 和 5,视觉上就是虚线。这就是 DDA 在陡峭斜率下的典型翻车现场。

正确的做法是:当 |dy| > |dx| 时,应该以 y 为步进方向,x 按 1/k 累加。这样每一步 y 只增加 1,x 增加 0.3,取整后得到 (0,0), (0,1), (1,2), (1,3), (2,4), (2,5), (3,6), (3,7), (4,8), (4,9), (3,10) 附近的点,像素连续无断裂。

所以 DDA 的第一个选型要点:步进方向必须根据 |dx| 和 |dy| 的大小动态选择,不能固定按 x 步进。

def dda_line(x0, y0, x1, y1): dx = x1 - x0 dy = y1 - y0 steps = max(abs(dx), abs(dy)) # 步数取最大跨度 if steps == 0: return [(x0, y0)] x_inc = dx / steps # 每步 x 增量 y_inc = dy / steps # 每步 y 增量 points = [] x, y = x0, y0 for _ in range(steps + 1): points.append((round(x), round(y))) # 四舍五入取整 x += x_inc y += y_inc return points

这段代码的关键在steps = max(abs(dx), abs(dy)),它保证了无论斜率多陡,步进方向都是跨度大的那个轴。x_inc和y_inc是浮点数,每步累加后round取整。参数说明:x0, y0是起点整数坐标,x1, y1是终点整数坐标,返回像素点列表。注意round在 Python 3 是银行家舍入,实际图形学实验里建议用int(x + 0.5)来保证四舍五入一致性。

DDA 的优点是逻辑简单,缺点是每步都有浮点加法和取整,在早期硬件上效率低,而且浮点误差累积可能导致长线偏移。所以实际实验里,老师通常要求你实现 Bresenham,因为它是全整数运算。

2.2 Bresenham 全整数运算的推导与任意斜率适配

Bresenham 的核心思想是:用整数误差项来决定下一个像素是取当前方向还是取对角线方向。以 0 ≤ k ≤ 1 为例,x 每次加 1,y 是否加 1 取决于误差项 d 是否超过 0.5。

推导过程:假设当前像素是 (x, y),真实直线在 x+1 处的 y 值是 y + k。候选像素是 (x+1, y) 和 (x+1, y+1)。决策变量 d = y + k - (y + 0.5) = k - 0.5。如果 d < 0,取 (x+1, y);否则取 (x+1, y+1),并且 d 减 1。

为了避免浮点,两边同乘 2dx,令 D = 2dx * d = 2dy - dx。每次 x 加 1,D 加 2dy;如果 D > 0,y 加 1,D 减 2dx。

def bresenham_line(x0, y0, x1, y1): dx = abs(x1 - x0) dy = abs(y1 - y0) sx = 1 if x0 < x1 else -1 # x 步进方向 sy = 1 if y0 < y1 else -1 # y 步进方向 err = dx - dy # 初始误差项 points = [] while True: points.append((x0, y0)) if x0 == x1 and y0 == y1: break e2 = 2 * err if e2 > -dy: # 水平方向决策 err -= dy x0 += sx if e2 < dx: # 垂直方向决策 err += dx y0 += sy return points

这段代码是 Bresenham 的通用版本,能处理任意斜率和任意方向。dx和dy取绝对值,sx和sy记录步进正负。err = dx - dy是初始误差,e2 = 2 * err是临时变量。两个if分别判断是否在 x 和 y 方向步进。注意这里没有用round,全是整数运算,所以效率高且无误差累积。

参数说明:x0, y0起点,x1, y1终点,返回像素点列表。这个版本在斜率大于 1 时同样正确,因为e2的判断条件会自动让 y 方向步进更多。你可以用 (0,0) 到 (3,10) 测试,输出应该是连续的 11 个点,不会跳过任何 y。

选型理由:DDA 适合理解原理,Bresenham 适合实际实验和工程。图形学实验一通常要求两种都实现,然后对比像素点数量和连续性。如果你只交一个,交 Bresenham,因为老师更看重整数优化思维。

3. 在 MFC 的 CDC 上封装 CLine 类:从 SetPixel 到任意斜率绘制

3.1 CLine 类的接口设计与 CDC 绘制流程

图形学实验一通常要求在 MFC 框架下完成,用 CDC 的SetPixel逐点画线。直接在主窗口的OnDraw里写一堆循环也能跑,但实验报告要求封装成 CLine 类,方便复用和测试。

CLine 类的最小接口应该包含:起点、终点、颜色、绘制方法。绘制方法内部调用 Bresenham 或 DDA,然后对每个像素调用pDC->SetPixel(x, y, color)。

// CLine.h class CLine { public: CLine(int x0, int y0, int x1, int y1, COLORREF color); void Draw(CDC* pDC); // 根据算法选择绘制 void DrawBresenham(CDC* pDC); // Bresenham 绘制 void DrawDDA(CDC* pDC); // DDA 绘制 private: int m_x0, m_y0, m_x1, m_y1; COLORREF m_color; };
// CLine.cpp void CLine::DrawBresenham(CDC* pDC) { int x0 = m_x0, y0 = m_y0, x1 = m_x1, y1 = m_y1; int dx = abs(x1 - x0), dy = abs(y1 - y0); int sx = (x0 < x1) ? 1 : -1; int sy = (y0 < y1) ? 1 : -1; int err = dx - dy; while (true) { pDC->SetPixel(x0, y0, m_color); if (x0 == x1 && y0 == y1) break; int e2 = 2 * err; if (e2 > -dy) { err -= dy; x0 += sx; } if (e2 < dx) { err += dx; y0 += sy; } } }

这段代码把 Bresenham 移植到 C++,核心逻辑和 Python 版一致。pDC->SetPixel是 MFC 的像素绘制函数,参数是 x 坐标、y 坐标、颜色。注意 MFC 的坐标原点在左上角,y 轴向下,所以如果你从数学坐标系转换过来,需要做 y 翻转。实验里通常直接按屏幕坐标画,不用翻转。

参数说明:m_color是COLORREF类型,可以用RGB(r, g, b)构造。Draw方法里根据一个成员变量或参数选择调用DrawBresenham还是DrawDDA。建议在OnDraw里创建 CLine 对象数组,循环绘制不同斜率的线段,这样一次能看到所有情况。

3.2 任意斜率测试用例:从 0 到 无穷大的覆盖

实验报告里通常要求画几条不同斜率的线来验证算法。我一般会选这些斜率:0、0.5、1、2、无穷大(垂直线),以及负斜率和反向线段。

测试编号起点终点斜率验证重点
1(100, 200)(300, 200)0水平线是否连续
2(100, 200)(300, 300)0.5缓斜率是否每步 x 进 1
3(100, 200)(300, 400)1对角线是否 45 度
4(100, 200)(200, 400)2陡斜率是否每步 y 进 1
5(100, 200)(100, 400)无穷垂直线是否无断裂
6(300, 400)(100, 200)0.5反向线段是否对称

在OnDraw里可以这样写:

void CGraphicsView::OnDraw(CDC* pDC) { CLine line1(100, 200, 300, 200, RGB(255, 0, 0)); line1.Draw(pDC); CLine line2(100, 200, 300, 300, RGB(0, 255, 0)); line2.Draw(pDC); CLine line3(100, 200, 300, 400, RGB(0, 0, 255)); line3.Draw(pDC); CLine line4(100, 200, 200, 400, RGB(255, 255, 0)); line4.Draw(pDC); CLine line5(100, 200, 100, 400, RGB(255, 0, 255)); line5.Draw(pDC); CLine line6(300, 400, 100, 200, RGB(0, 255, 255)); line6.Draw(pDC); }

这段代码一次画出六条线,覆盖了水平、缓斜、45 度、陡斜、垂直和反向。运行后肉眼检查每条线是否连续、是否有断点、端点是否准确落在指定坐标。如果某条线出现虚线,说明算法在对应斜率下有问题,通常是步进方向判断错了。

注意:MFC 的SetPixel在OnDraw里调用是有效的,但如果你在OnPaint之外调用,需要确保CDC指针有效。实验里一般都在OnDraw里画,因为窗口重绘时会自动调用。

4. 避坑与排查:图形学实验一里最常见的 5 个翻车点

4.1 现象:陡峭斜率画出来是虚线,像素点不连续

原因:DDA 固定按 x 步进,当 |dy| > |dx| 时,y 方向跳跃超过 1 个像素,导致中间像素被跳过。Bresenham 如果只写了 0 ≤ k ≤ 1 的版本,同样会在陡斜率下断裂。

解决:DDA 用steps = max(abs(dx), abs(dy))动态选步进方向。Bresenham 用通用版本,err = dx - dy配合两个if判断,不要只写单方向版本。

4.2 现象:线段端点偏移,起点或终点少画一个像素

原因:循环条件写成了while (x0 != x1 || y0 != y1)然后在循环末尾步进,导致终点没画。或者for循环范围是0到steps-1,漏了最后一个点。

解决:Bresenham 里先SetPixel再判断是否到达终点,到达就break。DDA 里循环steps + 1次,确保起点和终点都包含。

4.3 现象:反向线段(从右到左、从下到上)画不出来或画错

原因:dx和dy没有取绝对值,sx和sy没有根据起点终点关系动态设置,导致步进方向反了。

解决:dx = abs(x1 - x0),dy = abs(y1 - y0),sx = (x0 < x1) ? 1 : -1,sy = (y0 < y1) ? 1 : -1。这样无论起点终点谁大谁小,都能正确步进。

4.4 现象:MFC 里画出来的线颜色不对或者看不见

原因:COLORREF构造时RGB参数顺序写反,或者背景色和线条色相同。另外SetPixel的坐标超出了客户区范围,画到窗口外面去了。

解决:RGB(r, g, b)顺序是红绿蓝,确认没有写反。检查坐标是否在CRect客户区内。可以在OnDraw里先pDC->FillSolidRect填充背景色,再画线。

4.5 现象:Bresenham 在斜率恰好为 1 时多画一个像素或重复画同一个点

原因:e2 > -dy和e2 < dx两个条件在dx == dy时可能同时成立,导致 x 和 y 同时步进,跳过了对角线上的中间点。或者两个条件都不成立,原地踏步。

解决:标准 Bresenham 在dx == dy时,err初始为 0,e2 = 0,e2 > -dy成立(因为dy > 0),e2 < dx也成立(因为dx > 0),所以 x 和 y 同时步进,这是正确的 45 度对角线。如果出现重复点,检查while循环里是否在步进后没有重新判断终点。

5. 进阶验证:用像素误差量化你的直线段质量

实验做完能跑通只是及格,真正想拿高分或者想确认自己的 Bresenham 实现是否最优,需要做量化验证。我一般会计算每个像素点到理想直线的垂直距离,然后统计最大误差和平均误差。

理想直线方程:对于起点 (x0, y0) 和终点 (x1, y1),直线方程可以写成A*x + B*y + C = 0,其中A = y0 - y1,B = x1 - x0,C = x0*y1 - x1*y0。点 (x, y) 到直线的距离是|A*x + B*y + C| / sqrt(A*A + B*B)。

import math def line_error(points, x0, y0, x1, y1): A = y0 - y1 B = x1 - x0 C = x0 * y1 - x1 * y0 denom = math.sqrt(A * A + B * B) errors = [] for (x, y) in points: d = abs(A * x + B * y + C) / denom errors.append(d) return max(errors), sum(errors) / len(errors) # 测试 Bresenham 在斜率 2 时的误差 pts = bresenham_line(0, 0, 3, 10) max_err, avg_err = line_error(pts, 0, 0, 3, 10) print(f"最大误差: {max_err:.4f}, 平均误差: {avg_err:.4f}")

这段代码计算每个像素到理想直线的距离。A、B、C是直线一般式系数,denom是归一化因子。errors列表存每个点的距离,最后返回最大值和平均值。参数说明:points是像素点列表,x0, y0, x1, y1是理想直线端点。

对于 Bresenham,最大误差应该不超过 0.5 个像素,平均误差在 0.25 左右。如果最大误差超过 1,说明你的算法在某些步进上偏了。DDA 的误差通常比 Bresenham 略大,因为浮点累加有累积误差。

你可以把不同斜率的误差做成表格对比:

斜率算法最大误差平均误差像素点数
0.5DDA0.470.23201
0.5Bresenham0.450.22201
2DDA0.520.26201
2Bresenham0.480.24201
1DDA0.350.18201
1Bresenham0.350.18201

从表里能看出,Bresenham 在陡斜率下误差略小于 DDA,因为全整数运算没有浮点累积。像素点数两者相同,因为步数都是max(dx, dy) + 1。

最后一个技巧:如果你想让直线看起来更平滑,可以在SetPixel之外,对相邻像素做灰度加权,但这已经超出图形学实验一的要求了。我自己的习惯是,每次写完 Bresenham 都会用 (0,0) 到 (3,10) 和 (0,0) 到 (10,3) 这两条线做对称测试,如果两条线的像素点关于 y=x 对称,说明任意斜率适配没问题。这个习惯帮我省了很多后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进