
简介面向计算机图形学实验教学与课程设计场景这份实验报告围绕任意斜率直线段的绘制展开重点讲解中点Bresenham扫描转换算法在斜率[-1,0]区间内的实现适合正在学习扫描转换或需要完成类似作业的本科生使用。报告给出了CLine直线类的完整设计过程数据成员包括直线起点坐标和斜率k成员函数MoveTo()负责设置起点LineTo()负责按斜率逐点绘制同时附有Line.h、Line.cpp中的核心代码以及MFC视图下OnDraw函数的调用示例可直接用于实验报告撰写、代码调试或课堂演示。压缩包内仅有1个doc文档大小约60KB内容精炼包含实验目的、要求、核心代码、运行结果与心得体会等部分。目前已有410人浏览学习。读者可通过该文档快速掌握中点Bresenham算法的误差项推导、判别条件与递推公式并理解如何用“起点斜率”的方式构建直线绘制类帮助提升图形学实验的完成效率。1. 图形学直线段绘制为什么卡在“任意斜率”这个坎上刚接触计算机图形学时最容易产生误解的就是“画直线有什么难的两点之间套个 ykxb 代入像素坐标就行”。但当斜率 k 的绝对值大于 1或者直线从右上角斜向左下角时光栅化的像素点开始变得稀疏甚至断裂——因为屏幕上的像素是离散网格x 每推进 1 个单位y 的变化量如果小于 1就会产生连续多个像素停留在同一行而一旦斜率变得很大x 方向每走一步 y 要跨过好几行如果还用“逐 x 扫描、计算 y”的思路就会出现竖直方向像素点断档。这个问题的本质是我们不是在画数学意义上的连续直线段而是要把直线段映射到离散的像素坐标网格上并且保证相邻像素在 8 连通意义下是连续的。“任意斜率”四个字意味着算法不能只会画 0 到 45 度之间的线也不能靠 if 把斜率分成无穷多种情况分别处理它必须在数学上找到统一的形式。这篇文章就围绕“任意斜率的直线段光栅化”来设计一个计算机图形学实验先把斜率带来的坐标系选择问题立住然后落到可复现的 Bresenham 整数算法实现最后给出一套覆盖从 0 到无穷斜率的测试矩阵和压测方法。适合正在做计算机图形学课程实验、或者想搞清直线扫描转换为什么这么设计的人。2. 斜率空间与坐标系选择任意斜率直线段的数学归一化2.1 为什么斜率的绝对值大于 1 时必须交换主位移轴先看一个最简单的错误做法对直线 y kx b固定 x 从 x0 到 x1 每次加 1算出 y 取整后画点。当 0 k 1 时x 增加 1y 的增加量小于 1所以每列只会点亮一个像素画出来是连续的。但当 k 1 时例如从 (0,0) 到 (10, 100)x 每增加 1y 要跨过 10 行。若还按 x 步进扫描就会得到 10 个彼此不相邻的孤立点中间的行全部被跳过线段看起来像虚线。所以“任意斜率”的第一个结论是光栅化步进方向不能盲目选择 x 轴而应该选择“位移较大的那个坐标轴”作为主位移方向。设 dx |x1 - x0|dy |y1 - y0|则当 dx dy 时x 是主位移方向每次步进 1 个像素当 dx dy 时y 是主位移方向每次步进 1 个像素。这相当于把问题归一到“斜率绝对值小于等于 1”的子问题上处理。但很多初学者会把这理解成“先画斜率小于 1 的再特判斜率大于 1 的”然后写出两套分支代码结果在处理负斜率时又崩了。更好的做法是通过坐标交换统一当 dy dx 时交换 x 和 y 的角色然后用同一套增量逻辑处理。提示主位移方向是直线光栅化里最重要的概念。它决定了离散化的密度选错了方向哪怕后续误差计算全对画出来也是断的。2.2 误差项递推把乘法和浮点逐出像素循环得到主位移方向后接下来要解决的是“每一步该怎么决定次坐标是否前进”。以 dx dy 为例每一步 x 递增 1y 可能递增 1也可能保持不变。判断依据是当前像素点与理想直线之间的垂直距离是否跨过了半格阈值。传统做法是计算斜率 k dy / dx每一步比较累计误差是否大于 0.5但这需要浮点数。Bresenham 算法的核心贡献是把浮点比较变成整数误差项的错误积累。定义误差项 err初始为 0。每次主坐标步进 1 时err 增加 dy注意不是斜率 k。当 err 大于某个阈值时说明次坐标也该步进了然后 err 减去 dx。写成伪代码就是for each step in [0, dx]: plot(x, y) err dy if err * 2 dx: // 相当于 err 0.5 y sy err - dx x sx这里乘 2 是为了避免小数因为阈值是 0.5。这个递推的逻辑基础是每次主坐标走一步理想直线在次方向上的变化量是 dy/dx误差项 err 实际在累加这个小数的整数倍近似。当累加量超过半像素时说明实际像素已经偏离理想直线超过半格必须修正一次次坐标。2.3 统一任意斜率的增量符号与误差判断上述伪代码只覆盖了 dx dy 且斜率正数的情况。要扩展到任意斜率需要同时处理三类变化斜率正负、主位移方向、坐标增减方向。一个简洁的完备写法如下void drawLine(int x0, int y0, int x1, int y1) { int dx abs(x1 - x0); int dy -abs(y1 - y0); // dy 取负统一误差符号 int sx x0 x1 ? 1 : -1; int sy y0 y1 ? 1 : -1; int err dx dy; // 初始误差为 dx - |dy| while (1) { setPixel(x0, y0); if (x0 x1 y0 y1) break; int e2 2 * err; if (e2 dy) { // 主方向需要步进 err dy; x0 sx; } if (e2 dx) { // 次方向需要步进 err dx; y0 sy; } } }这段代码不用交换坐标轴却把任意斜率的直线段统一处理了。关键在于 dy 被设计为负数当 dx |dy| 时err 初始为正值或 0随着主方向步进 err 累加 dy即减小当 e2 dy 时说明主方向该走了当 e2 dx 时说明次方向该走了。如果把 dy 取正两个 if 判断的顺序和符号都要反过来更容易出错。这种写法来自中点偏差决策的统一形式也是我在实际实验课里最推荐学生抄的版本。3. 一套代码覆盖 8 方向C 语言直线段实验框架搭建3.1 实验环境与最小编译命令这个实验不依赖 OpenGL也不需要安装任何图形库核心目的是观察像素坐标序列与理想直线的关系。我一般建议用纯 C 语言在终端里输出字符画来模拟屏幕像素。这样一来任何人拿到代码就能编译运行适合作为计算机图形学课的入门实验。Linux 下使用 GCC 的编译命令是gcc -Wall -o line line.c ./line如果是 Windows 的 Visual Studio直接建一个控制台工程把代码贴进 main.c 即可。整个实验程序只需要几个头文件stdio.h和stdlib.h用来做输入输出string.h用来初始化缓冲区。不需要任何第三方库。提示建议把所有像素点的绘制结果先写进一个字符二维数组最后一次性打印避免每画一个点就换行刷屏这样能清楚看到整条线段的走向。3.2 实现 drawLine 并输出三条典型斜率直线下面给出一个可以直接运行的实验程序画布大小设定为 40 行 x 80 列分别测试三条直线段坡度 0.2接近水平、坡度 145 度、坡度 5接近垂直。这三条线覆盖了斜率小于 1、等于 1、大于 1 三种典型情况验证主位移方向的切换是否正确。#include stdio.h #include stdlib.h #include string.h #define WIDTH 80 #define HEIGHT 40 static char canvas[HEIGHT][WIDTH 1]; // 绘制一个像素x 向右y 向下与屏幕坐标一致 static void setPixel(int x, int y) { if (x 0 x WIDTH y 0 y HEIGHT) canvas[y][x] *; } void drawLine(int x0, int y0, int x1, int y1) { int dx abs(x1 - x0); int dy -abs(y1 - y0); int sx x0 x1 ? 1 : -1; int sy y0 y1 ? 1 : -1; int err dx dy; while (1) { setPixel(x0, y0); if (x0 x1 y0 y1) break; int e2 2 * err; if (e2 dy) { err dy; x0 sx; } if (e2 dx) { err dx; y0 sy; } } } int main(void) { for (int i 0; i HEIGHT; i) { memset(canvas[i], ., WIDTH); canvas[i][WIDTH] \0; } // 坡度约 0.2 的线 drawLine(2, 20, 60, 32); // 45 度对角线 drawLine(5, 5, 45, 45); // 坡度 5 的线 drawLine(60, 2, 68, 32); for (int i 0; i HEIGHT; i) printf(%s\n, canvas[i]); return 0; }代码逻辑说明主函数先初始化画布为点号然后依次画三条直线段。drawLine内部没有针对斜率做显式分支而是通过 dx、dy 的绝对值和正负号统一处理。setPixel增加了边界检查避免坐标越界导致数组访问异常这在实验调试中很重要——当测试用例故意超出画布时程序不会崩溃。参数说明WIDTH和HEIGHT决定了屏幕的宽高比。注意字符终端里一个字符的高宽比大约是 2:1所以画斜率为 1 的直线显示出来会被拉长。如果希望视觉上更接近真实比例可以把 WIDTH 设成 HEIGHT 的两倍。这个细节在做“任意斜率”展示时会影响观察效果我一般会在实验文档里让学生注意。3.3 实验数据集设计覆盖 0 到无穷斜率的测试矩阵要验证算法真的支持任意斜率光画三条线不够必须设计一张覆盖各种斜率特征的测试表。下表是我在这个实验里固定的测试用例集每一行关注一个斜率特征共 8 组覆盖两个维度的正负、大小以及边界条件。编号起点终点实际斜率约验证重点T01(0, 20)(60, 20)0水平线主方向为 x次方向永不步进T02(30, 0)(30, 39)无穷垂直线主方向为 yT03(0, 0)(60, 30)0.5T04(0, 0)(30, 60)2.0T05(60, 0)(0, 30)-0.5负斜率sx 为负T06(30, 60)(0, 0)2.0负方向步进sy 为负T07(0, 0)(40, 40)1.0对角特例两个 if 同时触发T08(60, 30)(0, 10)0.33起点终点都在画布中检查完整性测试用例 T04 和 T06 专门验证斜率绝对值大于 1 时不会断线。T07 是一个隐蔽的边界条件在斜率正好为 1 时e2 dy和e2 dx可能同时成立最终 x 和 y 同时步进。如果代码把两个 if 写成了 else if斜率等于 1 时会少走一步导致线段终点略有偏差。运行程序时可以先用printf打印出每一条线段的实际像素个数再对照理想像素距离做估算观察数量是否在合理范围内。更系统的验证方法见下一章。4. 实验中的 3 个高频坑负斜率、垂直线与误差符号4.1 负斜率场景下 sx/sy 与 dx/dy 的配合许多学生自己写 Bresenham 时会设计成“按 x 从 x0 到 x1 遍历y 用斜率计算”一旦 x1 x0循环根本进不去或者画出来是反的。上面给出的统一写法中sx和sy用来决定步进方向而 dx 和 dy 永远取绝对值。例如从 (60, 0) 到 (0, 30)dx 60dy -30sx -1sy 1。主循环每走一步x 递减 1y 偶尔递增 1画出一条向右下走却总体向左的线。实验里最容易出现的错误是在计算 dx 时写成x1 - x0而不是abs(x1 - x0)然后 sx 也参与判断导致 dx 为负数时整个误差项符号混乱。这在初学者代码里几乎必现。我的建议是第一步先算 dx 和 dy 的绝对值第二步再单独决定 sx/sy最后才写循环三步不要合并。int dx abs(x1 - x0); int dy -abs(y1 - y0); // 注意负号 int sx (x0 x1) ? 1 : -1; int sy (y0 y1) ? 1 : -1;这个顺序也是实验报告里应该写清楚的先归一化距离再决定方向方向与距离互不干扰。4.2 垂直线和水平线的误差项行为垂直线对应 dx 0、dy -40假设 y 差 40。初始 err 0 (-40) -40。进入循环后e2 -80。第一个条件e2 dy即 -80 -40 为假跳过第二个条件e2 dx即 -80 0 为真执行 y 步进。于是在每次循环中只触发第二个 ifx 从不变y 每次加 1画出一条竖直直线。从这个特例可以看出dy 取负之后两个 if 天然对应了“主方向是否步进”和“次方向是否步进”的判定。当 dx 为 0 时第一个 if 永远不会满足因为 e2 dy 只有在 dy 为负且 e2 不太小的时候才成立而主方向步进是错误行为。这比用else if处理垂直线要健壮得多——else if版本一旦遇到 dx0 且第一个 if 触发就会把 x 也推出去导致线段变成斜线。注意如果你的实验使用else if垂直线与水平线至少有一个方向会画错。务必让两个 if 独立存在不要互斥。4.3 像素坐标序列的 8 连通校验法实验不能只看终端里的字符画说“看起来像直线”还要有数值化校验。一个简单的办法是把所有绘制点坐标按顺序存到数组里然后检查相邻两个点的切比雪夫距离是否小于等于 1。如果出现某一对坐标的 x 差或 y 差同时超过 1说明线段在 8 连通意义下断了。下面的代码给出了这个校验逻辑int checkPixels(int px[], int py[], int n) { for (int i 0; i n - 1; i) { int dx abs(px[i 1] - px[i]); int dy abs(py[i 1] - py[i]); if (dx 1 || dy 1) return 0; // 出现断裂 } return 1; }你可以在setPixel里同时把坐标记录进数组整条线画完后跑一遍checkPixels。如果返回 0优先检查是否忘记处理斜率绝对值大于 1 的情况再检查两个 if 是否被写成了互斥结构。这个方法能把肉眼不容易看出的隔行断裂找出来也可以作为实验报告中的自动验证部分。更完善的做法是同时计算每一点到理想直线的垂直距离的最大值不过 8 连通校验已经足够发现大部分实现错误。对实验来说这一条是成本最低、信息量最高的检查手段。5. 用中点算法重构和批量斜率压测验证实验完备性了解 Bresenham 之后我通常会让学生再写一遍中点画线算法Midpoint Line Algorithm。两者在数学上是等价的但切入角度不同中点算法直接利用直线隐式方程 F(x, y) ax by c 的符号来判断像素中点在直线上方还是下方代码结构更接近几何推导也更容易验证“任意斜率”这一结论。中点算法的核心是在当前像素点 P 的下一个位置候选点是右方像素 E 和右上方像素 NE取两者连线的中点 M计算 F(M) 的符号。若 F(M) 0说明中点在直线下方选择 NE否则选择 E。当 |dy| |dx| 时把 x 和 y 的角色交换即可复用同一段逻辑。用中点算法重构直线绘制函数后与前面的 Bresenham 版本逐一对比像素输出结果应该完全一致。这个交叉验证步骤比任何单元测试都更让学生信服算法的正确性。最终实验还可以加一个批量压测程序随机生成 300 组起点和终点坐标保证每组斜率的绝对值分布在 0.1 到 20 之间正负各半。依次调用drawLine对每条线段执行断点校验并统计错误条数。如果错误为 0说明当前实现确实支持任意斜率。批量压测的 C 语言骨架只需要一个随机数生成器和一个计数变量两百行以内能写完却能一次性暴露边界缺陷。例如端点重合的情况、起点终点互换的情况都是普通测试用例不会覆盖的盲区。把这些内容写进实验报告直线光栅化这个实验的完整度就立住了。本文还有配套的精品资源点击获取