简介:吉林大学计算机图形学课程实验完整源码包,面向选修该课程或需要MFC图形编程参考的在校生与自学者。项目基于Visual C++开发,实现菜单驱动的交互绘图:支持鼠标绘制矩形与圆形,可通过对话框分别设置RGB颜色分量;绘制多边形时采用边标志算法完成内部填充,并按学号后四位数字填充。压缩包共30个文件,主体为8个cpp源文件与9个头文件,辅以MFC工程配置文件(dsp/dsw/clw/opt/plg)及界面资源(ico/bmp/rc),整体仅65KB,轻量便于快速编译。代码结构清晰,包含GDemo主程序、Graphics图形实现、Line绘制模块和DiaInput输入对话框等模块,可直接运行并作为实验参考或二次开发起点。目前已有1461人学习下载,是吉林大学图形学实验的高频参考资料。 前阵子把吉林大学计算机图形学课程那套实验源代码翻出来重新过了一遍,顺便帮几个学弟学妹做了代码走查和调试。说实话,这套代码放在今天看依然很有代表性——它几乎覆盖了图形学入门阶段所有绕不开的经典算法和工程细节,从最基础的光栅化直线、多边形填充,到裁剪、变换、曲线曲面,再到简单三维显示,整个链路非常完整。如果你正在学图形学、或者准备期末实验但不知道从哪里下手,这篇文章就是按我的理解把整套实验源代码拆开揉碎讲清楚:每一部分在实际代码里长什么样、有哪些容易踩的坑、拿到代码之后该怎么改怎么跑,全部基于我实际复现和调试的经验来写。
1. 实验体系与源码结构解析
1.1 课程实验都覆盖了哪些内容
吉林大学计算机图形学课程的实验设置,基本遵循国内高校图形学教学的主流路线:先解决“怎么把点、线、面画到屏幕上”的底层光栅化问题,再进入几何处理和高级渲染。整套源代码对应的实验通常包括下面几个模块:
- OpenGL环境搭建实验:配置窗口、初始化渲染上下文,用OpenGL画一个最基本的三角形或矩形,理解图形管线的起点。
- 直线生成算法实验:实现DDA和Bresenham两种直线扫描转换算法,在像素网格上输出线段。
- 圆和椭圆生成实验:基于中点画圆算法或Bresenham类算法,生成圆弧并对称扩展成整圆。
- 多边形填充实验:实现扫描线填充算法或种子填充算法,对一个闭合多变边形区域着色。
- 二维裁剪实验:用Cohen-Sutherland编码裁剪算法,把直线超过窗口边界的部分裁掉。
- 几何变换实验:实现平移、旋转、缩放以及复合变换,通常用齐次坐标矩阵完成。
- 曲线曲面实验:实现Bezier曲线、B样条曲线的绘制,部分版本还要求生成旋转曲面。
- 三维显示基础实验:简单立方体或球体的线框绘制,加上正交/透视投影。
- 光照模型实验(进阶):利用Phong或Gouraud光照模型给三维物体着色。
说实话,初学者拿到这套代码时最迷茫的不是某个算法看不懂,而是不知道每个实验的代码在项目中负责哪一段,改了某个变量到底会影响到哪里。下面我把源码的整体组织方式先讲清楚。
1.2 源码整体架构与文件组织方式
这套源代码在组织上有一个明显特点:它按实验编号建立文件夹,每个文件夹内通常包含一个独立的工程文件(比如.dsp或.vcxproj)、若干个.cpp源文件、对应的头文件,以及一些情况下附带的实验报告或说明文档。典型的目录结构大概是:
ComputerGraphics_Lab/ Lab1_LineDrawing/ line_drawing.sln main.cpp line_algo.cpp line_algo.h Lab2_CircleFill/ circle_fill.sln main.cpp fill_algo.cpp Lab3_ClipTransform/ main.cpp clip.cpp transform.cpp ...这样的结构对学习者很友好——每个实验是独立工程,不互相依赖,改坏了也不会影响其他部分。我在复现时习惯先整体编译一遍,确认每个工程都能通过,再逐个实验去推演代码逻辑。如果你用的编辑器或IDE版本比较新(比如Visual Studio 2019/2022),打开旧工程文件时先做一次“配置升级”,通常会自动迁移到新工具集,问题不大。
另外一个需要注意的点是,这套实验源码大部分基于固定功能管线(Fixed-Function Pipeline),也就是直接使用glBegin/glEnd、glVertex、glColor这类老式OpenGL API。这跟现代OpenGL的可编程管线(Shader)有本质差别,但作为理解光栅化、变换、裁剪原理的教材,固定管线其实“可见性”更强,每一步操作都能直观对应到课堂理论。下面进入核心算法拆解。
2. 核心技术点拆解:从直线到填充
2.1 直线生成算法:DDA与Bresenham的选型逻辑
直线扫描转换是图形学的“第一个坎”。实验代码里通常会同时给出DDA和Bresenham两种实现,但实际绘制效果和性能差异很大,弄清楚为什么需要两种算法,比背代码重要得多。
DDA(Digital Differential Analyzer)的思路很直白:已知起点和终点坐标,计算斜率,然后按单位x或单位y步进,每次把计算结果四舍五入取整。这种思路的缺点是涉及浮点数运算和取整操作,循环规模一大就会有性能损失,而且浮点累计误差可能导致直线在长距离绘制时出现断点或偏移。Bresenham算法的核心改进是“只用整数计算”地维护一个误差项,每走一步根据误差项决定下一个像素的位置,避免了浮点运算。代码实现通常是这样:
void BresenhamLine(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 (true) { 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; } } }理解这段代码的关键在于那个err变量——它本质上是在判断“当前像素点离理想直线更近的区域偏向x方向还是y方向”。如果你只是想把实验跑通,把代码复制进去就能画线;如果你想真正理解它,建议你手动推导一遍dx、dy、err的演变过程,这个过程对后续理解圆和椭圆的生成算法帮助很大。
2.2 中点画圆与多边形填充的实现细节
圆生成实验里,最经典的是“中点画圆”算法。它的思路是把圆按八对称性分成8个部分,只计算第一象限内45度弧线上的点,然后通过对称映射生成整圆。实现时维护一个判别式d,每一步判断下一个点是选正右方像素还是右下方像素。判别式的更新同样只用整数加减法,这是图形学算法“计算效率优先”的典型体现。
多边形填充实验则稍微复杂些。扫描线填充的核心步骤是:对多边形的每一条边建立边表,按y坐标排序,然后逐条扫描线计算交点、配对、填空。实现中最容易出错的地方是“交点计数”的边界处理——当扫描线恰好经过顶点时,需要判断这个顶点是局部极大值还是局部极小值,处理不当会出现填充区域边缘出现毛刺或者漏填一行。我看很多同学的代码在这个问题上都出过bug,排查方法也很直接:在关键的扫描线y值处打印出计算得到的交点列表,人工核对一下是否成对出现。
种子填充的思路和扫描线完全不同,它从一个已知在多边形内部的像素出发,用四连通或八连通搜索方式向外蔓延填充。实验里通常要求实现递归版本,但递归过深会造成栈溢出,实际工程中改成显式栈的迭代版本更稳妥。这个点虽然不一定写在实验报告里,但在检查代码时如果被问到“为什么不用递归”,你最好能答上来。
2.3 裁剪与几何变换的数学底层
二维裁剪实验里的Cohen-Sutherland算法,核心在于给窗口边界建立一个4位编码:上下左右分别对应一位。任意线段端点都可以算出一个编码,如果两个端点编码按位与不为0,说明线段整体在窗口外,可以直接舍弃;如果按位或为0,说明两个端点都在窗口内,直接保留;否则就求交点并拆分线段。这套逻辑用代码实现并不复杂,但考试和面试时经常要求你写出编码表和求交公式,建议把4位编码的分配规则记熟。
几何变换实验考察的则是对齐次坐标的理解。为什么要用齐次坐标?因为平移在普通二维坐标下无法写成矩阵乘法,只有把点表示成(x, y, 1),才能把平移、旋转、缩放统一成3x3矩阵的乘法形式。实验代码里通常会提供这样的矩阵定义:
typedef float Matrix3x3[3][3]; void translationMatrix(float tx, float ty, Matrix3x3 m) { memset(m, 0, sizeof(float) * 9); m[0][0] = m[1][1] = m[2][2] = 1.0f; m[0][2] = tx; m[1][2] = ty; }这里有一个非常容易搞反的坑:多个变换复合时,代码里如果使用列向量约定,那么变换矩阵要依次右乘;但实验代码里有些版本用的却是行向量约定,矩阵要左乘。你不把矩阵乘法的顺序理清楚,旋转加平移的结果经常会跑到意料之外的位置,调试时还以为是随机错误。拿到代码后,先确定它用的是“向量左乘矩阵”还是“矩阵左乘向量”,后面所有变换逻辑的阅读理解都会顺畅很多。
3. 核心环境配置与编译运行实操
3.1 Windows下OpenGL/GLUT环境搭建
这套源代码在Windows平台上通常默认依赖GLUT或FreeGLUT库。GLUT是OpenGL的辅助工具库,负责创建窗口、处理键盘鼠标事件、管理菜单等,图形学课程实验用它来写交互程序非常方便。但新装的Windows系统不会自带这个库,需要自己下载并正确配置,很多同学就是卡在这一步。
我的建议直接使用FreeGLUT替代老版GLUT,接口基本一致,但持续维护、兼容性好,64位环境也能正常编译。配置步骤分三步:把freeglut的头文件放到编译器include目录,把freeglut.lib放到库目录,把freeglut.dll复制到可执行文件目录。在Visual Studio里,还要在“链接器-输入-附加依赖项”里手动加上freeglut.lib、opengl32.lib、glu32.lib和glut.lib(如果源码里包含了glut初始化调用)。如果少了某个lib,报错通常是“无法解析的外部符号”,这时候对照这几个lib逐个检查。
还有个小细节:老版本的GLUT在窗口关闭时会调用glutMainLoop内部的消息循环,如果你的代码在现代操作系统上运行时出现“窗口一闪而过”,大概率是没有正确进入消息循环,或者初始化的像素格式不匹配。检查一下glutInitDisplayMode是否包含了GLUT_DOUBLE,双击缓冲可以避免刷新闪烁。
3.2 Linux/macOS下的替代方案
如果你用的是Linux或macOS,大概率不能直接编译Windows平台那份工程文件。我的处理方法是新建一个CMake工程,只把源代码里的.cpp和.h文件复制过来,再自己写一个简单的main函数来初始化GLUT。Linux上安装依赖一条命令搞定:
sudo apt-get install freeglut3-devmacOS上系统自带的OpenGL框架是过时版本,但还是能支持GLUT的,只是需要额外链接框架:
clang main.cpp -o lab -framework OpenGL -framework GLUT遇到编译报错时,很多问题出在头文件包含路径不一样。Windows习惯写#include <GL/glut.h>,Linux上的路径也基本一致,但macOS有时候要写成#include <GLUT/glut.h>,或者反过来。直接把头文件包含方式改成两种都兼容的写法,最省事:
#ifdef __APPLE__ #include <GLUT/glut.h> #else #include <GL/glut.h> #endif这套代码跨平台编译的坑基本集中在环境差异,算法本身跟平台无关,不会因为换了操作系统就跑出不同结果。
3.3 我把整套源码编译通过后的运行路径
实际编译运行时,我一般按下面这个顺序执行,能最大化避免“花了半天调环境、算法没时间看”的尴尬:
- 每个实验工程先编译一遍,记录下所有报错信息,按“头文件缺失-库缺失-语法错误”分类处理。
- 编译通过后,先不运行,直接读一遍main函数,找到glutDisplayFunc注册的回调函数,那里是整个图形的入口。
- 运行一遍,观察输出窗口里的图形是否符合预期,比如画线实验应该能看到一条平滑的像素直线、填充实验应该看到颜色均匀的多边形。
- 手动修改几个关键参数(比如Bresenham直线的端点坐标、裁剪窗口大小),重新编译看效果变化。
这套流程用下来,整套代码的“可运行性”问题基本能在一个小时内解决,剩下的时间全部留给算法理解。
4. 实验代码中的常见问题与排查技巧实录
4.1 像素离散化带来的锯齿问题
画直线和画圆实验中,很多同学发现输出的线段边缘是阶梯状的,也就是俗称的“锯齿”。这其实是光栅化的自然结果:屏幕像素本身就是离散网格,把连续直线映射到网格上,一定会出现阶梯。这不是代码写错了,而是理论上就无法避免的。实验采用的Bresenham算法已经是一种“视觉效果最优”的整数近似方案,想进一步改善,可以用抗锯齿(Anti-Aliasing)技术,比如多采样、灰度覆盖等。
代码层面需要检查的另一个问题是setPixel函数的坐标方向。图形学中习惯把窗口左下角设为坐标原点,但Windows窗口客户区的默认坐标系是左上角为原点、y轴向下,如果不做坐标转换,画出来的图形会出现上下翻转。实验源码通常已经在窗口重绘回调gluOrtho2D或glViewport里处理了这个映射,但你在自行添加交互代码时很容易忽略,导致点击鼠标定位和图形位置对不上。遇到这类奇怪现象,第一步就查坐标系的转换代码。
4.2 矩阵变换顺序导致的几何位置异常
几何变换实验是最容易出“玄学bug”的地方。我见过最常见的错误是:想实现“先旋转再平移”,代码里却把旋转和平移矩阵的乘法顺序写反了,结果物体跑到屏幕外面去。如果你的代码里定义了RotateMatrix和TranslateMatrix,复合变换时一定要搞清楚当前向量是“行向量”还是“列向量”。假设是列向量约定,变换顺序从右往左读:M = T * R,表示先旋转后平移;如果是行向量约定,M = R * T,表示先旋转后平移。同一段代码在不同版本教材里写法可能相反,最好的办法是写一个单点测试,比如把(1,0,1)这个点先旋转90度再平移,打印出变换后的坐标,一目了然。
另一个常见问题是窗口比例导致图形被拉伸。默认的视口可能是正方形,但你创建的窗口是长方形的,没有设置等比的投影矩阵时,圆形就会显示成椭圆。解决方案是设置gluOrtho2D时根据窗口宽高比调整左右边界,或者在glutReshapeFunc回调里动态更新glViewport,保持视口的宽高比和窗口一致。
4.3 三维显示与光照实验中的深度测试问题
到了三维显示和光照实验,代码复杂度明显上升。很多同学发现线框立方体旋转时,应该被遮挡的背面线条也显示出来了,这是因为没有开启深度测试。需要在初始化时加入:
glEnable(GL_DEPTH_TEST);同时,在每次绘制前还需要清空深度缓冲区,否则上一帧的深度信息会保留下来,导致新帧的图形出现不正常的遮挡效果:
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);光照实验里还要注意法向量的计算。用GLUT自带的几何体(如glutSolidSphere)时,法向量是自动生成的;但如果你自己用顶点数组绘制一个立方体或者茶壶,就必须手动为每个面或每个顶点指定法向量,否则即使开启了GL_LIGHTING,物体看起来也是全黑或者亮度怪异。这个问题我在检查代码时几乎每次都能遇到,属于高发bug。
4.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 图形上下颠倒 | 坐标系y轴方向不一致 | 检查投影矩阵和窗口坐标转换 |
| 图形被拉伸变形 | 视口宽高比与投影矩阵不匹配 | 调整glViewport或gluOrtho2D参数 |
| 图形闪烁严重 | 单缓冲模式导致重绘频繁 | 改用GLUT_DOUBLE双缓冲 |
| 程序一启动就崩溃 | GLUT或OpenGL版本不匹配 | 确认freeglut.dll和lib版本对应 |
| 直线出现断点 | 浮点取整误差或算法边界条件错误 | 手动推导Bresenham误差项 |
| 三维物体不显示 | 未设置投影矩阵或深度测试未开启 | 检查glMatrixMode和glEnable调用 |
4.5 几个独家避坑技巧
在整套代码调试过程中,我觉得最有价值的几个经验,可能不在任何实验指导书中,但能帮你在实践中省下大量时间。第一,在main函数入口添加glutInit版本检查,输出当前OpenGL版本,兼容性问题在第一时间暴露;第二,把每个算法模块(比如Bresenham直线、扫描线填充)单独封装成独立函数,用printf或写入文件的方式输出中间坐标数据,用第三方工具可视化验证;第三,尽量坚持使用double而不是float保存坐标计算结果,尤其是在做复合变换时时,float的精度不足会让旋转多次后的物体出现明显的“漂移”。
5. 这套源码的扩展方向与学习建议
5.1 如何把实验代码扩展成一个小型渲染器
把这套源码全部跑通之后,很多人的第一反应是“接下来怎么办”。我的建议是:不要急着追新框架,先在已有代码基础上做三个有价值的扩展。第一个扩展是把直线的Bresenham算法推广到任意斜率以及更复杂的线型(虚线、点划线),这是对光栅化理解的巩固;第二个扩展是把二维裁剪代码升级为Liang-Barsky算法,跟Cohen-Sutherland对比性能差异,并把这个算法整合到几何变换中,实现一个真正的“窗口-视区”映射系统;第三个扩展是给三维实验加入简单的键盘控摄像机逻辑,让视点可以前后左右移动,这能帮你建立起三维空间感和矩阵变化感知。
5.2 从固定管线到可编程管线的过渡
如果你希望把电脑图形学的知识延伸到现代3D渲染方向,这套源代码还有一个非常好的作用:它是学习现代OpenGL的桥头堡。固定功能管线中的许多概念——矩阵堆栈、光照参数、烘焙好的变换——在可编程管线的Shader中都有对应物,但你需要自己用GLSL重新实现一遍。
我建议的路径是:先用这套代码把“一个顶点如何经过模型变换、视图变换、投影变换最终映射到屏幕窗口”这条链路彻底想清楚,然后找一份现代OpenGL的教程,用Shader重写二维直线画法和三角形旋转。你会发现,底层的数学原理完全一样,只是实现粒度变得更精细了。这套实验源代码里那些看似“老掉牙”的glBegin/glEnd代码,恰恰是理解现代GPU工作逻辑的最佳起点。
5.3 给初学者的学习路径建议
最后说点学习上的体会。图形学实验和纯编程作业最大的不同是:它同时考验你的数学功底、算法设计能力和工程调试能力。如果你发现自己卡在某个实验上,不要急着抄代码,先回到对应的教材章节,把矩阵乘法的推导、光栅化算法的判别式推导手算一遍。这套源代码最大的学习价值不是给你标准答案,而是给你一个可以反复修改、观察现象、验证直觉的“试验场”。我在调试Bresenham算法的时候,曾经把误差项改成不同的初始值去观察整条直线的形状变化,这种动手对比带来的理解深度,比单纯看书本结论要牢固得多。
如果你也在跑这套实验代码,遇到具体问题欢迎在评论区带上截图和代码片段交流。我整理了完整的调试笔记,后面也会陆续把每个实验的详细重构思路和踩坑记录单独写出来。
本文还有配套的精品资源,点击获取