C++实现半边结构CAD建模:支持多环扫掠、欧拉操作与OpenGL实时旋转渲染 本文还有配套的精品资源点击获取简介用C从零搭建三维CAD建模核心功能基于半边数据结构Half-Edge精确维护几何拓扑关系。内置5种标准欧拉操作MakeVertex、KillFace等确保每次顶点/边/面增删后模型仍满足流形约束扩展实现带孔扫掠建模——可读取in.txt中定义的外环内环轮廓顶点逆时针排列及拉伸方向向量自动生成封闭实体网格。所有建模过程在内存中完成不依赖第三方CAD内核。OpenGL 3.3实时渲染模块独立封装在Draw.h和着色器中支持模型持续自转、WASD视角平移渲染结果直观验证拓扑正确性。控制台同步输出每个面的环结构与顶点顺序便于调试。工程已预配置GLFWGLADGLM依赖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轴拉伸成实体。这个操作看似简单但背后藏着四个必须实时回答的问题这条边属于哪个面外环边属于顶面内环边属于底面侧面边属于侧面面这条边的另一侧是哪个面侧面边的两侧分别是顶面和侧面或底面和侧面而顶面外环边的“另一侧”是空气即无面沿着这条边逆时针走下一个顶点是谁决定面的法向和渲染朝向这条边的“孪生兄弟”在哪即同一条物理边的另一半用于快速切换面传统“顶点数组索引数组”如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::vectorstd::unique_ptrT管理所有实体并用size_t索引代替指针避免悬空指针。核心结构如下struct Vertex { glm::vec3 pos; HalfEdge* incident_edge; // 指向任意一条以该顶点为起点的半边用于遍历邻接关系 }; struct Face { std::vectorHalfEdge* outer_loop; // 外环半边指针列表逆时针 std::vectorstd::vectorHalfEdge* 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_limitssize_t::max()。所有索引访问前都做边界检查这是课程设计中防止学生误操作导致崩溃的第一道防线。为什么用索引不用指针因为std::vector扩容时指针会失效。而索引是稳定的只要vector不shrink_to_fit索引永远有效。incident_edge字段在Vertex中存的是HalfEdge*但它的值是在vector稳定后通过索引计算得到的临时指针仅用于遍历不参与长期存储。这个设计让“安全增删”成为可能每次欧拉操作如MakeVertex只修改局部几个索引vector的内存布局不变所有已有索引依然有效。而std::unique_ptr确保了资源自动释放不会因KillFace忘记删半边而导致内存泄漏——这在学生作业里太常见了。2.3 欧拉操作5个原子动作构建拓扑的“最小完备集”欧拉操作Euler Operations不是魔法它是基于欧拉示性数公式V - E F 2 - 2gg为亏格推导出的一组保拓扑的原子操作。它们确保每次操作后模型仍是流形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 nullptrKillFace(f)删除面f及其所有半边(0, 0, -1)f必须存在且所有半边twin必须指向其他面即非边界Splice(he1, he2)将he1的next指向he2he2的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关键契约有三条外环必须存在且顶点数≥3这是生成顶面和底面的基础。所有环外环每个内环的顶点必须严格按逆时针CCW顺序排列这决定了面的法向右手定则MakeFace操作会校验叉积normal.z 0否则报错并退出。我故意没做自动纠正因为自动翻转顶点顺序会掩盖学生对“法向约定”的理解漏洞。内环必须完全位于外环内部程序不进行几何包含判断那是计算几何库的事但Sweep算法假设此条件成立。若内环顶点在外环外扫掠出的侧面会自交KillFace时因拓扑非法而崩溃——这恰恰是让学生debug几何关系的好机会。解析过程在Sweep.cpp的parseInputFile()中完成使用std::ifstream逐行读取用std::vectorstd::vectorglm::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_{i1})创建两条新边-e_i_top (v_i → v_{i1})已在顶面中-e_i_bottom (v_i → v_{i1})底面中-e_i_side1 (v_i → v_i)前侧面边-e_i_side2 (v_{i1} → v_{i1})后侧面边- 共创建4*N条新边N为外环顶点数。Step 3构建侧面面- 对每个i用四条半边[e_i_top, e_i_side2, reverse(e_{i1}_bottom), reverse(e_i_side1)]创建一个四边形侧面面。-reverse()操作通过getTwin()获取孪生半边实现确保边的方向正确。Step 4缝合Splice——最关键的一步- 将top_face的外环半边he_top_i的next指向side_face_i的首半边。- 将side_face_i的末半边next指向bottom_face的外环半边he_bottom_i需反转顺序因底面法向向下。- 将bottom_face的外环半边next指向side_face_{i-1}的某半边形成闭环。- 对内环逻辑相同但需额外处理“孔侧面”的缝合且内环的Splice必须在top_face和bottom_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个内环N1个侧面组1个外侧面连接外环 N个内侧面每个内环对应一个孔侧面关键挑战在于面的环结构管理。Face结构体中的inner_loops是vectorvectorHalfEdge*每个内环是一个独立的半边向量。在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里充斥着glVertexAttribPointer和MakeVertex混杂的代码。本项目强制分离Draw.h只做一件事——把内存中的半边结构翻译成GPU能理解的顶点缓冲区VBO和索引缓冲区IBO。Draw.h的核心函数是updateMeshFromHalfEdge(const HalfEdgeDataStructure heds)void updateMeshFromHalfEdge(const HalfEdgeDataStructure heds) { std::vectorglm::vec3 vertices; std::vectorunsigned int indices; // Step 1: 遍历所有面提取顶点坐标去重 std::mapsize_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立刻知道v6和v7在输入时标错了索引。或者当Edges: 24 / 48中48不是24*2说明有半边twin未正确设置——这是Splice漏写的铁证。我坚持用printf而非std::cout因为printf在Windows控制台输出更稳定且格式化字符串%zu能精准控制索引对齐方便肉眼比对。5. 工程实践与避坑指南那些README里不会写的血泪经验5.1 编译环境VS2019 GLFW/GLAD/GLM为什么是这个组合VS2019课程机房普遍安装C17支持完善std::optional,if constexpr且.vcxproj配置成熟。我刻意避免C20特性确保学生能在旧版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生成Makefilehello_opengl.slnWindows直接打开即可。所有路径在.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.cpp中processInput()未被调用在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.filters中EulerOperation.cpp是否在Source Files下用grep -r MakeVertex src/确认声明与定义一致5.3 课程设计延伸建议从“能跑”到“能用”这个项目是很好的起点但绝非终点。根据往届学生反馈以下延伸方向既保持难度可控又能深化理解添加布尔运算基于半边结构实现Union并集。核心是SplitEdge操作将一条边在交点处分割再用Splice重组。难点在于交点计算可用glm::intersectRayTriangle近似。支持STL文件导入/导出将半边结构序列化为ASCII STLfacet 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_idx和twin_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视角平移渲染结果直观验证拓扑正确性。控制台同步输出每个面的环结构与顶点顺序便于调试。工程已预配置GLFWGLADGLM依赖VS2019一键编译含完整readme.md、底面示意图、渲染效果图、opengl库文件及build.sh脚本开箱即可运行适用于高校CAD课程设计、大作业或轻量级几何建模原型开发。本文还有配套的精品资源点击获取