尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
C++ OpenGL计算机图形学实验源代码:光栅化到Z-buffer
简介面向高校计算机图形学课程学习者与软件工程相关专业学生的一套实验源代码合集围绕吉林大学课程实验要求整理重点解决二维图元绘制、颜色控制与多边形填充等上机任务的实现问题。压缩包共30个文件以C源文件与头文件为主体辅以图标、位图、工程配置及说明文本完整保留MFC工程结构包体约65KB体积轻便便于本地编译与调试。源代码涵盖鼠标交互绘制矩形、圆形通过对话框分别设置RGB颜色分量以及以鼠标输入顶点构建多边形并采用边标志算法完成填充填充内容使用学号后四位数字能帮助读者对照实验要求理解图元生成、交互响应与扫描填充的实现思路。已有1463人学习适合需要参考完整工程组织与核心算法代码的课程学习者。1. 期末前三天的黑屏比任何原理都难debug一位学弟在群里发截图窗口是纯黑的而实验报告要求画一棵带光照的 Bezier 曲面树。他的代码编译通过运行不报错就是看不见东西。这种“编译成功但窗口全黑”的问题比算法推导更难排查没有一组能逐像素对照的参考实现你根本不知道是矩阵写反了还是深度测试没开。这套计算机图形学实验源代码就是为这类问题准备的某高校图形学课程从第一节直线画到最后一节光照曲面每个实验一个独立可运行的工程算法实现和可视化窗口放在一起。它适合两类人刚上图形学、需要一个能修改的基准的本科生以及想重刷一遍光栅化管线、顺手把这套代码改成自己的小渲染器的开发者。下面按“这套代码怎么组织 → 核心算法怎么写的 → 跑不起来怎么查 → 怎么改成自己的东西”这条路往下拆。2. 项目结构一套 C 实验工程是怎么拼起来的2.1 技术栈与依赖C、OpenGL 固定管线和一个手写的像素画笔整套代码的技术选型很“课程实验”语言是 C窗口与绘图用 OpenGL 搭配 GLUT/freeglut交互靠键盘回调界面几乎不涉及所有算法都从“往帧缓冲里写一个点”开始。这个选型的好处是透明它不会替你算也不帮你插值glBegin(GL_POINTS)里画出来什么样完全取决于你setPixel里的取整和误差项。必须说明的一点是这里面绝大多数实验刻意使用的是固定管线接口glVertex2i、glMatrixMode原因不难猜课程要你手写 Bresenham、手写扫描线填充如果换成现代的可编程管线第一节课就得先教 shader 和 VAO实验进度排不开。常见做法是把算法层和渲染层分开——算法算出像素坐标数组再用最笨的逐点绘制把结果贴到屏幕上这样对照检查时一眼能看出是算法错还是绘制错。依赖清单大致是这样的缺一样都会在编译期或链接期直接翻车依赖作用缺了会怎样C11 及以上编译器编译实验工程nullptr、auto报语法错OpenGL 开发库提供绘制上下文链接期undefined reference to glBeginfreeglut / GLUT窗口、消息循环、键盘回调能链接但窗口起不来CMake 3.10 或 IDE 工程文件组织多实验目录单文件能编整目录编不过数学库可选 GLM 或手写 Mat4矩阵、向量运算变换实验只能写死数值选择 freeglut是因为经典 GLUT 在很多新系统上已经装不上选择 CMake则只是因为一个仓库里躺着十几个实验每次新建工程文件太消耗时间。这两条选择都不是唯一解我一般会先确认能不能跑通一个最小窗口再谈别的。2.2 目录结构与实验编号的对应关系仓库按实验拆目录公共部分像素写入、矩阵工具、颜色定义抽到common/每个labXX_xxx/下有自己的main.cpp和算法实现。这种组织方式的价值在于你改第 4 个实验的裁剪代码不会连带把第 9 个实验的曲面渲染弄坏报告里要交的截图也是各归各的。cg-labs/ ├── CMakeLists.txt # 顶层构建脚本统一开 C11 ├── common/ │ ├── include/ │ │ ├── framebuffer.h # setPixel / clearScreen / savePPM │ │ ├── mat4.h # 4x4 矩阵与变换栈 │ │ └── vec.h # 点、向量、叉积、归一化 │ └── src/ # 上述头文件的实现 ├── lab01_dda_bresenham/ # 直线生成 ├── lab02_midpoint_circle/ # 中点画圆与椭圆 ├── lab03_scanline_fill/ # 多边形扫描线填充 ├── lab04_transform2d/ # 二维齐次变换与复合变换 ├── lab05_clipping/ # Cohen-Sutherland、Sutherland-Hodgman ├── lab06_bezier_bspline/ # 曲线求值与反求控制点 ├── lab07_projection3d/ # 三维观察与投影 └── lab08_zbuffer_lighting/ # 消隐、Phong 光照、纹理把编号和课程进度对齐是一种很实用的设计你可以只看lab05就交第五次实验不用把整条管线跑一遍。代价是每个实验都要自己初始化窗口重复代码多但初学者反而更容易看懂——所有前置条件都摆在同一个main.cpp里没有藏起来的框架魔法。2.3 从零跑通三条命令加一个最小验证拉下来之后建议先用命令行构建不要一上来就导入 IDE否则很容易把“工程配置问题”和“代码问题”混在一起。以 Linux 或 macOS 为例构建流程大致是# 1. 建构建目录避免产物污染源码树 mkdir -p build cd build # 2. 生成构建文件指定 Release 减少软件光栅化的卡顿 cmake .. -DCMAKE_BUILD_TYPERelease # 3. 只编第一个实验快速验证工具链 make lab01_dda_bresenham -j4 # 4. 运行窗口出现后按 d 切换 DDA / Bresenham 对比 ./lab01_dda_bresenhamWindows 下用对应生成器比如cmake .. -G MinGW Makefiles或直接开 VS 工程命令含义一样。这里的关键不是记命令而是第 3 步只编一个目标整仓一次编完要连十几个可执行文件中间任何一处头文件路径写错报错信息会刷满屏幕新手很难从中定位到真正原因。最小验证的判据只有一条窗口出现后屏幕上有一条从左上到右下的直线键盘能切换算法。满足这条说明 OpenGL 上下文、GLUT 回调、像素写入三段链路都通了再去碰后面的裁剪、曲面才有意义。窗口全黑就按第 5 章的排查顺序走别急着改算法。3. 光栅化三件套直线、圆、填充的代码拆解3.1 DDA 与 Bresenham把浮点误差赶出内层循环直线生成是整套代码里最值得逐行读的一段因为它把“计算机图形学为什么要用整数运算”这件事讲得很直白。DDA 用斜率递推每一列像素算一次y k实现简单但浮点累加误差会随长度积累长线末端会看到抖动Bresenham 把误差项放大成整数用加减法就能决定下一个像素往上还是往下走。// lab01Bresenham 全象限版本核心是整数误差项 err void drawLineBresenham(int x0, int y0, int x1, int y1) { int dx abs(x1 - x0), sx x0 x1 ? 1 : -1; // x 方向步长与符号 int dy -abs(y1 - y0), sy y0 y1 ? 1 : -1; // dy 取负后续统一做加法 int err dx dy; // 初始误差项 for (;;) { setPixel(x0, y0); // 落一个像素 if (x0 x1 y0 y1) break; // 到终点即收 int e2 2 * err; if (e2 dy) { err dy; x0 sx; } // 误差过半x 走一步 if (e2 dx) { err dx; y0 sy; } // 误差过半y 走一步 } }这段代码里几个量值得单独说。dx是水平跨度dy被刻意写成负值目的就是把“x 走一步误差减多少、y 走一步误差加多少”统一成同一种加法省掉内层循环里的符号判断这是 Bresenham 能全程整数运算的关键。err的初值dx dy相当于把起点放在第一段的中点e2 2 * err是避免除法的常见写法。两个if都用和而不是严格不等号决定了 45 度对角线附近像素是偏一个还是偏两个换成严格不等号会出现斜率恰好为 1 时只走一个方向的问题。对比 DDA 的实现逻辑更“直觉”但也更脆弱// lab01DDA浮点累加作为对照实现保留 void drawLineDDA(int x0, int y0, int x1, int y1) { int steps std::max(abs(x1 - x0), abs(y1 - y0)); // 以跨度大的轴定步数 if (steps 0) { setPixel(x0, y0); return; } double dx (x1 - x0) / double(steps); double dy (y1 - y0) / double(steps); double x x0, y y0; for (int i 0; i steps; i) { setPixel(int(std::round(x)), int(std::round(y))); // 必须四舍五入 x dx; y dy; } }steps取两轴跨度最大值保证每一步至少推进一个像素不会有空洞。round不能省直接截断会让 45 度斜线整体偏向左上。这两份实现放在同一个工程里按键盘切换能用肉眼看出误差累积的差别——这也是这套代码最实用的地方之一算法的优劣不用背看屏幕就行。参数上唯一需要你改的是坐标范围多数实验用 0 到窗口宽高如果你的窗口是 800×600坐标超出就被裁掉画面上表现为“线跑出屏幕一半”这不是算法问题。3.2 中点画圆判别式 d 与八分对称圆的实验代码短但最容易在报告里被问“为什么只算八分之一”。原因是圆对 4 条对称轴共 8 个对称位置算出一个八分弧上的点用plot8一次写 8 个像素计算量直接降到原来的八分之一。// 一次写 8 个对称点cx/cy 为圆心x/y 为第一象限弧上的偏移 void plot8(int cx, int cy, int x, int y) { setPixel(cx x, cy y); setPixel(cx - x, cy y); setPixel(cx x, cy - y); setPixel(cx - x, cy - y); setPixel(cx y, cy x); setPixel(cx - y, cy x); setPixel(cx y, cy - x); setPixel(cx - y, cy - x); } void midpointCircle(int cx, int cy, int r) { int x 0, y r; int d 1 - r; // 判别式初值 d0 1 - r while (x y) { // 只走第二个八分圆 plot8(cx, cy, x, y); if (d 0) { // 中点在圆内选正右方像素 d 2 * x 3; } else { // 中点在圆外选右下方像素 d 2 * (x - y) 5; --y; } x; } }判别式初值1 - r是拿第一个中点 (1, r-0.5) 代入圆方程化简出来的结果d 0说明中点在圆内下一步该往右走否则往右下走并让y减一。两个增量2x3和2(x-y)5是判别式在 x 递增后的差分形式所以循环体内没有任何乘法。循环条件x y保证只画到 45 度分界越过之后八个对称点会重复。如果你的圆在斜 45 度方向出现缺口八成是把循环条件写成了x r。椭圆实现在同一目录下判别式换成两段上半段以 x 为主步进下半段以 y 为主步进分界点是切线斜率为 -1 的位置公式里用b²x和a²y比较。改参数时注意a、b是半轴长而不是直径写错会让椭圆整体缩小一半。3.3 扫描线填充活性边表怎么维护多边形填充是这套代码里最长的一个实验也是面试和答辩最爱问的点。核心结构是边表 ET 和活性边表 AETET 按扫描线 y 存“从这条线开始生效的边”AET 存“当前扫描线真正穿过的边”每推进一条扫描线就把新边并进来、把过期边删掉、按交点 x 排序然后两两配对填充区间。// 边结构x 为当前扫描线下的交点ymax 为边的上端点dxdy 为斜率倒数 struct Edge { double x; int ymax; double dxdy; }; void scanlineFill(const std::vectorPoint poly) { int ymin poly[0].y, ymax poly[0].y; for (auto p : poly) { ymin std::min(ymin, p.y); ymax std::max(ymax, p.y); } std::vectorstd::vectorEdge ET(ymax - ymin 1); // 按扫描线建表 for (size_t i 0; i poly.size(); i) { Point a poly[i], b poly[(i 1) % poly.size()]; if (a.y b.y) continue; // 水平边不入表 if (a.y b.y) std::swap(a, b); // 统一成下端点在前 double dxdy double(b.x - a.x) / double(b.y - a.y); ET[a.y - ymin].push_back({double(a.x), b.y, dxdy}); } std::vectorEdge AET; for (int y ymin; y ymax; y) { for (auto e : ET[y - ymin]) AET.push_back(e); // 新边加入 AET.erase(std::remove_if(AET.begin(), AET.end(), [y](const Edge e) { return e.ymax y; }), AET.end()); // 过期移除 std::sort(AET.begin(), AET.end(), [](const Edge a, const Edge b) { return a.x b.x; }); for (size_t i 0; i 1 AET.size(); i 2) { // 两两配对填区间 int xs int(std::ceil(AET[i].x)), xe int(std::floor(AET[i 1].x)); for (int x xs; x xe; x) setPixel(x, y); } for (auto e : AET) e.x e.dxdy; // 推进到下一条扫描线 } }几个容易出错的细节都在这里了。a.y b.y的水平边必须跳过否则会在配对时留下落单交点表现为多边形上下边缘被整条刷黑。端点排序统一成“下端点在前”是为了让dxdy的分母恒为正也让ymax表示终点删边条件e.ymax y才成立。配对循环用i 2且条件是i 1 size()交点数为奇数时最后一条不填这是有意为之——奇数交点通常意味着顶点处理有误而不是该补一段。交点的取整方式也有讲究起始用ceil、结束用floor是为了避免相邻多边形共享边界时重复填色。如果你的填充结果右侧总是多一列把floor换成round再对比一次就能确认是取整还是配对的问题。顶点是局部极值时的“计两次”规则这份代码用“水平边不入表”的方法绕过了代价是在很扁的三角形上偶尔少填一个顶点像素。4. 变换、裁剪与曲面从矩阵到深度缓冲4.1 齐次坐标与变换栈平移为什么必须占第四列二维变换实验看起来是最简单的一节但它是后面三维观察矩阵的地基。代码里把点写成(x, y, 1)矩阵用 3×3平移量放在第三列到了三维就是 4×4、第四列。这样做的原因很实际旋转和缩放都能用 2×2 或 3×3 表示唯独平移不能加一维就是为了把平移也写成乘法从而让任意多个变换可以连乘成一个矩阵。// common/mat4.h只保留关键构造其余置零 struct Mat4 { float m[4][4]; static Mat4 identity() { /* 对角线置 1 */ } static Mat4 translate(float tx, float ty, float tz) { Mat4 r identity(); r.m[0][3] tx; r.m[1][3] ty; r.m[2][3] tz; // 平移量在第 4 列 return r; } static Mat4 scale(float sx, float sy, float sz) { Mat4 r identity(); r.m[0][0] sx; r.m[1][1] sy; r.m[2][2] sz; return r; } }; // 矩阵乘法注意左乘顺序先应用的变换写在右边 Mat4 operator*(const Mat4 a, const Mat4 b) { Mat4 r{}; for (int i 0; i 4; i) for (int j 0; j 4; j) for (int k 0; k 4; k) r.m[i][j] a.m[i][k] * b.m[k][j]; return r; }m[i][3]这一列的语义是“平移”写在第 0、1、2 行分别对应 x、y、z。初学者最容易把平移写到第四行结果图形完全不动——因为点向量最后一维是 1只有第四列参与平移。另一个高频问题是复合变换顺序T * R表示先旋转再平移R * T表示先平移再旋转屏幕上差别巨大。实验代码里通常会提供按键让两种顺序切换方便对着看。变换栈glPushMatrix/glPopMatrix在当前代码里主要服务于树状结构绘制先压栈、画左子树、出栈再压栈、画右子树。如果你要改成自己的场景图建议直接维护一个std::vectorMat4当栈比依赖固定管线接口更可控。4.2 Cohen-Sutherland四位编码一次砍掉整条边裁剪实验分两块线段裁剪用 Cohen-Sutherland多边形裁剪用 Sutherland-Hodgman。前者的精髓是四位区域码上下左右各一位一个端点落在哪一侧就把对应位置 1。两个端点编码都为 0说明线段完全在窗口内直接保留按位与不为 0说明两个端点落在同一侧外部整条线段可以直接丢掉——这两个判断能省掉绝大多数求交运算。// 四位区域码左 1、右 2、下 4、上 8 static int encode(double x, double y, const Rect w) { int code 0; if (x w.xmin) code | 1; else if (x w.xmax) code | 2; if (y w.ymin) code | 4; else if (y w.ymax) code | 8; return code; } bool cohenSutherland(Point p0, Point p1, const Rect w) { int c0 encode(p0.x, p0.y, w), c1 encode(p1.x, p1.y, w); for (;;) { if (!(c0 | c1)) return true; // 全在窗口内保留 if (c0 c1) return false; // 同侧外部整条丢弃 int c c0 ? c0 : c1; // 取窗口外的那个端点来裁 double x 0, y 0; if (c 8) { x p0.x (p1.x - p0.x) * (w.ymax - p0.y) / (p1.y - p0.y); y w.ymax; } else if (c 4) { x p0.x (p1.x - p0.x) * (w.ymin - p0.y) / (p1.y - p0.y); y w.ymin; } else if (c 2) { y p0.y (p1.y - p0.y) * (w.xmax - p0.x) / (p1.x - p0.x); x w.xmax; } else if (c 1) { y p0.y (p1.y - p0.y) * (w.xmin - p0.x) / (p1.x - p0.x); x w.xmin; } if (c c0) { p0 {x, y}; c0 encode(x, y, w); } // 替换被裁掉的端点 else { p1 {x, y}; c1 encode(x, y, w); } } }求交那四行的分母都是“另一个方向上的跨度”所以调用前要先排除跨度为 0 的情况否则会出现除零得到inf的点画面表现为线段飞到窗口外一个极远处。代码里靠“同侧外部直接返回”把大部分水平线和垂直线挡在外面但恰好穿过窗口的绝对水平线仍可能进来稳妥做法是在函数入口加一句跨度判断。迭代最多四轮必收敛因为每轮至少把一个端点移出窗口外一位如果你担心死循环可以在循环里加计数器并在调试版断言。4.3 从 Bezier 求值到 Z-buffer曲线和消隐的关键几行曲线实验给出两种求值方式直接代入 Bernstein 基函数和 de Casteljau 递推。前者公式直观但控制点多了容易数值不稳后者用反复线性插值几何意义清晰也更好调试。// de Casteljau每轮把控制点折线收缩一层最后一层只剩一个点 Point deCasteljau(std::vectorPoint q, double t) { for (size_t k q.size() - 1; k 0; --k) for (size_t i 0; i k; i) q[i] { q[i].x (q[i 1].x - q[i].x) * t, q[i].y (q[i 1].y - q[i].y) * t }; return q[0]; }t从 0 到 1 采样多少份决定曲线平滑度实验里一般取 100 到 200 份再多屏幕上也就那么几个像素性能全浪费在重复点上了。q按值传入是关键函数内部的收缩不能改到调用方的控制点数组改成引用传参会把控制多边形直接改没这是很经典的一处翻车。消隐实验用 Z-buffer逐像素维护一个深度数组绘制时比较并更新// 软件 Z-buffer 的核心三步深度值越小越靠前 void putPixelDepth(int x, int y, float z) { if (z depth[y * WIDTH x]) return; // 被前面挡着丢弃 depth[y * WIDTH x] z; // 更新深度 color[y * WIDTH x] shadeAt(x, y, z); // 写颜色 }depth数组每帧开始要整体重置成极大值忘了这一步的表现是“第一帧正常转一下模型就糊成一片”因为上一帧的深度还在挡着。光照部分用 Phong 模型环境光、漫反射、镜面反射三项相加法向量必须在写入前归一化否则高光位置会偏。纹理映射那节用最近邻采样目的是让坐标对应关系一眼可查换成双线性插值属于二次开发范畴。5. 避坑与排查实验代码跑不起来的五个现场5.1 构建与运行期黑屏、链接失败、缩放错位现象一窗口能弹出里面全黑控制台没有任何输出。原因通常不在算法而在三条链路中的一条断了没有调用glClear或清屏颜色和绘制颜色相同绘制代码写在glutSwapBuffers之后投影矩阵的glOrtho范围和你用的像素坐标完全错开比如坐标是 0800正交范围却设成 -11。 解决顺序是固定的先把清屏色改成深灰画一个固定坐标的点确认能看见再检查glOrtho的左右下上四个参数是否覆盖坐标范围最后确认所有绘制都在glutDisplayFunc注册的回调里。三步走完还黑就基本可以断定是深度测试或混合状态把像素吃掉了。现象二链接期报一堆undefined reference to glBegin / glutInit。原因是只装了运行时库没装开发头文件和静态库或者 CMake 里find_package(OpenGL REQUIRED)找到的是另一个架构的库。解决方式是显式安装开发包并在CMakeLists.txt里把OpenGL::GL、GLUT::GLUT链接到每个可执行目标上。不要图省事把头文件路径写死成绝对路径换一台机器就得再来一遍。现象三高分屏上窗口标题栏正常绘图区只有左下角四分之一有内容。这是像素尺寸和逻辑尺寸不一致导致的常见于缩放比不是 100% 的显示器。GLUT 拿到的是逻辑尺寸而帧缓冲是物理像素glViewport用的还是逻辑尺寸画面自然被压缩到一角。解决办法是在reshape回调里按实际帧缓冲尺寸重设视口和正交范围或者干脆在启动时把窗口尺寸设成帧缓冲尺寸的整数分之一减少对照误差。5.2 算法结果不对填充漏点、裁剪越界、曲线不闭合现象四扫描线填充在多边形顶部漏了一整行或者左右边缘各少一列像素。原因集中在两处顶点是局部极值时被计了奇数次交点导致配对错位以及起止 x 用了ceil/floor后边界像素被排除在外。解决方式是把水平边排除在边表外这套代码的做法并对极值顶点做“计两次”补偿边缘少一列的问题则要看你的填充约定是“左闭右闭”还是“左闭右开”统一成一种相邻多边形拼合时才不会出现接缝。现象五Bezier 曲线首尾不落在控制点上或者中途出现折角。首尾不落在端点上多半是t的采样没覆盖到 0 和 1for (int i 0; i n; i) t i / n这种写法会漏掉t 1改成i / (n - 1)或i n即可。中途折角则通常是递推时误用了引用传参把上一层的结果覆盖掉或者控制点顺序在读取文件时被打乱。这类问题用“只画控制多边形不画曲线”的方式对照最快两条线一叠控制点对不对一眼就看出来。现象六开了 Z-buffer 之后模型表面出现闪烁的斑块。这是深度冲突两个面片深度值太接近浮点精度不够互相抢。解决方式是把近裁剪面从 0.001 这种极小值改到合理范围比如 0.1软件 Z-buffer 场景下则可以把深度数组从float换成double或者按包围盒把深度归一化到 01 再比较。别用“把其中一个面片往后挪一点”蒙混过去换个视角就又出现了。6. 二次开发把这堆实验改成能拿得出手的渲染小工具实验代码最尴尬的地方是每个 lab 都能跑但彼此不通。想画一个带光照的 Bezier 曲面你得把lab06的曲线求值、lab07的投影矩阵、lab08的光照一起搬进同一个main.cpp。我第一次干这事的时候直接把三个文件复制到一起结果setPixel被重复定义了三次编译报错刷了两屏——血泪经验是先做公共层再谈功能。改造的第一步是把common/真正用起来给每个实验提供一个统一入口。比如把所有 lab 的绘制逻辑收敛成一个接口// 统一实验入口每个实验只负责“往画布上画什么” using Scene std::functionvoid(Canvas, const Mat4 view); struct LabEntry { const char* name; Scene draw; // 绘制回调 bool useDepth; // 是否启用深度测试 }; // 主循环里按数字键切换实验不再为每个实验单开一个窗口 std::vectorLabEntry g_labs { {line, drawLineDemo, false}, {fill, drawFillDemo, false}, {bezier, drawBezierDemo, true }, };Canvas封装像素缓冲、深度缓冲和保存 PPM 的能力view传变换矩阵这样光照和投影只写一遍。这个改动的收益很直接加一个新实验只需往g_labs里塞一行不用再复制窗口初始化代码。第二步是把固定管线换成可编程管线。这件事不必一次做完可以按实验逐个迁移先保留glVertex2i的写法把setPixel换成写进一个std::vectoruint32_t再用一张纹理把整块缓冲贴到全屏四边形上。这样光栅化算法完全不用改只是输出目标从帧缓冲变成了内存数组顺带还能离线渲染出序列帧。改造方向具体改动收益输出目标setPixel写入内存缓冲统一贴图呈现可离线出图、可截序列帧管线固定管线迁移到 shader能加载模型、加后处理数据结构边表、控制点改用std::vector支持运行时增删图形交互鼠标拖拽控制点实时重算曲线演示和调试都更直观第三步是把验证做成自动化的。算法类代码最怕“这次看着对下次改一行就错”我一般会写一个小的对照脚本把几个固定输入的输出哈希记录下来改完跑一遍对比# tools/regress.py对固定输入跑一遍比对像素缓冲的哈希 import hashlib, subprocess, sys CASES [(line, 0,0,799,599), (circle, 400,300,150)] for name, args in CASES: out subprocess.run([./build/canvas_cli, name, args], capture_outputTrue, checkTrue).stdout digest hashlib.md5(out).hexdigest() print(f{name:8s} {digest})哈希变了不一定代表错了可能是你故意改了取整方式这时候就把新哈希作为基线更新进去。这套办法不高级但能挡住“改 A 功能顺手把 B 功能改坏”的情况尤其是裁剪和填充这种边界条件特别多的模块。最后一个技巧偏工程把窗口尺寸、采样份数、裁剪窗口范围全部提到一个配置文件或命令行参数里别写死在代码中间。图形学实验的参数极度依赖场景窗口 800×600 时看着正常的线宽换到 1920×1080 就细得像头发丝。从那以后我每次动这类教学代码都强制先跑一遍固定参数的回归再开窗口手看——机器比眼睛可靠眼睛只负责最后那点审美判断。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

从人肉运维到AI团队服务:托管Agent的运作逻辑与实践

从人肉运维到AI团队服务:托管Agent的运作逻辑与实践

1. 从“人肉运维一堆脚本”到“把AI团队作为一个服务调”先说我自己的现状。过去大半年,我一直在折腾多Agent协作的自动化工作流,最大的感受是:写Agent本身不难,难的是“带队伍”。一个任务拆给三个代理去做,每个代理有…

📅 2026/10/24 4:25:55
gdrcopy与GPUDirect RDMA:GPU显存直通RDMA的内核机制解析

gdrcopy与GPUDirect RDMA:GPU显存直通RDMA的内核机制解析

简介:gdrcopy 是一个面向 Linux 平台的高性能 GPU 内存复制库,专为利用 NVIDIA GPUDirect RDMA 技术优化跨 GPU 或 GPU-远程内存直传而设计,适用于 HPC、AI 训练与大规模数据处理等对带宽和延迟敏感的 C/C 开发场景。资源包共 51 个文件&…

📅 2026/10/24 4:20:54
DS90UB947 Linux驱动三重身份解析:I2C+V4L2+Media Controller协同机制

DS90UB947 Linux驱动三重身份解析:I2C+V4L2+Media Controller协同机制

简介:本资源是面向嵌入式Linux驱动开发工程师与汽车电子系统工程师的DS90UB947串行器IC驱动实现,专为解决i.MX6平台LVDS直驱成本高、布线难的问题而设计,适用于车载仪表盘等对EMI敏感、空间受限的远程显示场景。压缩包仅4KB,含3个…

📅 2026/10/24 4:20:54
MORE NEWS

更多资讯

📰

PON口割接技术解析:光接入网业务零感知迁移实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Servlet部署到Tomcat全流程:从war包到web.xml的实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

LTE实操指南:从协议栈到工业场景调优的硬核经验

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

CGTO算法优化:提升勘探能力与混沌映射效率

1. CGTO算法改进概述:为什么需要优化勘探能力?CGTO(Chaos-Global Topology Optimization)算法作为一种结合混沌理论与全局拓扑优化的新型智能算法,近年来在复杂函数优化、工程参数反演等领域展现出独特优势。但在实际应…

📰

CCF-CSP前两题:建模精度决定成败,拆解模拟题与边界陷阱

简介:CCF-CSP历年真题解答与代码解析资料包,汇集了“小明上学”“卖菜”“跳一跳”“相反数”以及“仓库规划”“因子化简”等经典小题,每道题均完整呈现题目描述、分步解题思路与可直接运行的C参考代码,面向备考CSP认证的学生、开…

📰

Windows/Office激活工具风险全解析:原理、危害与正规授权指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬