
简介基于C与MFC框架的计算机图形学源码集由孔令德编写适合正在学习图形学课程、需结合工程代码理解原理的学生与开发者。内容覆盖二维/三维坐标变换、Bresenham与DDA线段绘制、扫描线与梯形填充、正斜投影、Phong光照模型、贝塞尔曲线与NURBS曲面、碰撞检测及图形渲染等基础专题每个专题均提供完整的MFC工程可直接编译运行观察输出效果。压缩包共1522个文件总大小140.24MB以323个头文件与279个C源文件为主附295个图标、96个位图等界面资源配套43组dsp/dsw/rc工程配置文件目录按章节独立组织便于定位和修改。目前已有2583人学习下载适合作为课堂实验、课后自学及课程设计的参考起点既能加深对图形生成流程的理解也能提升MFC界面与交互编程的实际能力。1. 孔令德这套MFC图形学源码为什么软件渲染比 OpenGL 更适合入门计算机图形学源码 MFC 实现是图形学课程里流传很广的一套教学源码。老孔孔令德把教材里每个算法都写成了独立可运行的 MFC 工程画线、多边形填充、几何变换、投影、光照、曲线曲面全部用 C 和 MFC 的 CDC 在像素级上实现不依赖 OpenGL 和 GPU代码里全是 SetPixel、for 循环和矩阵乘法。单步调试时你能亲眼看到每个像素是怎么被算出来、写进去的。这正好补上大多数自学者的知识断层背了一堆图形 API 却不知道背后发生了什么。适合正在上图形学课、需要跑实验的学生也适合想补齐光栅化、裁剪、投影算法底子的从业者。想看 OpenGL 调参技巧的人不必下想搞懂算法本质的人这套源码比任何教程都直接。2. 拿到压缩包之后工程组织、VS 版本与编译前的三处设置2.1 解压后的目录结构先分清哪些是源码、哪些是缓存老孔这套源码的解压结构很有规律基本是「一个实验一个文件夹」每个文件夹里是一个完整的 MFC 工程。我建议拿到压缩包后别急着双击打开工程先按下面这张表过一遍文件类型免得把缓存文件当源码改了半天。文件类型典型文件名用途能不能动工程文件xxx.dsw / xxx.dsp老 VC 工程或 xxx.sln / xxx.vcxproj定义工程结构、编译选项可改升级向导会自动处理源码文件xxx.cpp / xxx.h算法与界面实现核心修改对象资源脚本xxx.rc / resource.h菜单、对话框、图标定义一般不用动资源缓存xxx.aps资源脚本的二进制缓存建议删掉会自动重建工程缓存xxx.ncb / xxx.sdf / .vs智能提示与编译缓存可删不影响编译这里特别说一下 .aps 文件。它本质是 .rc 资源脚本的二进制缓存供资源编辑器快速读取用的不是源码。很多新手打开资源编辑器报错、或者往对话框里拖控件时程序崩溃根源就是这个缓存和 .rc 不同步。我处理这类工程的第一习惯就是先删掉所有 .aps再打开工程。另外这套源码里不少工程是上个时代的 Visual Studio 写的用的是 .dsw/.dsp 工程格式新版 VS 会弹升级向导。升级时建议逐项确认而不是一路点「下一步」。升级完成后先编译一下原始代码确认基线是通的再开始读代码。2.2 编译前必改的三处设置字符集、平台、警告级别老 MFC 工程在新版 VS 里编译最常见的翻车点不是代码本身而是工程默认设置变了。我一般拿到代码后先做三件事能省掉后面一大半报错。第一处是字符集。老代码大量使用 char* 和 CString 的隐式转换而新版 VS 工程默认是 Unicode 字符集一编译就是一堆 C2664 无法从 char* 转换为 LPCTSTR。改法很简单右键工程 → 属性 → 配置属性 → 常规 → 字符集把「使用 Unicode 字符集」改成「使用多字节字符集」。大多数图形学算法代码只处理像素和坐标完全不涉及宽字符切回多字节是最省事的方案。第二处是目标平台。新版 VS 默认会有 x64 配置但老代码里常有把指针强转成 int 或 DWORD 的写法在 64 位下指针被截断运行起来数据全是乱的而且很难排查。解决方案是老老实实用 Win32x86平台编译。图形学教学代码没有大内存需求32 位完全够用。第三处是警告级别和 SDL 检查。老代码里有太多 sprintf、fopen 这类函数新版 VS 的 SDL 检查会直接把它们报成 error C4996。我一般把「SDL 检查」设为否把警告级别从 /W4 降到 /W3等代码跑通了再回头收拾警告。这三处设置是这套源码能不能顺利编译的关键也是我后来处理所有老 MFC 工程时的固定动作。// 设置路径速查VS2015 及以上通用 // 项目 - 属性 - 配置属性 - 常规 - 字符集使用多字节字符集 // 项目 - 属性 - 配置属性 - 常规 - 平台工具集Visual Studio 2015 (v140) 或更低 // 项目 - 属性 - C/C - 常规 - SDL 检查否 // 项目 - 属性 - 链接器 - 系统 - 子系统Windows (/SUBSYSTEM:WINDOWS)这几项改完后大部分工程能直接编译通过。如果还报错重点看是不是某个 .cpp 文件没被加入工程——老工程经常出现文件在文件夹里但不在工程列表里的情况解决方案是右键工程 → 添加 → 现有项把缺失的文件补进去。2.3 理解 MFC 的绘制入口为什么都在 OnDraw 里画图这套源码的算法函数第一个参数几乎都是 CDC* pDC很多初学者不理解为什么。这里要抓住 MFC 视图机制的核心视图类的 OnPaint 会调用 OnDraw而 OnDraw 接收一个 CDC 指针这个指针就是当前窗口的设备上下文所有 SetPixel、MoveTo 这样的绘图操作都要通过它来完成。void CMyView::OnDraw(CDC* pDC) { // 这就是绘制入口窗口需要重绘时系统自动调用 DrawLineBresenham(pDC, 10, 10, 200, 100, RGB(255, 0, 0)); }关键机制在于OnDraw 不是只调用一次。窗口被遮挡后恢复、尺寸改变、程序启动时系统都会触发重绘。所以你的绘制代码放进 OnDraw 里每次重绘都会自动执行。反过来说如果你把绘制放在按钮点击事件里只画了一次窗口一刷新图形就消失了这也是后面避坑章节里白屏问题的根源。我在看这套源码时习惯先在 OnDraw 里打断点确认它什么时候被调用再看某个具体算法函数。这样能把「MFC 的消息驱动机制」和「图形学算法」两件事分开理解代码就不乱了。3. 画线与填充算法逐段拆解从 DDA、Bresenham 到扫描线活性边表3.1 DDA 算法最容易理解的直线增量实现DDADigital Differential Analyzer数字微分分析仪的思路非常直白把直线分成若干小段每一段在 x 和 y 方向上按照斜率各走一小步步数取两个方向中较长的那一个保证每一步至少在一个方向上推进一个像素。void DrawLineDDA(CDC* pDC, int x0, int y0, int x1, int y1, COLORREF color) { int dx x1 - x0; int dy y1 - y0; int steps max(abs(dx), abs(dy)); // 步数取长轴长度 float xIncrement (float)dx / steps; // x 方向每步增量 float yIncrement (float)dy / steps; // y 方向每步增量 float x x0, y y0; for (int i 0; i steps; i) { pDC-SetPixel((int)(x 0.5f), (int)(y 0.5f), color); // 四舍五入取整 x xIncrement; y yIncrement; } }这段代码有三个细节需要注意。第一steps 取 max 而不是 min保证了任何方向的直线都不会因为步数不足而出现断点。第二加 0.5 再取整是经典的四舍五入技巧直接把 float 转 int 是截断画出来的线会整体偏移。第三xIncrement 和 yIncrement 是浮点数所以当直线很陡时x 方向增量很小、y 方向每次走一格多SetPixel 的取整操作会把像素点均匀地撒在理想直线附近。DDA 的优点是逻辑简单适合教学缺点是每次循环都要做浮点加法和浮点转整数的操作性能一般。这套源码里 DDA 通常是第一个实验我建议你把它跑起来后故意把 x0 换成比 x1 大的值观察负方向画线是否正确——很多人的第一版 DDA 只处理了从左到右的情况。3.2 Bresenham 算法用整数运算消灭浮点Bresenham 是对 DDA 的经典改进核心思想是把误差项用整数累积来表示全程只做加减法和比较不做浮点乘除。它的推理过程是从起点出发每走一步比较理想直线和实际像素点之间的误差误差超过某个阈值就往另一个方向走一步。void DrawLineBresenham(CDC* pDC, int x0, int y0, int x1, int y1, COLORREF color) { int dx abs(x1 - x0); int dy abs(y1 - y0); int sx (x0 x1) ? 1 : -1; // x 方向的步进方向 int sy (y0 y1) ? 1 : -1; // y 方向的步进方向 int err dx - dy; // 决策误差初始值 int x x0, y y0; while (x ! x1 || y ! y1) { pDC-SetPixel(x, y, color); int e2 2 * err; if (e2 -dy) { err - dy; x sx; } // 沿 x 方向走 if (e2 dx) { err dx; y sy; } // 沿 y 方向走 } pDC-SetPixel(x, y, color); // 补画终点 }这个版本和我早年抄的教科书版不一样它用了 sx 和 sy 两个方向变量所以八个象限的直线全都能处理而很多教材只推导了第一象限。err 的初始值是 dx - dy它代表当前像素点偏离理想直线的累积误差。每次循环先算 e2 2 * err当 e2 大于 -dy 时说明 x 方向必须走一步了当 e2 小于 dx 时说明 y 方向必须走一步。两个条件同时满足时x 和 y 都会步进正好对应斜率介于 -1 和 1 之外的对角线方向。理解和记忆这个算法的关键不在于背条件而在于明白 err 是一个「误差累积器」每走一步误差被另一轴的增量修正决策参数的正负号决定下一次往哪个方向迈步。DDA 是把理想直线方程直接采样Bresenham 是把误差项凑成整数迭代两者最后画出来的像素集合完全一致但 Bresenham 在现代 CPU 上几乎没有浮点开销。如果你在跑实验时发现直线有断点优先查两点第一while 循环结束后终点像素是否补画了第二dx 和 dy 用的绝对值但步进方向有没有跟着起止点的相对位置变化。这套源码里 Bresenham 的封装一般会带一个 Viewport 转换步骤调试时先去掉转换直接传入窗口坐标。3.3 扫描线多边形填充活性边表AET是核心数据结构多边形填充比画线复杂一个量级难点在于怎么高效地知道每条水平扫描线和多边形的哪些边相交、交点的 x 坐标是多少。教科书上的经典方案是「扫描线 活性边表」老孔这套源码里正是用结构体链表实现的下面的数据结构就是全套代码的骨架。typedef struct Edge { int yUpper; // 边所在的最大扫描线 y 值 float xIntersect; // 当前扫描线与边的交点 x 坐标 float dxPerScan; // 每越过一条扫描线x 的增量1/斜率 struct Edge* next; // 链表指针 } Edge; typedef struct EdgeTableEntry // 边的桶表结点 { int yScan; // 扫描线 y 值 Edge* edges; // 该扫描线对应的边链表 struct EdgeTableEntry* next; } EdgeTableEntry;扫描线填充的完整流程分五步第一步把多边形的每条边按其 y 坐标拆进一个「边桶表」ET每条边从它的 yMin 开始挂到对应的桶里第二步从最小的扫描线 y 值开始把当前扫描线对应的边从 ET 挪进「活性边表」AET第三步对 AET 里的边按 xIntersect 排序然后两两配对配对区间内的像素全部填充第四步处理完当前扫描线后把 yUpper 等于当前 y 的边从 AET 中删除说明这条边已经走完了第五步剩下的边更新 xIntersect加上 dxPerScan进入下一条扫描线。// 核心填充循环的骨架结构 void ScanLineFill(CDC* pDC, EdgeTableEntry* et) { int y et-yScan; // 从第一条扫描线开始 Edge* aet NULL; // 初始活性边表为空 while (et ! NULL || aet ! NULL) { // 1. 把 y 对应的边从 ET 加入 AET // 2. 按 xIntersect 对 AET 排序 // 3. 两两配对在配对区间内用 SetPixel 填色 // 4. 删除 yUpper 等于当前 y 的边 // 5. xIntersect dxPerScany 进入下一行 } }这套骨架在实际跑的时候有三个容易翻车的点。第一配对规则必须是「奇偶配对」即交点按 x 排序后第 0 和第 1 个配一对、第 2 和第 3 个配一对不能乱配否则奇偶规则失效填充区域会错乱。第二当扫描线恰好穿过多边形顶点时交点的计数要特殊处理——常规做法是让低端点参与计数、高端点不参与即「左闭右开」的 y 区间原则否则顶点会被重复计数导致配对错位。第三水平边yUpper 等于 yMin 的边必须跳过不能加入桶表否则它的交点是整条线段配对会直接崩掉。这套源码比直接看教材伪代码强的地方在于你可以对多边形的顶点连续打断点观察 ET 和 AET 里链表结点的增减把「抽象的数据结构」变成「运行时的对象」。我当时就是逐行跟踪 AET 在每一条扫描线上的变化才真正理解了为什么斜率负数的边 dxPerScan 是负数以及为什么排序必须按 xIntersect 而不是按斜率。4. 三维变换与透视投影4x4 矩阵的每一列到底在干什么4.1 齐次坐标与矩阵组装平移为什么能写进矩阵二维图形学的变换是 3x3 矩阵三维变换是 4x4 矩阵核心原因是「齐次坐标」把原来的三维坐标 (x, y, z) 扩充成四维 (x, y, z, w)w 通常取 1。这样做的直接好处是让平移变成线性变换能和其他矩阵统一写成乘法。如果没有齐次坐标平移只能写成加法旋转缩放是乘法两类变换没法合成一个矩阵多次变换就得反复判断非常笨拙。typedef struct { float m[4][4]; // 行主序存储M[0] 为第一行 } Matrix4x4; // 生成平移矩阵 void BuildTranslate(Matrix4x4 mat, float tx, float ty, float tz) { memset(mat, 0, sizeof(mat)); mat.m[0][0] mat.m[1][1] mat.m[2][2] mat.m[3][3] 1.0f; // 单位阵 mat.m[0][3] tx; // 平移量放在第 0 行第 3 列 mat.m[1][3] ty; mat.m[2][3] tz; } // 矩阵乘法result a * b void MatrixMultiply(Matrix4x4 result, const Matrix4x4 a, const Matrix4x4 b) { Matrix4x4 tmp; for (int i 0; i 4; i) for (int j 0; j 4; j) { tmp.m[i][j] a.m[i][0] * b.m[0][j] a.m[i][1] * b.m[1][j] a.m[i][2] * b.m[2][j] a.m[i][3] * b.m[3][j]; } result tmp; }这里有一个绕不开的坑MFC 教学代码里普遍用「行向量左乘矩阵」的约定即变换后的坐标 原坐标向量 × 变换矩阵而 OpenGL 和大多数图形学库用「列向量右乘」即变换后的坐标 变换矩阵 × 原坐标向量。两种约定的矩阵在内存里互为转置。如果你把 MFC 里的矩阵直接搬到 Shader 里旋转方向会反。我见过很多人在实验里发现物体绕 z 轴旋转的方向和预期相反最后查出来是矩阵转置问题。组装复合变换的顺序也有讲究。比如「先旋转再平移」对应矩阵是 T × R应用时先乘 R 再乘 T写成行向量形式就是 v v × R × T注意顺序不能颠倒。图形学里所有变换顺序的争议到矩阵乘法这一层就全部归一为「怎么乘都对但顺序写反了全错」。4.2 透视投影矩阵的参数fov、aspect、近裁剪面、远裁剪面投影是三维图形学里最像「玄学」的一步因为参数只要错一个物体要么看不见要么被拉伸变形。老孔这套源码里的透视投影用到了和 OpenGL 近似的矩阵结构参数一般有四个垂直视角 fovY、宽高比 aspect、近裁剪面 znear、远裁剪面 zfar。// 构建透视投影矩阵列主序风格与 OpenGL 约定一致 void BuildPerspective(Matrix4x4 mat, float fovY, float aspect, float znear, float zfar) { float f 1.0f / tanf(fovY * 0.5f * 3.14159265f / 180.0f); memset(mat, 0, sizeof(mat)); mat.m[0][0] f / aspect; mat.m[1][1] f; mat.m[2][2] (zfar znear) / (znear - zfar); mat.m[2][3] (2.0f * zfar * znear) / (znear - zfar); mat.m[3][2] -1.0f; // 让 w 分量承载 -z }这组参数的含义直接决定画面效果。fovY 是垂直方向的可视角60 度是人眼的舒适范围90 度以上会有明显的广角畸变太小则物体显得很平像用了长焦镜头。aspect 就是窗口宽高比宽视口用 800 除以 600 得到 1.33如果这个值不匹配窗口实际宽高比圆会变成椭圆。znear 和 zfar 不是「画的远近距离」而是「深度缓冲的裁剪范围」近裁剪面不宜设得太小太小会导致近处物体深度精度不足出现闪烁。实际调试时有个经验如果物体「穿过了相机」却看不见了大概率是 znear 太大把物体裁掉了如果物体缩放成为一个小点检查 fovY 是不是设成了角度制却被三角函数按弧度处理。MFC 的数学库里没有内置 sin、cos 的角度制版本所有角度在调用数学函数前必须乘上 π/180。4.3 从模型坐标到屏幕坐标顶点流水线的完整顺序把这一步理顺三维实验的所有代码都能串起来了。一个顶点从定义到显示在屏幕上要依次经过五个变换模型变换模型自身的旋转缩放平移、视图变换把相机放到原点物体放到相机前方、投影变换透视或正交得到裁剪空间坐标、透视除法把齐次坐标的 w 除回去得到规范化设备坐标、视口变换把规范化坐标映射到窗口像素坐标。// 完整的顶点变换流程示意 Vector4 TransformVertex(const Vector4 vertex, const Matrix4x4 model, const Matrix4x4 view, const Matrix4x4 proj, int viewportW, int viewportH) { Vector4 clip vertex * model * view * proj; // 行向量左乘 // 透视除法齐次坐标转规范化设备坐标 float w clip.w; Vector4 ndc { clip.x / w, clip.y / w, clip.z / w, 1.0f }; // 视口变换[-1,1] 映射到 [0,width] 和 [0,height] float screenX (ndc.x 1.0f) * 0.5f * viewportW; float screenY (1.0f - ndc.y) * 0.5f * viewportH; // 注意 y 翻转 return { screenX, screenY, ndc.z, w }; }这段代码里有一个新手必踩的坑屏幕坐标系的 y 轴向下而数学坐标系的 y 轴向上所以视口变换里要做1.0f - ndc.y这个翻转动作。如果不翻转立方体画出来是上下颠倒的。老孔源码里有的实验直接做了这个翻转有的把翻转留到了 OnDraw 的设备坐标里读代码时要分清「算法坐标」和「屏幕坐标」两套体系。我最开始跑旋转立方体实验时对着代码看了半天发现每个顶点都经过了 model → view → proj 的矩阵链却忽略了 MFC 的窗口逻辑坐标本来就是 y 向下。这套源码的调试价值恰好在这里你能在 TransformVertex 的返回处打断点对比顶点在进入流水线前后的坐标值把「为什么 w 会变成负数」「为什么矩阵乘完坐标跑到屏幕外面去了」这类黑匣子问题一次性解开。5. 避坑与排查跑这套 MFC 源码时最容易翻车的五个位置5.1 编译链接期报错LNK2019 无法解析的外部符号现象链接时报LNK2019: unresolved external symbol public: void __thiscall CDrawView::DrawLineBresenham(...)但代码里明明写了这个函数。原因分成两类一类是算法函数写了声明和定义但对应的 .cpp 文件没有被包含进工程另一类是字符集不一致导致 MFC 库函数的重载符号没匹配上。解决先确认报错符号对应的 .cpp 在工程文件列表里右键工程 → 添加 → 现有项再检查字符集是否为多字节。按我的经验八个工程里至少一个会犯这个错。5.2 运行白屏图形只画了一次窗口刷新就消失现象程序编译通过点击菜单或按钮后图形正常显示但窗口被拖拽、遮挡后图形全部消失只剩一片灰色背景。原因绘制代码写在菜单响应函数或鼠标点击事件里直接往窗口 DC 上画而窗口重绘时 OnDraw 里是空的。MFC 的机制是「窗口任何一次重绘都会完整重建客户区内容」不把绘制逻辑放进 OnDraw图形就是一次性的。解决在响应函数里修改数据成员然后调用Invalidate()触发重绘让 OnDraw 统一完成绘制。从那以后我写任何 MFC 绘图程序都强制自己遵守这个规则数据变更只改变量绘制永远归 OnDraw 管。5.3 图形上下颠倒屏幕坐标系和数学坐标系的 y 轴方向不一致现象画三角形时顶点坐标明明是正的显示出来却在窗口底部。原因数学坐标系 y 向上屏幕坐标系 y 向下MFC 的窗口逻辑坐标默认 y 向下。解决在视口变换或 OnDraw 中对 y 做一次翻转。我见过有人把翻转硬编码在顶点数据里结果物体一旋转就乱套正确的做法是在流水线最后一步统一处理让算法代码保持标准的数学坐标系只在输出到屏幕前翻转。5.4 斜线断断续续Bresenham 只处理了斜率绝对值小于等于 1 的象限现象画一条从 (10, 10) 到 (10, 200) 的竖线线条完整画一条从 (10, 10) 到 (200, 100) 的斜线中间出现明显断点。原因教科书版本为了推导方便只讨论 0 到 1 斜率的情形主循环里固定用 x 作为步进方向y 作为误差修正方向当斜率大于 1 时x 每次只走一格y 来不及走像素点稀疏。解决换成上面 3.2 节里带 sx、sy 的全象限版本或者先判断 |dx| |dy|若成立则交换 x 和 y 的角色让长轴始终作为步进方向。5.5 资源编辑器打不开.aps 缓存和 .rc 文件不同步现象双击 .rc 文件资源编辑器卡死或直接崩溃或者对话框上拖进去的控件保存后重新打开不见了。原因.aps 是资源脚本的二进制缓存Visual Studio 在某些异常退出后没有把缓存同步回 .rc 文件旧缓存和新工程信息冲突。解决关闭工程找到工程目录下所有 .aps 文件删除重新打开工程资源编辑器会依据 .rc 自动重建缓存。这是最典型的一个「看似玄学、实则缓存」的问题我处理老 MFC 工程已经养成肌肉记忆解压后第一件事就是把 .aps 全部删掉。6. 二次改造把源码变成你自己的图形学实验台如果有人问这套源码最好的用法是什么我的答案不是逐章跑完而是把它改造成自己的实验框架。跑通一个算法只是起点能让你在上面快速验证新想法才是价值所在。这里给出三个我实际改过的方向每个都能直接落地。第一个改造点是双缓冲解决画面闪烁。MFC 的 OnDraw 里每画一个像素就调用一次 SetPixel批量绘制时窗口闪烁非常严重。改进方式是先在内存里创建一块兼容位图把图案全部画到内存 DC 上最后一次性 BitBlt 到窗口 DCvoid CMyView::OnDraw(CDC* pDC) { CDC memDC; CBitmap memBitmap; memDC.CreateCompatibleDC(pDC); memBitmap.CreateCompatibleBitmap(pDC, m_clientWidth, m_clientHeight); CBitmap* pOldBitmap memDC.SelectObject(memBitmap); // 把原先画在 pDC 上的代码全部改用 memDC DrawScene(memDC); // 一次性拷贝到窗口 pDC-BitBlt(0, 0, m_clientWidth, m_clientHeight, memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOldBitmap); }第二个改造点是状态栏显示实测中我用得最多。图形学实验经常需要看鼠标坐标、当前算法名、渲染耗时写到 OutputDebugString 里查看麻烦弹 MessageBox 又打断流程。最顺手的是用 MFC 状态栏在 MainFrame 里获取状态栏指针然后在视图类响应鼠标移动时把坐标格式化后写进状态栏窗格void CMyView::OnMouseMove(UINT nFlags, CPoint point) { CString str; str.Format(_T(坐标: (%d, %d)), point.x, point.y); ((CMainFrame*)AfxGetMainWnd())-SetStatusText(str); CView::OnMouseMove(nFlags, point); }第三个改造点是把软件渲染替换成 OpenGL适合想从「算法学习」过渡到「工程实践」的阶段。做法是把算法类保留原封不动只替换输出层原来调用 SetPixel 的地方改成往内存像素缓冲区写颜色值然后用 glTexImage2D 把这个缓冲区作为纹理上传到 GPU最后画一个全屏四边形显示出来。这样一来Bresenham 画线、扫描线填充的结果仍然由你自己写的光栅化代码生成但呈现效果和性能由 GPU 接管等于给自己的软件渲染器加了一个显示器。这套源码我前后完整读过两遍。第一遍是照着跑实验第二遍是把每个算法单独抽出来改造成上面的框架才真正理解了为什么书上要用活性边表而不是直接求交。从那以后我每次拿到任何图形学或图像处理的源码包第一件事永远是先删缓存、改字符集、跑通基线再谈读代码和改造——这套流程在 MFC 源码上验证了千百遍从来没翻过车。希望这套源码也能成为你图形学路上的垫脚石帮你把课本上的公式变成屏幕上的像素。本文还有配套的精品资源点击获取