ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

简易图形绘制系统复盘:从光栅化到交互设计

简易图形绘制系统复盘:从光栅化到交互设计 简介本资源是南京大学计算机科学与技术系图形学课程的大作业成果——一个基于Python实现的简易图形绘制系统面向高校计算机图形学初学者及实践者旨在通过完整项目驱动方式掌握核心图形学原理与编程实现。资源共13个文件包含3个核心Python源码含算法实现、命令行与GUI交互模块、6幅BMP测试图像、1张PNG结构示意图、1份README说明文档及辅助文本文件整体压缩包仅88KB轻量易读。已有897人学习下载体现其在教学实践中的广泛参考价值。读者可直接运行cg_gui.py启动图形界面结合cg_algorithms.py深入理解Bresenham直线算法、RGB颜色操作、二维几何变换矩阵应用及基础渲染逻辑目录结构清晰分层涵盖输入/输出/演示/图像资源等模块便于按图索骥开展复现、调试与二次开发。 图形学课程里几乎每个学校都会安排一次“简易图形绘制系统”大作业。听起来很简单无非就是鼠标点两下画条直线、画个圆但真动手之后才会发现这个项目把图形学最核心的一整条链路全串起来了几何模型怎么组织、坐标怎么变换、光栅化怎么把连续几何变成离散像素、交互怎么把鼠标点击变成图元参数、拾取和裁剪又怎么处理。它不是一个“画图板”Demo而是一个微缩版的图形系统你把这条链路走通一遍后面再学OpenGL、学渲染管线会轻松非常多。这篇文章是我在做南京大学计算机科学与技术系图形学大作业“简易图形绘制系统”时的完整复盘包含整体设计思路、光栅化算法的实现要点、交互框架的搭建过程、常见问题的排查记录以及一些课程文档里不会写但实测非常管用的经验。适合正在做类似图形学作业的同学参考也适合想快速了解一个最小图形系统内部结构的读者。1. 项目整体设计与思路拆解1.1 核心需求演进从“画出来”到“建系统”很多同学拿到这类题目第一反应是“我直接用Qt的QPainter::drawLine不就行了”或者“OpenGL一句glDrawArrays搞定”。如果你只是在做普通的桌面绘图应用这么写没有任何问题但图形学大作业的真正考点是你要在不依赖高级封装的前提下把“几何对象”变成“屏幕像素”的过程自己实现一遍。也就是说这个项目的本质不是做一个画图软件而是做一个“解释器”用户在界面上用鼠标描述出直线、圆、椭圆、多边形、曲线的几何参数系统把这些参数交给光栅化模块由光栅化模块决定哪些像素被点亮。你做的是渲染管线的简化版不是界面控件堆砌。从课程评分角度常见的分档大概是这样的及格档能用鼠标交互画出直线、圆、矩形并且图元能显示在画布上。良好档图元可以选中、删除、移动、改变颜色实现了多边形填充或曲线绘制。优秀档算法实现完整Bresenham、中点画圆、种子填充、Bezier曲线等支持裁剪、图形变换平移/旋转/缩放、保存/读取工程文件并且界面交互顺畅。所以你在动手之前最好先确认自己的目标分数段。我的建议是先保证算法层完全独立再考虑界面层。因为算法层是课程的核心界面层只是外壳算法写得扎实哪怕界面粗糙一点分数也不会低。1.2 技术选型为什么我用了 C Qt 手写光栅化技术栈选择上我最终敲定的是 C Qt 5绘制部分不用 QPainter 的现成图元函数而是自己实现所有光栅化算法然后把像素数据交给 QImage 显示。为什么这么选C 是图形学课程的默认语言内存管理和对象组织方式更贴近底层实现写出来的代码能直接对照教材伪代码。Qt 的消息循环和事件系统非常成熟处理鼠标交互比 Win32 API 省心太多跨平台也不用操心。不直接用 OpenGL是因为 OpenGL 本身已经把光栅化隐藏到硬件里了你在这里能获得的知识量不够但你可以把 QImage 理解成一个“内存帧缓冲”这跟 OpenGL 的 framebuffer 概念是相通的后面对接硬件渲染并不会白学。要强调的是QPainter 可以用来做最终显示和 UI 绘制但不应拿它来画业务图元。你可以把 QImage 当作画布自己写的 drawLine 往这个 image 上 setPixel也可以把 QImage 直接显示在 QLabel 上。这样既保证了“算法透明”又不用从头写窗口系统。1.3 模块划分把职责分清楚后面调试少一半痛苦我在动手写代码前先画了一张模块划分图把所有职责按照“算法、数据、交互、显示”四个方向拆开算法层直线光栅化、圆/椭圆光栅化、多边形填充、Bezier 曲线、裁剪算法等。这一层不依赖任何 Qt 类型输入输出都是自定义结构体方便单元测试。数据层维护当前画布中的所有图元对象Shape 基类以及直线、圆等子类每个图元保存自己的几何参数和样式属性。交互层处理鼠标事件把鼠标拖拽变成“正在预览的图元”在释放时把预览图元提交给数据层。显示层每次数据层变更后把所有图元重新光栅化到 QImage然后触发界面刷新。这个分层在初期会显得有点“重”但对中期调试和后期的扩展帮助非常大。比如你在做裁剪算法时只需要盯着算法层的函数输入输出完全不用管界面有没有闪屏你在做交互时也只需要关心事件状态机不需要碰像素。2. 核心算法拆解直线、圆、曲线与填充实战2.1 Bresenham 直线光栅化为什么不用 DDA以及怎么处理斜率直线光栅化是这门课的“第一道坎”。教材里通常讲三种方法数值微分法 DDA、中点画线法、Bresenham 算法。DDA 的实现最直观核心就是利用斜率 step 一步步算 y 值但 DDA 每一步都有浮点加法和取整操作效率低且在某些坐标范围下会出现不均匀的像素分布。Bresenham 的做法是全程只用整数运算通过误差项的正负决定 y 是否递增。Bresenham 算法的核心思想可以这样理解在像素网格上从起点到终点x 方向每前进一列y 方向要么保持不动要么前进一行。到底动不动取决于当前点与理想直线的“垂直误差”是否超过 0.5 像素。误差项增量可以用整数维护避免浮点。我用的是处理任意斜率版本的实现核心代码如下void drawLineBresenham(int x0, int y0, int x1, int y1, QImage canvas, QRgb color) { 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 (true) { setPixelSafe(canvas, x0, y0, 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; } } }这里最需要注意的一点是setPixelSafe一定要做边界检查。我一开始没加边界检查画布外的像素写入会导致随机崩溃因为 QImage 在越界写像素时行为是未定义的。另外测试时建议把起终点调成各种方向水平、垂直、主对角线、斜率大于 1、斜率小于 1、反向等逐项确认像素点连成的线段没有断点。2.2 中点画圆与椭圆利用对称性把计算量降到八分之一圆的光栅化如果老老实实遍历 x 求 y会出现两个问题一是水平方向像素稀、垂直方向像素密曲线看起来不均匀二是涉及 sqrt 计算效率太低。经典方案是“中点画圆法”利用圆的八对称性只计算第一象限内从 (0, R) 到 (R/√2, R/√2) 的八分之一圆弧然后通过对称变换生成全部像素。中点画圆的核心是维护一个判别式 d初始值为1 - R每一步根据 d 的符号判断下一个点是走“正右方”还是“右下方”。我用的是教材中的整数版本void drawCircleMidpoint(int cx, int cy, int radius, QImage canvas, QRgb color) { int x 0; int y radius; int d 1 - radius; while (x y) { setPixelSafe(canvas, cx x, cy y, color); setPixelSafe(canvas, cx - x, cy y, color); setPixelSafe(canvas, cx x, cy - y, color); setPixelSafe(canvas, cx - x, cy - y, color); setPixelSafe(canvas, cx y, cy x, color); setPixelSafe(canvas, cx - y, cy x, color); setPixelSafe(canvas, cx y, cy - x, color); setPixelSafe(canvas, cx - y, cy - x, color); if (d 0) { d 2 * x 3; } else { d 2 * (x - y) 5; y--; } x; } }我第一次写这个算法的时候把 d 的更新公式记错了结果圆在第二象限出现断层。排查了很久才发现是自己把d 0和d 0的更新式写反了。所以建议你写完代码后先画一个半径很小的圆比如 r5逐步打印 d 和 x,y 的值跟教材的推导表对比一遍确认无误后再画大圆。圆画完椭圆就可以在这基础上扩展。标准椭圆没有八对称性只有四对称性所以中点法需要维护两个区域斜率绝对值小于 1 的区域和大于 1 的区域分别用不同的判别式。这里有个容易踩的坑椭圆的长短轴半径 a、b 不相等时判别式的增量公式都要带上 a、b 的平方项不能直接套圆的公式。2.3 Bezier 曲线与多边形填充数学表达与像素化如果说直线和圆是图形学的“基本功”那 Bezier 曲线就是“进阶题”。课程里一般要求实现三次 Bezier 曲线定义是给定 4 个控制点 P0、P1、P2、P3曲线上的点由参数 t 决定B(t) (1-t)³P0 3(1-t)²tP1 3(1-t)t²P2 t³P3实际绘制时最简单的办法是在 [0,1] 区间内按固定步长取 n 个 t 值计算对应的点然后用直线段连接相邻点。n 取多少合适呢我试过 n100 和 n1000肉眼几乎看不出区别但性能上 n100 明显更流畅。你可以根据控制点之间的距离自适应调整步长控制点包围盒对角线越长步长越密。多边形填充算法我采用的是扫描线种子填充。思路是先计算多边形每条边与当前扫描线的交点把所有交点按 x 排序然后成对填充像素区间。这里最容易出问题的是交点数量为奇数的情况一般原因是顶点恰好落在扫描线上导致顶点被重复计数。解决办法是采用“上开下闭”或“左开右闭”的像素规则也就是当边与扫描线相交于顶点时只在边的一侧计数避免重复。我在实现时专门写了一个isVertexOnScanline的辅助函数来处理这种边界情况最终填出来的多边形内部不会出现漏填或溢出。3. 交互设计与图形变换让系统真正可用3.1 鼠标事件状态机按下、拖拽、释放的逻辑闭环图形系统的交互核心是“橡皮筋”效果鼠标按下时确定图元的起点拖拽过程中持续显示预览松开时确认终点并生成正式图元。实现这个效果需要维护一个简单的状态机状态包括Idle空闲状态鼠标按下后进入 Drawing 状态。Drawing正在拖拽每个 MouseMove 事件都更新临时终点并且触发重绘。DoneMouseRelease 触发把临时图元正式加入数据层回到 Idle。在 Qt 里我重写了mousePressEvent、mouseMoveEvent、mouseReleaseEvent三个方法。这里有一个非常关键的细节在拖拽过程中Qt 默认不会持续发送 MouseMove 事件除非你在构造函数里调用setMouseTracking(true)。我刚开始没开这个开关导致只有按住鼠标按钮时才能收到 move 事件松开后曲线就断了。如果你发现拖拽时图形不跟随鼠标第一个要检查的就是 mouseTracking 是否开启。另一个容易忽视的点是预览图元不应该直接写进正式图元列表否则每次移动鼠标都会往列表里增加一个新对象造成内存膨胀和重绘变慢。我的做法是为预览图元单独设一个成员变量m_previewShape每次 MouseMove 时只更新这个对象然后调用update()让整个画布重绘。重绘函数里先画正式图元再画预览图元这样用户看到的就是实时跟随的橡皮筋效果。3.2 拾取与命中测试用户点选图元背后的算法画好了图元下一个功能需求通常是“选中它、移动它”。选中的本质就是拾取也就是判断鼠标点击坐标是否落在某个图元附近。最简单的拾取策略是距离阈值直线计算点到线段的距离如果小于阈值比如 5 像素则判定命中。圆计算点击点到圆心的距离与半径的差值绝对值小于阈值即命中。曲线可以对曲线做密集采样然后计算点击点到每个采样点的距离取最小值判断。这个方案实现起来非常简单但有一个小坑对于填充图形比如填充后的多边形用户点选时可能希望点击多边形内部也能选中这时就需要额外做一个点与多边形的包含测试。最经典的算法是射线法从点击点向右发射一条水平射线统计与多边形边的交点个数如果交点数为奇数则点在多边形内部。测试时记得覆盖“点击点在顶点上”和“点击点在水平边上”这两种边界情况否则会出现错误的奇偶判断。我实际测试下来阈值 5 像素在普通分辨率屏幕上比较舒适用户不会觉得太难点也不会误触。如果你的系统支持缩放画布记得把阈值除以缩放倍数否则放大后拾取区域会偏大。3.3 图形变换矩阵从局部坐标到世界坐标如果作业要求支持“移动、旋转、缩放选中图元”那么你需要在图元对象里额外保存一个变换矩阵或者更简单一点保存图元的“局部坐标”并应用变换后得到“世界坐标”。我用的是 3x3 齐次坐标矩阵支持平移、旋转、缩放三种基本变换struct Matrix3x3 { double m[3][3]; static Matrix3x3 identity() { Matrix3x3 mat{}; mat.m[0][0] mat.m[1][1] mat.m[2][2] 1.0; return mat; } static Matrix3x3 translation(double tx, double ty) { Matrix3x3 mat identity(); mat.m[0][2] tx; mat.m[1][2] ty; return mat; } static Matrix3x3 rotation(double angle) { Matrix3x3 mat identity(); double c cos(angle), s sin(angle); mat.m[0][0] c; mat.m[0][1] -s; mat.m[1][0] s; mat.m[1][1] c; return mat; } static Matrix3x3 scale(double sx, double sy) { Matrix3x3 mat identity(); mat.m[0][0] sx; mat.m[1][1] sy; return mat; } };需要说明的是旋转变换矩阵默认是绕原点旋转的。如果你希望图元绕自身的中心点旋转需要先平移到原点、旋转、再平移回去。这一步是很多同学丢分的地方直接乘旋转矩阵结果图元一边旋转一边跑位。表达式应该是T(cx, cy) * R(angle) * T(-cx, -cy)把这个组合矩阵作用到图元的所有控制点上图元就会围绕自己的中心点旋转。我在实现变换交互时是用鼠标拖拽角度来实时更新旋转矩阵的效果很直观而且代码逻辑集中在“计算矩阵”和“顶点应用矩阵”两个函数里调试起来很清爽。3.4 裁剪算法为什么说裁剪是窗口系统的基石图形学课程里裁剪是绕不开的。这个项目里我实现了 Cohen-Sutherland 线段裁剪算法应用场景是以后如果要加“视口”功能或者用户把图元拖出画布后再拖回来系统能正确显示画布内的部分。Cohen-Sutherland 的核心是区域编码把平面按裁剪窗口的上下左右边界分成 9 个区域每个区域用 4 位编码表示然后对线段两个端点做编码判断如果两端点的编码按位与不为 0说明两端点都在同一外部区域线段完全不可见直接丢弃。如果两端点编码均为 0说明完全在裁剪窗口内保留。否则就是部分可见需要求线段与窗口边界的交点把线段缩短后继续判断。实现时要注意浮点除法可能产生除零问题如果线段的 dx 为 0说明是垂直线求交点时不能直接除以 dx。我第一次写的时候没考虑水平线和垂直线结果这两种线段在裁剪时全部失效。裁剪算法写完以后建议用一组覆盖所有情况的测试用例来验证完全在窗口内的线段、完全在外面的、横穿窗口的、起点在内终点在外的、起点在外终点在内的、以及恰好经过窗口顶点的。这几种情况全部通过才说明裁剪逻辑是健壮的。4. 实操过程与核心环节实现记录4.1 工程搭建Qt Widgets QImage 画布的最小骨架我用的 Qt 版本是 5.15创建的是 Qt Widgets Application。整个工程的核心文件只有 4 个主窗口类 MainWindow、画布控件类 CanvasWidget、图元基类 Shape 和三个子类、以及一个独立的 Algorithm 命名空间用来放光栅化函数。主窗口界面非常简单左侧是图元类型选择按钮直线、圆、椭圆、多边形、曲线中间是画布 QLabel右侧是颜色选择和操作按钮撤销、清空、保存图片。我没有用 Qt Designer 拖界面而是直接代码写布局这样更容易控制逻辑。画布控件部分关键的成员变量包括class CanvasWidget : public QWidget { Q_OBJECT public: explicit CanvasWidget(QWidget* parent nullptr); protected: void paintEvent(QPaintEvent* event) override; void mousePressEvent(QMouseEvent* event) override; void mouseMoveEvent(QMouseEvent* event) override; void mouseReleaseEvent(QMouseEvent* event) override; private: QVectorstd::shared_ptrShape m_shapes; std::shared_ptrShape m_previewShape; ShapeType m_currentType; QColor m_currentColor; QPoint m_pressPos; QPoint m_currentPos; bool m_drawing false; };paintEvent 里做的事情非常简单先调用rebuildImage()把 m_shapes 里的图元重新光栅化到一个 QImage 上这一步是耗时操作数据量大时考虑做增量更新然后把 QImage 绘制到 widget 上。这样你操作的是“内存位图”而屏幕上永远显示的是最新状态。4.2 从“画到屏幕”到“持久化图元”状态管理的关键差异交互层踩过最大的坑是把“绘制像素”和“保存图元”混为一谈。直接调用 drawLine 往 QImage 上画像素确实能看到图形但一旦窗口刷新比如最小化再恢复所有像素都会消失。因此必须让数据层保存“图元描述”而不是保存“像素结果”。在这个架构里每次鼠标释放后我会把新图元加入 m_shapes然后调用clearCanvas()清空 QImage再遍历 m_shapes 重新绘制所有图元。如果图元数量不大几十到几百这个方案完全够用而且天然支持撤销和重绘。如果图元数量上千可以考虑把绘制结果缓存到 QImage 里只在图元增删时局部重绘但课程作业通常不需要优化到这个程度。撤销功能也受益于这个设计我维护了一个QVectorQVectorstd::shared_ptrShape m_history作为历史栈每次提交新图元之前先把当前 m_shapes 的深拷贝压入栈。撤销时从栈顶弹出恢复 m_shapes然后触发一次全量重绘。注意这里必须是深拷贝否则历史里的图元对象和当前对象共享同一块内存改了当前对象历史也会变撤销就失效了。4.3 画布刷新与性能优化setPixel 有没有更快的写法QImage 的setPixel是按像素操作的每调用一次都会检查坐标范围和格式转换所以当你绘制一条包含几万个像素的曲线时性能会明显下降。我的优化方案是调用bits()直接操作底层内存void setPixelSafe(QImage image, int x, int y, QRgb color) { if (x 0 x image.width() y 0 y image.height()) { uchar* line image.scanLine(y); line[x * 4 0] qBlue(color); line[x * 4 1] qGreen(color); line[x * 4 2] qRed(color); line[x * 4 3] qAlpha(color); } }这里有个前提QImage 的格式必须设置为QImage::Format_ARGB32每个像素占 4 字节顺序是 BGRA。如果你用了其他格式比如Format_RGB32字节布局也不同需要按实际格式调整偏移。直接操作内存虽然快但要注意你的图像格式必须是 32 位的否则line[x * 4]的偏移会错位导致画出来的颜色全部错乱。另外如果画布很大比如 1920x1080每次全量重绘时把所有图元都重新光栅化一次在大图元数量下会有卡顿。我的优化技巧是维护两个 QImage一个m_backgroundImage保存当前所有图元的光栅化结果一个m_tempImage在拖拽时用来叠加预览图元。平时只更新 m_backgroundImage拖拽预览时复制一份 background 到 tempImage然后在 tempImage 上画预览图元。这样正式图元不会被反复重画性能提升非常明显。4.4 文件保存与读取自定义 JSON 格式 vs 导出图片课程作业做到后期很自然会要求“把画布内容保存下来”。这里有两种截然不同的需求保存为图片PNG/JPG适合导出最终效果用 QImage::save 一行代码就能完成。保存为工程文件适合把图元数据保存下来下次打开还能继续编辑这时需要把图元的类型、坐标、颜色、线宽等序列化到文件。我实现的是 JSON 格式用 QJsonDocument 把每个图元序列化成一个对象{ type: circle, center: [320, 240], radius: 80, color: #FF0000, lineWidth: 2 }加载时按 type 字段创建对应的子类对象。这个格式有两个好处一是人眼可读调试时可以直接打开文件查看坐标对不对二是扩展性极好以后加一个“填充颜色”字段老文件依然能正常解析。这里提醒一个坑JSON 序列化浮点数时如果直接输出 double会有很多位小数文件体积会膨胀。我建议保存前先对坐标取整或限制到小数点后 2 位因为图像绘制是像素级的小数精度意义不大反而会拖累读写性能。5. 常见问题与排查技巧实录5.1 编译链接阶段moc 文件缺失与头文件依赖Qt 项目最常见的编译问题有两个。第一个是自定义类带了Q_OBJECT宏但没有在 .pro 文件里正确包含对应头文件导致 moc 文件没有生成。解决方法一般是在 .pro 里检查HEADERS 是否包含了该类头文件Qt Creator 在保存时会自动调用 moc如果没生效执行一次qmake再重新构建。第二个问题是链接阶段报undefined reference to vtable这通常是因为某个带 Q_OBJECT 的类的 moc 文件没有被重新生成或者类定义里新增了 slot 但没有重新编译。遇到这类问题先执行“清除项目、qmake、重新构建”三步曲大多数能解决。如果还不行检查类的析构函数和拷贝构造函数是否声明了但没有实现这个也会导致 vtable 链接失败。5.2 画面显示问题图片模糊、坐标反转、颜色异常如果你用 QLabel 显示 QImage需要调用ui-label-setPixmap(QPixmap::fromImage(image))。不要直接把 QImage set 到 label 上那样不会自动刷新。另一个常见问题是 QLabel 默认会缩放图片导致模糊如果你希望 1:1 显示需要把 label 的 sizePolicy 设置为固定大小或者使用 QScrollArea 包裹。坐标反转是我在实现圆和曲线时踩过的一个隐蔽的坑。屏幕坐标系 y 轴朝下数学坐标系 y 轴朝上如果你从鼠标事件里获取的坐标直接传给光栅化算法那么用户向上拖动时圆的 y 坐标是减小的看起来圆是“反着画”的。解决办法是定义一层逻辑坐标转换在交互层把鼠标坐标转换为“世界坐标”在光栅化层再把世界坐标转换为像素坐标。虽然麻烦但这是做图形系统的正道后续加缩放、平移会非常轻松。颜色异常的问题一般出在 QImage 格式上。如果你创建图像时用了Format_Mono或Format_RGB16后面 setPixel 的颜色通道就会错乱。我的建议是统一用Format_ARGB32其他格式一律不用能规避掉 80% 的颜色问题。5.3 交互行为bug拖拽断线、点选误触、撤销错乱拖拽时看不到图形跟随上面已经说过是setMouseTracking的问题但还有一个隐蔽问题如果你在mouseMoveEvent里做的操作比较重比如每次都做全量重绘鼠标移动时画面会一卡一卡看起来像“断线”。解决方法是把预览图元的绘制放到 paintEvent 里完成而不是在 mouseMoveEvent 里直接画到 QImage 上。点选误触通常发生在图元密集区域。我一开始用的是“点击点是否在图元外接矩形内”作为选中的判定条件结果矩形区域互相重叠用户点击一条直线经常选到旁边的圆。后来改成精确的“点到图元距离”判定误触率大幅下降。建议你在做拾取时遍历顺序改为“最后绘制的图元优先判定”这样用户可以很方便地选中重叠区域的上层图元。撤销错乱的问题我在上面提过根因是浅拷贝。这里再补充一个细节如果你的 Shape 类里包含 QVector 等动态容器默认拷贝构造函数是浅拷贝两个对象会共享容器的数据指针。必须实现深拷贝的 clone 方法或者用 std::shared_ptr 管理数据把图元对象放进智能指针里这样撤销历史里保存的每个对象都独立存在。5.4 算法边界问题圆弧断层、填充溢出、裁剪除零这一节记录的是我在测试阶段专门整理的问题速查表建议你自己做同样的事情把每种算法容易出 bug 的边界情况列出来逐一验证。症状可能原因排查方法圆出现十字形缺口八对称性只画了四分之一或者对应像素坐标写错检查对称变换时 x、y 是否互换正确圆在某个方向半径明显偏大圆心坐标参与对称后出现偏移确认圆心 cx, cy 在每一组对称点中都保持不变填充后边缘多出或漏掉一列像素扫描线交点奇偶计数出错检查顶点扫描线规则采用上开下闭线段裁剪结果多个水平或垂直端点除零或把边界交点计算错单独测试 dx0 和 dy0 的情况Bezier 曲线弯曲方向不对控制点顺序颠倒确认 P0 到 P3 的顺序与用户点击顺序一致圆弧断层还有一个常见原因中点画圆法写错判别式初值。标准版本是 d 1 - R为什么不是 1.25 - R因为我们要避免浮点。但如果你把初值写成 1 - R在 R 特别大时可能会差一个像素这是我做的大半径测试时发现的。更稳妥的写法是用浮点初值 1.25 - R然后比较 d 与 0 的关系但这样会引入浮点运算。绝大部分课程作业不会强求半径超过几千像素所以整数版本够用但你自己要清楚这个取舍。填充算法如果使用递归种子填充当多边形面积很大时递归深度会非常大甚至导致栈溢出。我的解决方法是改成显式栈的“扫描线种子填充”用一个 std::stack 保存种子点。这是工程上常用的优化不会改变算法正确性只是把隐式递归改成了显式循环。5.5 一点实用经验写算法前先写测试用例写完再写界面如果你只打算从这篇文章里带走一条经验那就是先写算法然后用独立的小测试程序验证最后再集成到界面。我一开始图省事直接把算法写进 CanvasWidget 类里一边写界面一边调算法。结果出现了一个 bug画直线时看起来没问题但画圆时偶尔出现颜色不对的像素。我误以为是光栅化算法的问题查了大半天才发现是 QImage 颜色格式设置错误跟圆算法毫无关系。后来我把所有光栅化函数抽到独立的namespace raster里写了一个简单的命令行测试程序用 ReadBack 的方式检查像素坐标是否在期望位置。比如画一条从 (0,0) 到 (10,5) 的直线程序输出所有实际被点亮的像素坐标我可以直接对照 Bresenham 的推导过程一眼看出哪里多了像素、哪里少了像素。这个习惯帮我省下的调试时间至少是三分之一的开发周期。图形学大作业难吗难但难不在某个算法看不懂而在于把一整个系统串起来的时候各种小问题会互相干扰。分开隔离、逐层验证是这个项目最重要的工作方法。本文还有配套的精品资源点击获取
返回列表