C++实现半边结构CAD建模:支持多环扫掠、欧拉操作与OpenGL实时旋转渲染

C++实现半边结构CAD建模:支持多环扫掠、欧拉操作与OpenGL实时旋转渲染

本文还有配套的精品资源,点击获取

简介:用C++从零搭建三维CAD建模核心功能,基于半边数据结构(Half-Edge)精确维护几何拓扑关系。内置5种标准欧拉操作(MakeVertex、KillFace等),确保每次顶点/边/面增删后模型仍满足流形约束;扩展实现带孔扫掠建模——可读取in.txt中定义的外环+内环轮廓(顶点逆时针排列)及拉伸方向向量,自动生成封闭实体网格。所有建模过程在内存中完成,不依赖第三方CAD内核。OpenGL 3.3实时渲染模块独立封装在Draw.h和着色器中,支持模型持续自转、WASD视角平移,渲染结果直观验证拓扑正确性。控制台同步输出每个面的环结构与顶点顺序,便于调试。工程已预配置GLFW+GLAD+GLM依赖,VS2019一键编译,含完整readme.md、底面示意图、渲染效果图、opengl库文件及build.sh脚本,开箱即可运行,适用于高校CAD课程设计、大作业或轻量级几何建模原型开发。

1. 这不是“画个3D盒子”,而是一次对几何建模底层逻辑的亲手拆解

如果你在高校上过《计算机辅助设计》《几何建模基础》或《计算几何》这类课,大概率见过“半边结构”“欧拉操作”这些词被写在PPT第17页,配一张抽象的拓扑图,然后老师说:“这部分原理很重要,考试不考,但你们以后做CAD、CAE、甚至游戏引擎网格编辑,绕不开它。”——这话一点没夸张。我带过三届本科生课程设计,每年都有学生卡在“为什么删一个面后模型就破了?”“为什么两个面共享一条边,却找不到对方?”“扫掠出的孔洞边缘怎么和侧面自动缝合?”这些问题背后,不是代码写错了,而是对流形几何的拓扑约束缺乏肌肉记忆。

这个项目,就是我把这门课最硬核的“手写内核”环节,从讲义里拎出来,用C++一行行敲出来的完整实现。它不调用OpenCASCADE、不链接ACIS、不依赖任何商业CAD内核——所有顶点、边、面的创建、连接、销毁,全靠你自己定义的数据结构和5个欧拉操作来兜底。你输入的in.txt里只有几行坐标和一个向量,程序输出的却是一个数学上严格封闭(2-manifold)、每条边恰好被两个面共享、每个面边界由单一环构成的实体网格。控制台打印出的每一行“Face #3: outer loop: v0→v1→v2→v3; inner loops: [v4→v5→v6]”,不是日志,是拓扑健康的体检报告。

关键词里“半边结构”是骨架,“欧拉操作”是手术刀,“扫掠建模”是第一个能落地的应用,“OpenGL渲染”是验证眼睛,“CAD建模”是它的归宿。它适合谁?适合正在啃《Computational Geometry for Design and Manufacture》第4章的学生;适合想搞懂SolidWorks“缝合”功能底层怎么判断边是否可缝合的工程师;也适合那个在GitHub上搜“half edge c++”翻了二十个半成品项目、最后发现要么缺扫掠、要么没欧拉操作、要么渲染只是画个线框的你。它不承诺“一键生成复杂曲面”,但它保证:当你把in.txt里底面顶点顺序改反了,程序会立刻在控制台报错“outer loop not CCW”,而不是静默生成一个法向翻转的废模型——这种确定性,才是工程级建模的起点。

我试过用其他数据结构替代半边:用面-边表(Face-Edge List)做扫掠时,处理内环与外环的边匹配要写三重嵌套循环;用翼边(Winged-Edge)虽然也能维护邻接关系,但KillFace操作时更新四条翼边指针的逻辑极易出错,调试三天才发现某条边的left_face指针指向了已释放内存。而半边结构,用“一条边存两个半边”的设计,把“找邻面”“找邻边”“找邻顶点”全部变成单次指针跳转,欧拉操作的实现变得像搭乐高——每个操作只动局部几个指针,全局一致性由结构本身保障。这不是炫技,是经过十几次崩溃重启后,唯一让我敢在课程答辩现场实时修改in.txt并当场演示拓扑修复的方案。

2. 半边结构不是“多存一倍边”,而是为拓扑一致性预埋的保险丝

2.1 为什么必须是半边?——从“一条边,四个问题”说起

想象你要建一个带圆孔的矩形板(即底面含外环+内环),然后沿Z轴拉伸成实体。这个操作看似简单,但背后藏着四个必须实时回答的问题:

  1. 这条边属于哪个面?(外环边属于顶面,内环边属于底面,侧面边属于侧面面)
  2. 这条边的另一侧是哪个面?(侧面边的两侧分别是顶面和侧面,或底面和侧面;而顶面外环边的“另一侧”是空气,即无面)
  3. 沿着这条边逆时针走,下一个顶点是谁?(决定面的法向和渲染朝向)
  4. 这条边的“孪生兄弟”在哪?(即同一条物理边的另一半,用于快速切换面)

传统“顶点数组+索引数组”(如OpenGL的glDrawElements)只回答问题3,且仅限于绘制时。而“面-边表”(每个面存一个边列表)能回答问题1和3,但问题2需要遍历所有面去搜索——O(n)时间,建模中频繁查询不可接受。问题4则根本不存在,因为边没有“孪生”概念。

半边结构(Half-Edge Data Structure)的精妙,在于它把“一条物理边”拆成两个有向的“半边”(Half-Edge),每个半边只负责一个方向。我们定义:

  • HalfEdge结构体包含:
  • Vertex* origin:该半边起点顶点
  • Face* face:该半边所属的面
  • HalfEdge* next:在当前面内,沿逆时针方向的下一条半边
  • HalfEdge* prev:在当前面内,沿顺时针方向的上一条半边(可由next推导,但缓存提升性能)
  • HalfEdge* twin:该半边的孪生兄弟(指向相反方向的另一半边)
  • Edge* edge:指向其所属的物理边(可选,用于存储边长、材质等属性)

提示:twin指针是整个结构的“心脏”。有了它,问题2的答案就是this->twin->face(如果twin存在且twin->face != nullptr);问题4的答案就是this->twin。一次指针解引用,O(1)解决。

2.2 数据结构设计:如何让“增删”不破坏拓扑?

HalfEdgeDataStructure.h中,我没有用裸指针堆砌,而是用std::vector<std::unique_ptr<T>>管理所有实体,并用size_t索引代替指针(避免悬空指针)。核心结构如下:

struct Vertex { glm::vec3 pos; HalfEdge* incident_edge; // 指向任意一条以该顶点为起点的半边(用于遍历邻接关系) }; struct Face { std::vector<HalfEdge*> outer_loop; // 外环半边指针列表(逆时针) std::vector<std::vector<HalfEdge*>> inner_loops; // 内环列表,每个内环也是半边指针向量 glm::vec3 normal; // 面法向,由外环顶点叉乘计算 }; struct HalfEdge { size_t origin_idx; // 顶点索引 size_t face_idx; // 所属面索引 size_t next_idx; // 下一半边索引 size_t prev_idx; // 上一半边索引 size_t twin_idx; // 孪生兄弟索引(若为边界边,则twin_idx == INVALID_IDX) size_t edge_idx; // 物理边索引(可选) };

注意:INVALID_IDX定义为std::numeric_limits<size_t>::max()。所有索引访问前都做边界检查,这是课程设计中防止学生误操作导致崩溃的第一道防线。

为什么用索引不用指针?因为std::vector扩容时指针会失效。而索引是稳定的,只要vectorshrink_to_fit,索引永远有效。incident_edge字段在Vertex中存的是HalfEdge*,但它的值是在vector稳定后,通过索引计算得到的临时指针,仅用于遍历,不参与长期存储。

这个设计让“安全增删”成为可能:每次欧拉操作(如MakeVertex)只修改局部几个索引,vector的内存布局不变,所有已有索引依然有效。而std::unique_ptr确保了资源自动释放,不会因KillFace忘记删半边而导致内存泄漏——这在学生作业里太常见了。

2.3 欧拉操作:5个原子动作,构建拓扑的“最小完备集”

欧拉操作(Euler Operations)不是魔法,它是基于欧拉示性数公式V - E + F = 2 - 2g(g为亏格)推导出的一组保拓扑的原子操作。它们确保每次操作后,模型仍是流形(manifold)且满足欧拉公式。本项目实现了5个最常用、最基础的操作,构成一个最小完备集:

操作名功能改变量 (ΔV, ΔE, ΔF)关键约束
MakeVertex(v)创建新顶点v(+1, 0, 0)v必须未存在
MakeEdge(e, v1, v2)创建新边e连接v1→v2(0, +1, 0)e必须未存在,v1/v2必须存在
MakeFace(f, he_list)创建新面f,由半边列表he_list围成(0, 0, +1)he_list必须构成闭合环,且所有半边face == nullptr
KillFace(f)删除面f及其所有半边(0, 0, -1)f必须存在,且所有半边twin必须指向其他面(即非边界)
Splice(he1, he2)将he1的next指向he2,he2的prev指向he1(0, 0, 0)用于缝合两个环,是扫掠的核心

实操心得:Splice操作看似简单,却是扫掠建模成败的关键。当扫掠一个带孔底面时,你需要将“顶面外环”与“侧面顶部环”缝合,同时将“底面外环”与“侧面底部环”缝合,还要将“顶面内环”与“孔侧面顶部环”缝合……总共6次Splice。我最初漏掉一次,结果模型在孔洞处出现一条“裂缝边”,OpenGL渲染时那条边是黑色的(因为缺失面)。后来在Sweep.cpp里给每次Splice加了断言:assert(he1->twin != INVALID_IDX && he2->twin != INVALID_IDX),立刻定位到问题。

这5个操作之所以“完备”,是因为所有更复杂的建模动作(如ExtrudeFace,BridgeEdges)都可以由它们组合而成。例如,“拉伸一个面”可以分解为:MakeVertex×4(创建新顶点)→MakeEdge×8(创建新边)→MakeFace×4(创建4个侧面)→Splice×4(缝合上下环)。课程设计要求学生手动写出这个分解过程,比直接调用extrude()函数更能理解拓扑是如何被一步步构建的。

3. 扫掠建模:从二维轮廓到三维实体,一场精确的“拓扑缝合手术”

3.1 输入解析:in.txt的格式即拓扑契约

in.txt文件是整个建模流程的“宪法”,它的格式强制规定了拓扑的合法性。一个典型的带孔底面输入如下:

# 外环顶点(逆时针) 4 0.0 0.0 0.0 2.0 0.0 0.0 2.0 1.0 0.0 0.0 1.0 0.0 # 内环数量 1 # 第1个内环顶点(逆时针) 3 0.5 0.3 0.0 1.5 0.3 0.0 1.0 0.7 0.0 # 扫掠方向向量 0.0 0.0 1.0

关键契约有三条:

  1. 外环必须存在且顶点数≥3:这是生成顶面和底面的基础。
  2. 所有环(外环+每个内环)的顶点必须严格按逆时针(CCW)顺序排列:这决定了面的法向(右手定则),MakeFace操作会校验叉积normal.z > 0,否则报错并退出。我故意没做自动纠正,因为自动翻转顶点顺序会掩盖学生对“法向约定”的理解漏洞。
  3. 内环必须完全位于外环内部:程序不进行几何包含判断(那是计算几何库的事),但Sweep算法假设此条件成立。若内环顶点在外环外,扫掠出的侧面会自交,KillFace时因拓扑非法而崩溃——这恰恰是让学生debug几何关系的好机会。

解析过程在Sweep.cppparseInputFile()中完成,使用std::ifstream逐行读取,用std::vector<std::vector<glm::vec3>> contours存储所有环。这里有个细节:所有顶点z坐标被强制设为0.0,因为扫掠是沿指定方向的刚性平移,底面必须共面。如果输入z坐标非零,程序会警告但继续执行(视为用户有意为之的倾斜底面)。

3.2 扫掠四步法:从轮廓到实体的原子步骤

扫掠(Sweep)不是“复制粘贴”,而是基于欧拉操作的四步精密手术。以单外环无内环为例,步骤如下:

Step 1:创建顶面与底面
- 调用MakeFace两次:一次用原始外环顶点创建top_face,一次用原始顶点 + 扫掠向量创建bottom_face
- 此时模型有两个分离的面,无连接。

Step 2:创建侧面顶点与边
- 对外环每个顶点v_i,创建新顶点v_i' = v_i + direction(即底面顶点)。
- 对每条外环边e_i = (v_i → v_{i+1}),创建两条新边:
-e_i_top = (v_i → v_{i+1})(已在顶面中)
-e_i_bottom = (v_i' → v_{i+1}')(底面中)
-e_i_side1 = (v_i → v_i')(前侧面边)
-e_i_side2 = (v_{i+1} → v_{i+1}')(后侧面边)
- 共创建4*N条新边(N为外环顶点数)。

Step 3:构建侧面面
- 对每个i,用四条半边[e_i_top, e_i_side2, reverse(e_{i+1}_bottom), reverse(e_i_side1)]创建一个四边形侧面面。
-reverse()操作通过getTwin()获取孪生半边实现,确保边的方向正确。

Step 4:缝合(Splice)——最关键的一步
- 将top_face的外环半边he_top_inext指向side_face_i的首半边。
- 将side_face_i的末半边next指向bottom_face的外环半边he_bottom_i(需反转顺序,因底面法向向下)。
- 将bottom_face的外环半边next指向side_face_{i-1}的某半边,形成闭环。
- 对内环,逻辑相同,但需额外处理“孔侧面”的缝合,且内环的Splice必须在top_facebottom_face的内环已创建后进行。

实操心得:缝合顺序不能错。我第一次实现时先缝了顶面-侧面,再缝侧面-底面,结果底面内环的半边twin指向了不存在的面。后来严格按“先建所有面,再统一缝合”的流程,并在Splice函数里加入assert(he1->face_idx != INVALID_IDX && he2->face_idx != INVALID_IDX),问题立刻暴露。现在Sweep.cpp里缝合部分有超过15行注释,详细说明每条Splice连接哪两个面的哪条边,这是学生最容易抄错的地方。

3.3 带孔扫掠:内环带来的拓扑复杂度跃迁

当底面有内环时,扫掠的复杂度不是线性增加,而是指数级。一个外环+N个内环的底面,扫掠后实体包含:

  • 1个顶面:含1个外环 + N个内环
  • 1个底面:含1个外环 + N个内环
  • N+1个侧面组:1个外侧面(连接外环) + N个内侧面(每个内环对应一个孔侧面)

关键挑战在于面的环结构管理Face结构体中的inner_loopsvector<vector<HalfEdge*>>,每个内环是一个独立的半边向量。在MakeFace创建顶面时,必须将外环半边放入outer_loop,每个内环半边放入对应的inner_loops[i]。稍有不慎,inner_loops[0]里混入了外环半边,后续KillFace就会因环不闭合而失败。

我在Sweep.h中专门写了validateFaceTopology(const Face& f)函数,它检查:
-outer_loop是否闭合(last->next == first
- 每个inner_loops[i]是否闭合
- 所有半边的face指针是否都指向当前面
- 所有半边的twin是否都不为空(边界边除外)

这个函数在每次MakeFace后自动调用,控制台会输出“Face #2 topology valid”或具体错误。学生第一次看到“inner loop #1 not closed at vertex v7”时,就知道该回去检查in.txt里内环顶点数是不是少写了一个。

4. OpenGL实时渲染:不只是“画出来”,而是“证明它正确”

4.1 渲染架构:为何Draw.h要与算法完全解耦?

很多学生把OpenGL代码和建模算法写在一起,结果main.cpp里充斥着glVertexAttribPointerMakeVertex混杂的代码。本项目强制分离:Draw.h只做一件事——把内存中的半边结构,翻译成GPU能理解的顶点缓冲区(VBO)和索引缓冲区(IBO)

Draw.h的核心函数是updateMeshFromHalfEdge(const HalfEdgeDataStructure& heds)

void updateMeshFromHalfEdge(const HalfEdgeDataStructure& heds) { std::vector<glm::vec3> vertices; std::vector<unsigned int> indices; // Step 1: 遍历所有面,提取顶点坐标(去重) std::map<size_t, unsigned int> vertex_map; // half-edge索引 -> VBO索引 for (const auto& face : heds.faces) { // 添加外环顶点 for (auto he_ptr : face.outer_loop) { size_t v_idx = he_ptr->origin_idx; if (vertex_map.find(v_idx) == vertex_map.end()) { vertex_map[v_idx] = vertices.size(); vertices.push_back(heds.vertices[v_idx].pos); } } // 添加内环顶点(同样去重) for (const auto& inner_loop : face.inner_loops) { for (auto he_ptr : inner_loop) { size_t v_idx = he_ptr->origin_idx; if (vertex_map.find(v_idx) == vertex_map.end()) { vertex_map[v_idx] = vertices.size(); vertices.push_back(heds.vertices[v_idx].pos); } } } } // Step 2: 为每个面生成三角剖分索引(耳切法) for (const auto& face : heds.faces) { triangulateFace(face, vertices, indices, vertex_map, heds); } // Step 3: 更新VBO/IBO glBindBuffer(GL_ARRAY_BUFFER, VBO); glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(glm::vec3), vertices.data(), GL_STATIC_DRAW); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, IBO); glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW); num_indices = indices.size(); }

注意:triangulateFace()使用简单的耳切法(Ear Clipping),对凸多边形和简单凹多边形足够。它不处理自交多边形——这再次把几何健壮性检查的责任交还给输入和扫掠算法,符合“建模与渲染分离”的哲学。

这种设计的好处是:EulerOperation.cpp里可以放心地KillFace,只要heds对象还有效,Draw.h下次updateMeshFromHalfEdge()就能立刻渲染出新拓扑。学生在控制台看到“KillFace #5”后,转头看屏幕,那个面真的消失了,而且周边面无缝衔接——这种即时反馈,是理解拓扑操作最直观的方式。

4.2 实时交互:WASD视角与自转,不只是炫技

Camera.h封装了一个自由移动相机,支持:

  • WASD键:沿相机本地X/Z轴平移(cameraPos += cameraFront * speed
  • 鼠标拖拽:改变yaw(水平角)和pitch(俯仰角),更新cameraFront向量
  • 滚轮:缩放fov(视野角),实现镜头推拉

但最关键的,是模型自转。在main.cpp的渲染循环中:

float rotation_angle = glfwGetTime() * 30.0f; // 每秒30度 glm::mat4 model = glm::rotate(glm::mat4(1.0f), glm::radians(rotation_angle), glm::vec3(0.0f, 1.0f, 0.0f));

这个自转有双重意义:第一,它让隐藏的背面(如孔洞内壁)旋转到可视范围,学生能直观确认“孔确实是通的”;第二,它暴露了法向错误——如果某个面的法向算反了,它在旋转到特定角度时会突然“消失”(被OpenGL背面剔除)。我在Face::calculateNormal()里强制要求外环叉乘结果z > 0,就是为了确保所有面法向一致朝外。

提示:Shader.h中的顶点着色器(shader.vs)和片元着色器(shader.fs)极其简洁,没有光照模型,只有纯色(fragColor = vec4(0.3f, 0.7f, 0.9f, 1.0f))。因为课程重点是拓扑,不是渲染效果。颜色越简单,越容易看出哪个面缺失、哪条边断裂。

4.3 控制台输出:拓扑健康报告,比渲染图更可靠

main.cpp在建模完成后,会调用printTopologySummary(const HalfEdgeDataStructure& heds),输出类似:

=== TOPOLOGY SUMMARY === Vertices: 16 Edges: 24 (Physical) / 48 (Half-Edges) Faces: 10 Face #0 (Top): outer loop: v0→v1→v2→v3; inner loops: [v4→v5→v6] Face #1 (Bottom): outer loop: v8→v9→v10→v11; inner loops: [v12→v13→v14] Face #2 (Side0): outer loop: v0→v1→v9→v8 Face #3 (Side1): outer loop: v1→v2→v10→v9 ...

这个输出不是装饰。当学生发现Face #0的内环顶点是v4→v5→v6,但in.txt里内环顶点是v4→v5→v7,立刻知道v6v7在输入时标错了索引。或者,当Edges: 24 / 48中,48不是24*2,说明有半边twin未正确设置——这是Splice漏写的铁证。

我坚持用printf而非std::cout,因为printf在Windows控制台输出更稳定,且格式化字符串(%zu)能精准控制索引对齐,方便肉眼比对。

5. 工程实践与避坑指南:那些README里不会写的血泪经验

5.1 编译环境:VS2019 + GLFW/GLAD/GLM,为什么是这个组合?

  • VS2019:课程机房普遍安装,C++17支持完善(std::optional,if constexpr),且.vcxproj配置成熟。我刻意避免C++20特性,确保学生能在旧版VS上编译。
  • GLFW:轻量级窗口管理,glfwSetKeyCallback完美支持WASD事件,比原生Win32 API简单十倍。
  • GLAD:现代OpenGL加载器,gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)一行搞定函数地址绑定,避免wglGetProcAddress的手动繁琐。
  • GLM:数学库,glm::vec3,glm::rotate等API与OpenGL规范一致,无需自己写矩阵类。

注意:opengl/目录下预置了编译好的glfw3.lib,glad.lib和头文件。build.sh脚本(Linux/macOS)用cmake生成Makefile,hello_opengl.sln(Windows)直接打开即可。所有路径在.vcxproj中用$(SolutionDir)相对路径,杜绝绝对路径导致的“在我电脑上能跑”问题。

5.2 常见问题速查表

现象可能原因排查命令/技巧
程序启动后黑屏,无报错in.txt路径错误,或文件为空parseInputFile()开头加printf("Reading in.txt...\n"); fflush(stdout);,确认是否进入函数
控制台报“outer loop not CCW”in.txt中某环顶点顺序是顺时针底面.png对照,或在validateLoop()中打印叉积值:printf("Cross product z = %f\n", cross.z);
模型旋转时某面闪烁消失该面法向计算错误,被背面剔除注释掉glEnable(GL_CULL_FACE),看是否所有面都显示;或检查Face::calculateNormal()中叉积符号
WASD键无反应glfwSetKeyCallback未正确注册,或main.cppprocessInput()未被调用processInput()开头加printf("Key pressed: %d\n", key);,确认回调触发
扫掠后孔洞边缘有裂缝(黑色线)Splice操作漏掉一次,导致某条边twin为空Splice(he1, he2)中加assert(he1->twin_idx != INVALID_IDX && he2->twin_idx != INVALID_IDX);
编译报错“LNK2019: unresolved external symbol”EulerOperation.cpp未添加到项目,或EulerOperation.h中函数声明与定义不匹配检查.vcxproj.filtersEulerOperation.cpp是否在Source Files下;用grep -r "MakeVertex" src/确认声明与定义一致

5.3 课程设计延伸建议:从“能跑”到“能用”

这个项目是很好的起点,但绝非终点。根据往届学生反馈,以下延伸方向既保持难度可控,又能深化理解:

  • 添加布尔运算:基于半边结构实现Union(并集)。核心是SplitEdge操作(将一条边在交点处分割),再用Splice重组。难点在于交点计算,可用glm::intersectRayTriangle近似。
  • 支持STL文件导入/导出:将半边结构序列化为ASCII STL(facet normal ... outer loop ... endloop endfacet),或从STL解析出三角面再重建半边。这是连接教学项目与工业数据的桥梁。
  • 实现简单倒角(Chamfer):选中一条边,为其创建两个新面。需要SplitEdge×2 +MakeFace×2 +Splice×4。能直观展示欧拉操作如何塑造细节。
  • 添加线框模式切换:在Draw.h中增加drawWireframe()函数,用glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)绘制所有边,比填充模式更能暴露拓扑缺陷。

我个人在指导毕设时,会要求学生至少完成一项延伸。去年有个学生做了STL导入,他发现工业STL常有非流形边(一条边被三个面共享),于是他在parseSTL()里加了自动修复逻辑:检测到三面共享边时,将第三个面分割为两个面。这个“小修补”,让他真正理解了为什么商业CAD软件要花巨资做“STL修复”。

6. 最后一点体会:建模的终点,是让机器理解几何的“为什么”

写完这个项目,我重新翻开了大二时那本翻烂的《Computational Geometry》,发现当年看不懂的公式,现在能对着HalfEdgeDataStructure.h里的next_idxtwin_idx一一对应。欧拉操作不再是纸上的符号,而是我每天在调试器里看着he->twin->face->outer_loop一层层跳转的真实指针链。

这个项目的价值,不在于它能生成多复杂的模型,而在于它强迫你直面几何的本质:点、线、面不是孤立的,它们的关系(邻接、包含、方向)才是模型的灵魂。当你在in.txt里把内环顶点顺序写反,程序拒绝建模,这不是bug,是它在告诉你:“这个环无法定义一个有向区域,我无法理解你的意图。”

所以,如果你正为CAD课程设计发愁,别急着搜“C++ CAD开源库”,先把这个半边结构从头敲一遍。从MakeVertex开始,到KillFace结束,中间穿插Splice的每一次缝合。当你的第一个带孔扫掠实体在OpenGL里平稳旋转,控制台清晰打印出每个面的环结构时,你就已经跨过了那道从“使用者”到“构建者”的门槛。后面的路,无论是深入OpenCASCADE源码,还是自己写一个轻量级B-rep内核,脚下都已有了坚实的基石。

毕竟,所有伟大的CAD系统,最初都是从一个正确指向的twin指针开始的。

本文还有配套的精品资源,点击获取

简介:用C++从零搭建三维CAD建模核心功能,基于半边数据结构(Half-Edge)精确维护几何拓扑关系。内置5种标准欧拉操作(MakeVertex、KillFace等),确保每次顶点/边/面增删后模型仍满足流形约束;扩展实现带孔扫掠建模——可读取in.txt中定义的外环+内环轮廓(顶点逆时针排列)及拉伸方向向量,自动生成封闭实体网格。所有建模过程在内存中完成,不依赖第三方CAD内核。OpenGL 3.3实时渲染模块独立封装在Draw.h和着色器中,支持模型持续自转、WASD视角平移,渲染结果直观验证拓扑正确性。控制台同步输出每个面的环结构与顶点顺序,便于调试。工程已预配置GLFW+GLAD+GLM依赖,VS2019一键编译,含完整readme.md、底面示意图、渲染效果图、opengl库文件及build.sh脚本,开箱即可运行,适用于高校CAD课程设计、大作业或轻量级几何建模原型开发。


本文还有配套的精品资源,点击获取