简介:本资源为三维CAD课程作业的完整C++源码包,面向计算机、自动化等专业学生及从业者,可用于课程设计、大作业或毕业设计参考。项目基于半边数据结构实现五个欧拉操作,并在此基础上完成扫掠操作,最终通过OpenGL将构建的实体模型进行图形显示,覆盖从数据结构定义到几何建模再到可视化的完整链路。压缩包共446个文件,以hpp头文件与inl内联实现为主,辅以少量h与cpp源文件,另含png效果图、txt输入数据及sln工程文件,整体约816KB,目录按图形绘制、数据结构与算法、程序入口等模块清晰划分。资源已有281人学习下载,代码经严格调试可运行,评审分达95分,读者可据此理解半边结构的组织方式、欧拉操作的实现细节与扫掠建模流程,并借助输入文件与图形交互快速验证效果,具备较高的学习借鉴与二次开发价值。
1. 半边结构、欧拉操作与扫成:三维 CAD 内核的最小闭环
很多人第一次接触三维 CAD 内核开发,都是从「画个立方体」开始的,但真正让一个建模内核站得住脚的,不是画得出来,而是改得动、查得清、存得下。这个课程作业标题里其实藏着一条完整的建模内核主线:半边数据结构负责「存得下」,欧拉操作负责「改得动」,扫成操作负责「造得出」,图形显示负责「看得见」。四件事串起来,就是一个最小可用的实体建模闭环。适合谁看?正在做三维 CAD 课程设计、想自己写一个能跑通的 B-rep 内核、或者被 OpenCASCADE 这类大库劝退想从底层理解拓扑与几何关系的开发者。我当年第一次写这套东西时,最大的感受是:半边结构不难,难的是欧拉操作里那些「先改哪根指针」的顺序,错一步整个模型就成黑匣子,调试全靠打印邻接关系。
2. 半边数据结构:为什么它比翼边更适合做欧拉操作
2.1 半边结构到底存了什么
半边结构(Half-Edge)的核心思想是把每条无向边拆成两条方向相反的有向半边,每条半边记录五个关键指针:起点顶点、所属面、下一条半边、上一条半边、配对半边。顶点记录一条从它出发的半边,面记录一条属于它的半边。这样做的直接好处是:给定任意半边,能在 O(1) 时间内拿到它所在的面、它的两个端点、它所在环的邻居。
和翼边结构相比,半边结构在欧拉操作里更顺手。翼边需要同时维护左右前后四个方向的边指针,插入和删除时指针更新量大;半边结构把「方向」显式拆开,欧拉操作里的分裂与合并逻辑更接近图论里的有向边操作。常见做法是:顶点、半边、面各用一个数组或 vector 存,索引用整数而不是裸指针,这样拷贝、序列化和调试都方便。
struct Vertex { double x, y, z; int halfEdge; // 从该顶点出发的一条半边索引 }; struct HalfEdge { int origin; // 起点顶点索引 int face; // 所属面索引 int next; // 下一条半边 int prev; // 上一条半边 int twin; // 配对半边 }; struct Face { int halfEdge; // 属于该面的一条半边 }; struct Solid { std::vector<Vertex> vertices; std::vector<HalfEdge> halfEdges; std::vector<Face> faces; };逻辑说明:用索引代替指针,避免深拷贝时的指针失效问题。每条半边只存起点,终点通过halfEdges[twin].origin拿到,这样顶点信息不冗余。参数说明:origin和twin是欧拉操作里最常改的两个字段,next和prev维护面内环,face在扫成操作生成侧面时需要批量赋值。
2.2 最小合法实体的构建与校验
一个合法实体要满足欧拉公式 V - E + F = 2(单连通闭合体)。用半边结构表示时,E 等于半边数的一半。构建一个立方体,常见做法是手动填 8 个顶点、12 条边拆成 24 条半边、6 个面,然后逐条设置 next、prev、twin。
bool validateSolid(const Solid& s) { int V = s.vertices.size(); int E = s.halfEdges.size() / 2; int F = s.faces.size(); if (V - E + F != 2) return false; // 检查每条半边的 twin 是否互指 for (int i = 0; i < (int)s.halfEdges.size(); ++i) { int t = s.halfEdges[i].twin; if (t < 0 || s.halfEdges[t].twin != i) return false; } // 检查每个面的环是否闭合 for (int f = 0; f < F; ++f) { int start = s.faces[f].halfEdge; int cur = start; do { if (s.halfEdges[cur].face != f) return false; cur = s.halfEdges[cur].next; } while (cur != start); } return true; }逻辑说明:先验欧拉公式,再验 twin 对称性和面环闭合性。参数说明:如果 V - E + F 不等于 2,说明有悬边、悬面或重复边;twin 检查失败通常是配对时漏改或改反;面环检查失败多半是 next 指针成环但环不闭合。这个校验函数建议在每次欧拉操作后都调用一次,早期能省下大量调试时间。
3. 欧拉操作:从 MVFS 到 KEMR 的指针更新顺序
3.1 五个基本欧拉操作的分工
欧拉操作是一组保持拓扑合法性的原子操作,常见的有 MVFS(造点面体)、MEV(造边点)、MEF(造边面)、KEMR(删边合并环)、KEV(删边点)。它们的特点是:每次操作只改变少量拓扑元素,且操作前后欧拉公式的差值可预期。MVFS 让 V+1、F+1、E 不变;MEV 让 V+1、E+1;MEF 让 E+1、F+1。
实现时最容易翻车的地方是指针更新顺序。以 MEV 为例:在某个顶点 v1 和它所在面的一条半边 h 之间插入新顶点 v2 和新边。正确顺序是:先分配新半边对,设置 origin 和 twin,再改 h 的 prev 的 next、h 的 prev、新半边的 next/prev,最后改 v1 或 v2 的 halfEdge 指向。顺序错了会出现「半边指向自己」或「面环断裂」。
int MEV(Solid& s, int v1, int h) { int v2 = s.vertices.size(); s.vertices.push_back({0, 0, 0, -1}); int h1 = s.halfEdges.size(); int h2 = h1 + 1; s.halfEdges.push_back({v1, s.halfEdges[h].face, h, s.halfEdges[h].prev, h2}); s.halfEdges.push_back({v2, s.halfEdges[h].face, s.halfEdges[h].next, h1, h1}); // 修正原半边 h 的前驱和后继 s.halfEdges[s.halfEdges[h].prev].next = h1; s.halfEdges[h].prev = h2; s.halfEdges[h2].next = h; s.vertices[v2].halfEdge = h2; return v2; }逻辑说明:新边把原半边 h 所在的面环切开,h1 和 h2 互为 twin。参数说明:v1是原顶点,h是面内一条半边,新顶点 v2 的坐标由调用方后续设置。注意s.halfEdges[h].prev必须在修改前先保存,否则链会断。
3.2 MEF 与 KEMR:造面与删面的对称性
MEF 在同一个面内连接两个顶点,把原面一分为二。KEMR 是它的逆操作,删掉一条边并把两个面合并。这两个操作是扫成操作生成侧面时的核心。MEF 的关键是:找到两个顶点之间的两条半边路径,分别作为两个新面的环。
bool MEF(Solid& s, int v1, int v2) { // 找到从 v1 到 v2 的两条半边路径 int h1 = findHalfEdge(s, v1, v2); int h2 = findHalfEdge(s, v2, v1); if (h1 < 0 || h2 < 0) return false; int newFace = s.faces.size(); s.faces.push_back({h1}); // 新建一条半边对连接 v2 到 v1 int h3 = s.halfEdges.size(); int h4 = h3 + 1; s.halfEdges.push_back({v2, newFace, h1, h2, h4}); s.halfEdges.push_back({v1, s.halfEdges[h1].face, h2, h1, h3}); // 重定向环 s.halfEdges[h2].next = h3; s.halfEdges[h1].prev = h4; s.halfEdges[h1].face = newFace; return true; }逻辑说明:findHalfEdge沿面环查找从 v1 到 v2 的半边。参数说明:MEF 要求 v1 和 v2 在同一个面内且不直接相连,否则会造出退化面。KEMR 实现时把上述步骤反向执行,注意先保存两个面的环信息再删半边。
4. 扫成操作:从二维轮廓到三维实体的批量生成
4.1 扫成的基本流程与轮廓预处理
扫成操作(Sweep)是把一个二维轮廓沿某方向拉伸成三维实体。常见做法是:先对二维轮廓做离散化,得到有序顶点列表,然后为每个顶点生成一条侧边,再用 MEF 或直接构建侧面环。轮廓必须是闭合且不自交的,否则扫成出来的实体欧拉公式不成立。
Solid sweep(const std::vector<std::pair<double,double>>& profile, double height) { Solid s; int n = profile.size(); // 底层顶点 for (auto& p : profile) s.vertices.push_back({p.first, p.second, 0, -1}); // 顶层顶点 for (auto& p : profile) s.vertices.push_back({p.first, p.second, height, -1}); // 底面和顶面 int bottomFace = s.faces.size(); s.faces.push_back({-1}); int topFace = s.faces.size(); s.faces.push_back({-1}); // 侧面:每对相邻轮廓点生成一个四边形面 for (int i = 0; i < n; ++i) { int j = (i + 1) % n; // 四个顶点:i, j, j+n, i+n // 依次创建四条半边并设置 next/prev/twin // 此处省略具体指针赋值,逻辑同 MEF 的环构建 } return s; }逻辑说明:先建上下两层顶点,再建底面和顶面,最后逐段生成侧面。参数说明:height是扫成高度,轮廓点顺序必须是逆时针或顺时针一致,否则侧面法向会翻转。实际实现时,侧面环的 next/prev 设置要保证每个面的环闭合,建议每生成一个面就调用一次局部校验。
4.2 扫成中的欧拉操作调用时机
扫成不是一次性把所有半边都建好,而是按「造点 → 造边 → 造面」的顺序调用欧拉操作。常见做法是:先用 MVFS 造一个初始点面体,然后对每个轮廓段调用 MEV 造边,再用 MEF 造侧面。这样每一步都保持拓扑合法,出错时容易定位。
// 伪代码:扫成中欧拉操作的调用顺序 int v0 = MVFS(s); int prev = v0; for (int i = 1; i < n; ++i) { int v = MEV(s, prev, someHalfEdge); prev = v; } // 闭合轮廓 MEF(s, prev, v0); // 沿高度方向扫成 for (int i = 0; i < n; ++i) { int vTop = MEV(s, i, someHalfEdge); // 设置 vTop 的 z 坐标 }逻辑说明:先造底面环,再逐点向上扫成。参数说明:someHalfEdge需要根据当前面环动态查找,不能写死。如果扫成过程中欧拉公式突然不成立,优先检查 MEV 的h参数是否指向了正确的面环。
5. 避坑与排查:欧拉操作和扫成里最容易翻车的五件事
5.1 半边 twin 配对反了,面法向集体朝内
现象:模型能显示,但光照全黑或法向朝内。原因:MEF 或扫成时 twin 的 origin 设置反了,导致面环方向不一致。解决:在 validateSolid 里加一条检查——对每个面,计算环上相邻半边的叉积,法向应一致朝外。发现反了就把该面的半边环整体反转,或重建时统一轮廓方向。
5.2 MEV 后顶点 halfEdge 指向悬空
现象:遍历顶点邻接边时崩溃或死循环。原因:MEV 里新顶点的 halfEdge 没有指向新半边,或者指向了一条已被删除的半边。解决:MEV 返回前强制设置s.vertices[v2].halfEdge = h2,并在每次删除操作后检查所有顶点的 halfEdge 是否仍有效。
5.3 扫成轮廓自交导致欧拉公式不成立
现象:V - E + F 不等于 2,模型出现自交面。原因:二维轮廓有自交或重复点,扫成后侧面环交叉。解决:扫成前对轮廓做去重和自交检测,常见做法是用线段相交测试,发现自交就报错而不是硬扫。
5.4 KEMR 删边后残留孤立顶点
现象:欧拉公式差值不对,模型里有多余顶点。原因:KEMR 只删了半边和面,没有删顶点。解决:KEMR 后检查两个端点是否还有半边从它们出发,没有就删顶点并更新索引。注意删顶点会导致后续索引偏移,建议用「标记删除 + 定期压缩」的方式。
5.5 图形显示时半边索引用 int 但渲染用 float 精度不够
现象:模型显示有裂缝或顶点不重合。原因:顶点坐标用 float 存,扫成高度累加后精度丢失。解决:顶点坐标统一用 double,渲染时再转 float。如果必须用 float,扫成高度不要用累加,直接用乘法算每层 z 坐标。
6. 进阶技巧:用欧拉操作实现倒角和局部细分
欧拉操作的价值不只是造基本体,更在于支持局部修改。比如倒角:在一条边的两个端点各做一次 MEV,再用 MEF 连接新顶点,就能把一条边替换成一个斜面。局部细分也是类似思路:对目标面做多次 MEF,每次在面内插入新边,逐步加密。
验证欧拉操作是否正确,我一般用两个指标:一是欧拉公式在每次操作后的差值是否符合预期,二是模型能否导出为 OBJ 后再导入回来,拓扑不变。导出 OBJ 时只写顶点和面,不写半边,导入时重建半边结构,如果重建后的 V - E + F 和原来一致,说明拓扑是干净的。
// 倒角一条边的简化流程 int vA = MEV(s, edgeStart, h1); // 在起点附近造新点 int vB = MEV(s, edgeEnd, h2); // 在终点附近造新点 MEF(s, vA, vB); // 连接新点,形成斜面 // 调整 vA、vB 坐标实现倒角宽度逻辑说明:倒角本质是用一个新面替换一条边,欧拉操作保证替换过程中拓扑合法。参数说明:h1和h2要选在边的两侧面内,否则倒角会穿透。倒角宽度通过 vA、vB 到原边的距离控制,建议先做小宽度测试再放大。
我自己的习惯是:每写一个欧拉操作,先用手算的立方体验证一遍,再上扫成。扫成出来的实体如果欧拉公式不对,九成是侧面环的 next/prev 在某个角落没闭合。这个坑我踩过不止一次,后来养成了「每造一个面就打印一次环」的习惯,虽然土,但省时间。希望帮到你。
本文还有配套的精品资源,点击获取