ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

UE5动态剖切效果实现:程序化网格与实时几何切割详解

2026/8/8 16:05:02 拓冰建站 浏览量
UE5动态剖切效果实现:程序化网格与实时几何切割详解

1. 项目概述:在UE5中实现动态剖切效果

在虚幻引擎5(UE5)的项目开发中,尤其是涉及建筑可视化、医疗模拟、机械拆解或特效展示时,我们常常会遇到一个核心需求:如何动态地“切开”一个三维模型,直观展示其内部结构?这个需求的专业术语叫做“剖切”(Sectioning或Clipping)。传统的做法是预先在三维建模软件中制作好剖切面的静态模型,但这完全丧失了交互性。而今天要探讨的,则是利用UE5的程序化网格体组件,在运行时动态生成剖切面,实现真正的实时、可交互剖切功能。

简单来说,这个项目的目标就是:给定一个任意形状的静态网格体(Static Mesh)和一个平面(剖切面),我们能在游戏中实时计算出该网格体被此平面切割后形成的剖面,并将其渲染出来。这不仅仅是隐藏一半模型那么简单,它需要精确的几何计算,生成代表剖面轮廓的新的网格体,并为其赋予合适的材质(比如显示内部结构的材质)。这项技术能极大地增强应用的沉浸感和专业性,比如让玩家在游戏中“解剖”一台机器,或者让设计师动态查看建筑内部的管线布局。

实现这一功能的核心挑战在于几何处理。我们需要获取原始模型的三角形数据,与剖切平面进行求交运算,对三角形进行分割、重组,最终生成新的、表示剖面的三角形集合。幸运的是,UE5提供了强大的底层几何库和灵活的组件系统,让我们不必从零开始实现复杂的数学算法。接下来,我将拆解整个实现流程,从设计思路到代码细节,并分享我在实际开发中踩过的坑和总结的经验。

2. 核心思路与架构设计

在动手写代码之前,我们必须明确几个关键的设计决策,这直接决定了后续实现的复杂度和最终性能。

2.1 为什么选择程序化网格体组件?

UE5中用于动态生成网格的组件主要有两个历史选择:UProceduralMeshComponent (PMC)UStaticMeshComponent (SMC)结合运行时构建。此外,在编辑器工具集中还有一个USimpleDynamicMeshComponent (SDMC)

  • UProceduralMeshComponent (PMC):这是最直接的选择。它专为运行时动态生成和更新网格数据而设计。其API(如CreateMeshSection)允许我们直接提交顶点、三角形索引、法线、UV等数据。对于每帧都可能变化的剖切面,PMC的“动态绘制”路径虽然每帧提交数据有一定开销,但更新非常灵活。一个重要的限制是,PMC不支持在共享顶点处拆分顶点属性(如法线、UV),这意味着如果剖面需要不同的UV或法线(例如,剖面材质需要独特的UV展开),我们需要手动处理顶点复制。
  • UStaticMeshComponent (SMC) + 运行时构建:从UE4.25开始,可以通过UStaticMesh::BuildFromMeshDescriptions()在运行时创建或更新UStaticMesh。其优势是渲染性能高(“静态绘制”路径),适合剖切面生成后相对静止的场景。但每次更新网格都需要调用Build,开销比PMC的单次数据提交要大得多,不适合高频更新。
  • USimpleDynamicMeshComponent (SDMC):它存储FDynamicMesh3,这是一种支持属性拆分的更丰富的网格表示,非常适合编辑操作。它同样使用动态绘制。但它是MeshModelingTools插件的一部分,主要用于编辑器模式,在打包的游戏项目中默认不可用,增加了项目依赖和分发复杂度。

我的选择与理由:对于大多数游戏或实时应用中的剖切功能,UProceduralMeshComponent是平衡度最高的选择。它的API直观,无需处理复杂的FMeshDescription转换,并且天然支持运行时高频更新。虽然SDMC的FDynamicMesh3在几何处理上更强大,但PMC的轻量化和易用性使其成为本项目更稳妥的起点。因此,本方案将围绕PMC展开。

2.2 剖切算法流程设计

整个剖切功能可以分解为以下几个步骤:

  1. 数据获取:从目标UStaticMeshComponent中提取原始的顶点和三角形数据。这需要访问其渲染资源或使用UProceduralMeshComponentCopyProceduralMeshFromStaticMeshComponent函数(注意:此函数可能无法获取所有LOD或自定义数据)。
  2. 空间变换:将获取的模型空间顶点数据,统一变换到世界空间(或一个统一的参考空间),以便与世界空间中定义的剖切平面进行求交计算。
  3. 平面求交与三角形分类:遍历每个三角形,计算其三个顶点到剖切平面的有符号距离。根据距离的正负,将三角形分类为:完全在平面正面(保留)、完全在平面背面(丢弃)、与平面相交(需要切割)。
  4. 相交三角形处理:这是核心难点。对于被平面穿过的三角形,需要计算边与平面的交点,生成新的顶点。根据相交情况(一个顶点在正面两个在背面,或两个在正面一个在背面),将原三角形分割为1个四边形(进而拆分为2个三角形)或2个三角形。同时,需要为新顶点插值计算法线、UV、顶点色等属性。
  5. 剖面轮廓生成:除了处理被切割的模型本身,我们还需要生成代表切割面的那个“剖面”。这需要收集所有相交多边形(在步骤4中生成的新四边形或三角形的边),并从中提取一个闭合的轮廓环,然后将其三角化(通常处理为凸多边形或进行耳切法三角剖分)形成一个平面网格。
  6. 网格更新与渲染:将处理后的三角形数据(被切割后保留的部分)提交给一个PMC进行渲染。同时,将生成的剖面网格数据提交给另一个专用的PMC,并为其赋予一个半透明或高亮的材质,以清晰显示内部结构。

注意:直接修改原始StaticMeshComponent的数据是不可行且不推荐的。我们的策略是“先复制,后处理,再显示”。即复制原始网格数据到一个PMC进行处理和渲染,原始模型可以隐藏或保留。

2.3 系统架构蓝图

基于以上思路,我设计了一个简单的Actor蓝图结构:

  • 父Actor:包含原始UStaticMeshComponent(作为切割目标)和一个USceneComponent作为剖切平面的位置/旋转参考。
  • 子组件
    • UProceduralMeshComponent (ClippedMesh):用于渲染被切割后剩余的模型部分。
    • UProceduralMeshComponent (SectionMesh):专门用于渲染生成的剖面。
    • 一个UBoxComponentUPlaneComponent(仅用于编辑器可视化)代表剖切平面。
  • 逻辑:在Tick或响应事件时,获取剖切平面(从SceneComponent派生),执行上述算法,更新ClippedMeshSectionMesh

这种分离使得我们可以独立控制剖面材质、是否显示剖面等功能,非常灵活。

3. 关键技术细节与实现解析

接下来,我们深入到代码层面,看看每一步具体如何实现,以及有哪些需要特别注意的“魔鬼细节”。

3.1 获取原始网格数据

我们不能直接从UStaticMesh在运行时获取易用的三角形列表。可靠的方法是使用UProceduralMeshComponentCopyProceduralMeshFromStaticMeshComponent函数。但为了更底层控制或处理复杂属性,我们可以通过渲染接口获取。

这里提供一个更通用的函数,用于从UStaticMesh获取位置、法线、UV和索引数据:

bool GetMeshDataFromStaticMesh(UStaticMesh* StaticMesh, int32 LODIndex, TArray<FVector>& Vertices, TArray<int32>& Triangles, TArray<FVector>& Normals, TArray<FVector2D>& UVs, TArray<FProcMeshTangent>& Tangents) { if (!StaticMesh || LODIndex >= StaticMesh->GetNumLODs()) return false; Vertices.Empty(); Triangles.Empty(); Normals.Empty(); UVs.Empty(); Tangents.Empty(); FPositionVertexBuffer* PositionVertexBuffer = nullptr; FStaticMeshVertexBuffer* StaticMeshVertexBuffer = nullptr; FRawStaticIndexBuffer* IndexBuffer = nullptr; FStaticMeshLODResources& LODResource = StaticMesh->GetLODForExport(LODIndex); // 获取顶点缓冲区 PositionVertexBuffer = &LODResource.VertexBuffers.PositionVertexBuffer; StaticMeshVertexBuffer = &LODResource.VertexBuffers.StaticMeshVertexBuffer; IndexBuffer = &LODResource.IndexBuffer; if (!PositionVertexBuffer || !StaticMeshVertexBuffer || !IndexBuffer) return false; // 获取顶点数据 int32 NumVertices = PositionVertexBuffer->GetNumVertices(); for (int32 i = 0; i < NumVertices; i++) { Vertices.Add(PositionVertexBuffer->VertexPosition(i)); // 注意:这里是模型空间坐标 Normals.Add(StaticMeshVertexBuffer->VertexTangentZ(i)); UVs.Add(StaticMeshVertexBuffer->GetVertexUV(i, 0)); // 获取第一套UV // 切线可能也需要,这里简化处理 Tangents.Add(FProcMeshTangent(StaticMeshVertexBuffer->VertexTangentX(i), false)); } // 获取索引数据(三角形列表) int32 NumTriangles = IndexBuffer->GetNumIndices() / 3; for (int32 i = 0; i < NumTriangles; i++) { Triangles.Add(IndexBuffer->GetIndex(i * 3)); Triangles.Add(IndexBuffer->GetIndex(i * 3 + 1)); Triangles.Add(IndexBuffer->GetIndex(i * 3 + 2)); } return true; }

实操心得:直接访问渲染缓冲区虽然高效,但代码较为底层,且需要注意线程安全(最好在游戏线程进行)。对于快速原型,CopyProceduralMeshFromStaticMeshComponent是更安全简单的选择,但它可能只复制第一套UV和顶点颜色,且不复制切线。你需要根据剖面材质的需要决定采用哪种方式。如果剖面材质需要法线信息来实现光照,那么复制法线数据是必须的。

3.2 平面求交与顶点分类

定义一个剖切平面,通常使用FPlane(由法线N和到原点的距离D表示,即 N·X = D)。对于每个顶点V,计算有符号距离:Distance = FVector::DotProduct(V, Plane.N) - Plane.D

  • Distance > 0:顶点在平面正面(我们想要保留的一侧)。
  • Distance < 0:顶点在平面背面(被切掉的一侧)。
  • Distance ≈ 0:顶点在平面上(视为正面,避免数值精度问题)。

遍历每个三角形,根据三个顶点的距离符号,可以将其分为三类:

  1. 全部正面:三角形完全保留。
  2. 全部背面:三角形完全丢弃。
  3. 有正有负:三角形被平面切割,需要处理。

3.3 处理相交三角形与生成新几何体

这是算法中最复杂的部分。假设三角形ABC,顶点A、B、C到平面的距离分别为dA, dB, dC。

情况1:一个顶点在正面,两个在背面(1-2分割)假设dA > 0, dB < 0, dC < 0。则边AB和AC与平面相交。设交点为M(在AB上)和N(在AC上)。 我们需要生成新的三角形:保留原来的正面部分,即三角形AMN。同时,四边形MBCN被丢弃(在背面)。但是,为了生成剖面,我们需要记录线段MN。

情况2:两个顶点在正面,一个在背面(2-1分割)假设dA > 0, dB > 0, dC < 0。则边AC和BC与平面相交。设交点为M(在AC上)和N(在BC上)。 原三角形ABC被分割为两个三角形:ABN 和 ABN? 不对,应该是四边形ABNM被分割为三角形ABN和三角形AMN?这里需要仔细处理。实际上,我们得到的是四边形ABNM,需要将其三角化为三角形ABN和三角形AMN。同时,线段MN是剖面轮廓的一部分。

交点计算:使用线性插值。对于边AB,交点M = A + (B - A) * (t),其中 t = dA / (dA - dB)。这里t是A到交点的比例,因为dA是正数,dB是负数,dA - dB > 0,计算安全。

属性插值:新顶点M的位置通过上述公式得到。同时,它的法线、UV、顶点色也需要通过同样的比例t在A和B之间进行线性插值。

FVector IntersectPoint = VertexA + (VertexB - VertexA) * (DistA / (DistA - DistB)); FVector IntersectNormal = NormalA + (NormalB - NormalA) * (DistA / (DistA - DistB)); FVector2D IntersectUV = UVA + (UVB - UVA) * (DistA / (DistA - DistB)); // 确保法线单位化 IntersectNormal.Normalize();

注意事项:分母(DistA - DistB)可能为零(即两个顶点都在平面上),在实际代码中必须加入容错判断,例如if (FMath::Abs(DistA - DistB) < KINDA_SMALL_NUMBER),则可以直接取中点或任一顶点。

3.4 生成剖面网格

剖面轮廓的生成是关键且容易出错的一步。在上一步中,我们每处理一个相交三角形,就会得到一条或多条位于剖切平面上的线段(如上面的MN)。所有这些线段构成了一个或多个闭合的多边形环(如果模型有内部空腔,可能不止一个环)。

步骤

  1. 收集所有边:将每条线段(两个端点)存储起来。注意,需要定义一个容差,将距离极近的点视为同一点,避免浮点误差导致轮廓无法闭合。
  2. 连接成环:这是一个图论问题。将线段端点视为顶点,线段视为边。从一个未处理的顶点开始,寻找与之相连的另一条线段,依次连接,直到回到起点,形成一个环。如果还有未处理的顶点,说明有多个轮廓环(例如切一个空心球体)。
  3. 三角化:将一个平面上的多边形环三角化。对于凸多边形,可以直接连接第一个顶点与其他非相邻顶点形成三角形扇。对于凹多边形,则需要更复杂的算法,如耳切法(Ear Clipping)。UE5的几何处理模块(GeometryProcessing)提供了强大的多边形处理工具,例如FPolygon2f类,可以辅助进行三角化。
  4. 创建剖面网格:将轮廓环上的顶点(已在平面空间)提取出来,使用三角化得到的索引,构建新的三角形列表。这些顶点的法线应统一为剖切平面的法线(或反向,取决于你想显示哪一面),UV可以根据顶点在平面上的投影坐标生成。

由于这部分计算量较大且逻辑复杂,如果性能要求高,可以考虑将轮廓生成和三角化放在异步任务中计算。

3.5 更新程序化网格体组件

一旦我们有了:

  • ClippedVertices,ClippedTriangles,ClippedNormals,ClippedUVs(被切割后保留的模型部分)
  • SectionVertices,SectionTriangles,SectionNormals,SectionUVs(剖面部分)

就可以更新对应的PMC了:

// 更新被切割的模型 ProceduralMeshComponent->ClearAllMeshSections(); ProceduralMeshComponent->CreateMeshSection(0, ClippedVertices, ClippedTriangles, ClippedNormals, ClippedUVs, TArray<FVector2D>(), TArray<FColor>(), TArray<FProcMeshTangent>(), true); // 设置材质 ProceduralMeshComponent->SetMaterial(0, ClippedMaterial); // 更新剖面 SectionMeshComponent->ClearAllMeshSections(); SectionMeshComponent->CreateMeshSection(0, SectionVertices, SectionTriangles, SectionNormals, SectionUVs, TArray<FVector2D>(), TArray<FColor>(), TArray<FProcMeshTangent>(), true); // 剖面通常使用半透明或发光材质 SectionMeshComponent->SetMaterial(0, SectionMaterial);

重要提示CreateMeshSection的最后一个参数bCreateCollision决定了是否为该段网格生成碰撞。对于动态更新的剖切面,通常设置为false以避免每帧生成碰撞带来的性能开销。如果需要碰撞检测,可以考虑使用其他简化形状(如剖切平面本身)来代替。

4. 性能优化与常见问题

实现基础功能后,性能往往是下一个挑战,尤其是对于高面数模型。

4.1 性能优化策略

  1. 减少计算频率:除非剖切平面在持续移动,否则不需要每帧(Tick)都进行完整的剖切计算。可以将计算逻辑绑定到剖切平面变换事件(OnTransformUpdated)上,或者由玩家操作手动触发。
  2. 空间划分与粗略剔除:在遍历三角形之前,先判断模型的包围盒(Bounds)与剖切平面的关系。如果整个包围盒都在平面正面或背面,则无需进行复杂的逐三角形计算,直接显示或隐藏整个模型即可。
  3. 并行处理:三角形分类和求交计算是相互独立的,非常适合并行化。可以使用UE提供的ParallelFor来加速处理过程。但要注意,向动态数组(如TArray)添加新顶点/索引时需要加锁或使用线程本地存储后再合并。
    TArray<FVector> ParallelNewVertices; ParallelNewVertices.Reserve(EstimatedCount); FCriticalSection CriticalSection; ParallelFor(Triangles.Num() / 3, [&](int32 TriangleIndex) { // ... 处理三角形计算 ... // 当需要写入共享数组时 CriticalSection.Lock(); ParallelNewVertices.Append(NewVertsForThisTriangle); CriticalSection.Unlock(); });
  4. 细节层次(LOD):对距离摄像机较远的被切割物体,使用其低LOD模型进行计算,可以显著减少三角形数量。
  5. 剖面网格简化:生成的剖面轮廓可能非常复杂(尤其是切割曲面时)。在轮廓三角化后,可以对剖面网格进行轻度的网格简化,减少不必要的三角形,这对性能提升和视觉效果都有好处。

4.2 常见问题与排查技巧

  1. 剖面闪烁或撕裂:这是最常见的问题,通常由两个原因导致:
    • 数值精度:在计算交点t = dA / (dA - dB)时,如果分母接近零,t值会不稳定。必须添加容错处理,当FMath::Abs(dA - dB) < SMALL_NUMBER时,直接取端点或中点。
    • 轮廓不闭合:由于浮点误差,收集到的轮廓线段端点可能无法精确连接。必须在连接前对顶点进行“焊接”(Welding),将距离小于某个阈值(如1e-4)的点视为同一个点。
  2. 法线/光照错误:新生成的顶点(交点)的法线是通过插值得到的,这通常是正确的。但剖面网格的法线需要统一设置为平面法线。如果设置错误,会导致剖面在光照下显示为黑色或高光异常。确保剖面顶点法线归一化,且方向正确(通常朝向摄像机或正面方向)。
  3. UV错乱:插值得到的UV对于切割后模型通常是连续的。但对于剖面,我们需要为其生成新的UV。一个简单有效的方法是使用顶点在剖切平面上的局部2D坐标作为UV。例如,以轮廓的中心为UV原点,轮廓的包围盒大小进行缩放。
  4. 材质ID丢失:如果原始模型有多个材质槽(Material Slots),我们的简单实现可能会丢失这些信息。更完善的实现需要跟踪每个三角形所属的材质索引,并在切割后为新生成的三角形分配正确的材质ID。在PMC中,这可以通过创建多个Mesh Section(每个Section对应一个材质)来实现。
  5. 性能瓶颈定位:使用UE5的Profiler(Stat Unit, Stat Game)来定位是CPU端的三角形处理耗时,还是GPU端的渲染压力。如果是CPU瓶颈,优化算法和引入并行计算;如果是GPU瓶颈,考虑减少剖面网格的面数或使用更简单的材质。

一个实用的调试技巧:在开发阶段,可以创建一个调试绘制(Debug Draw)功能,将计算出的交点、轮廓线段实时绘制在屏幕上。这能帮助你直观地验证算法每一步的正确性,快速定位是交点计算错误还是轮廓连接逻辑有误。

5. 高级扩展与实战应用

基础剖切功能实现后,可以在此基础上进行很多有趣的扩展,提升效果和实用性。

5.1 多平面剖切与布尔运算

单个平面剖切只是开始。我们可以扩展算法,支持多个剖切平面(例如,一个立方体框),实现“剖切盒”效果,只显示盒子内部的模型部分。这本质上是一个CSG(构造实体几何)布尔差集的简化版:模型与剖切盒(定义为多个平面构成的凸体)做差集运算。

实现思路是迭代处理:用第一个平面对模型进行切割,保留正面部分;然后将结果作为输入,用第二个平面再次切割,如此反复。这个过程需要仔细管理中间网格数据,但核心算法与单平面相同。UE5的GeometryProcessing插件提供了完整的网格布尔运算功能,如果项目允许依赖此插件,直接调用其API是更强大和稳定的选择。

5.2 动态剖面材质与特效

剖面材质是视觉效果的关键。除了使用简单的纯色或半透明材质,还可以:

  • 使用视差遮挡映射(POM)或法线贴图:让剖面看起来有粗糙的切割痕迹或内部材质纹理,增强真实感。
  • 边缘高光:在剖面轮廓边缘添加发光效果。这可以在材质中通过检测像素在模型中的位置(比如使用世界空间法线或自定义顶点数据)来实现,或者在生成剖面网格时,额外生成一条沿着轮廓的带状网格并赋予自发光材质。
  • 渐隐动画:当剖切面移动时,可以让被切掉的部分不是突然消失,而是有一个渐隐(Fade Out)的动画。这需要保留被切割部分的网格,并随时间动态改变其材质透明度。

5.3 与物理和导航的交互

默认生成的PMC碰撞是关闭的。如果你需要剖面也能参与物理模拟或角色导航,会面临挑战:

  • 物理碰撞:为动态生成的复杂剖面生成精确碰撞体开销很大。一个折中方案是,不为剖面生成复杂碰撞,而是使用剖切平面本身作为一个简单的碰撞平面(如一个UBoxComponent),来近似处理交互。例如,子弹击中剖面平面时触发事件。
  • 导航网格:动态剖切会改变场景的可行走区域。要让AI适应,需要在剖切发生后,局部更新或重新生成导航网格(NavMesh)。UE5提供了NavMeshBoundsVolume和导航系统接口,可以在运行时标记需要更新的区域。

5.4 封装为蓝图节点与组件

为了让美术和策划也能方便地使用此功能,最好的方式是将核心C++逻辑封装成蓝图可调用的函数库或一个完整的Actor组件。

  1. 创建UBlueprintFunctionLibrary:将剖切计算函数(输入:StaticMesh Component引用,剖切平面参数;输出:成功与否)暴露为蓝图节点。
  2. 创建UClipingComponent:继承自UActorComponent。将其添加到任意Actor上,指定目标Mesh组件和剖切平面组件。该组件内部管理PMC的创建、更新和销毁。通过暴露一些变量(如剖面材质、是否启用)和事件(如剖切完成时),可以非常灵活地进行集成。

这样,在关卡中,设计师只需要拖拽一个“剖切器”Actor,将其目标指向需要切割的模型,然后通过动画蓝图或序列器控制剖切平面的移动,就能轻松创建出复杂的剖切动画序列。

6. 替代方案与引擎模块选择

虽然我们详细介绍了基于PMC的实现,但了解其他方案及其适用场景同样重要。

方案一:使用Geometry Processing插件(FDynamicMesh3)这是最强大、最专业的方案。GeometryProcessing插件提供了完整的FDynamicMesh3数据结构以及网格布尔、切割、镶嵌等高级操作。你可以将UStaticMesh转换为FDynamicMesh3,调用FDynamicMeshEditor::CutMeshWithPlane之类的函数,轻松完成切割,然后再转换回PMC需要的数据格式。这种方法代码更简洁,功能更健壮(能处理复杂拓扑和孔洞),但需要项目包含并加载该插件,增加了打包体积和依赖。

方案二:自定义渲染通道(Stencil Buffer/Clip Planes)这是一个完全不同的思路:不修改几何体,而是利用渲染器的裁剪平面功能。在材质中或通过渲染设置,可以启用Clip Planes,直接让GPU丢弃指定平面另一侧的像素。这种方法性能极高(几乎零开销),实现简单。但缺点也非常明显:

  1. 它只影响视觉效果,模型的碰撞体、射线检测等依然完整存在。
  2. 无法生成剖面几何体,你看到的切口是“空”的,无法赋予内部材质。
  3. 在UE中,每个模型可能需要单独设置材质来响应裁剪平面,管理起来麻烦。 因此,这种方法仅适用于只需要视觉剖切且不关心剖面效果的简单场景。

方案三:预计算与蒙皮对于已知的、固定的剖切动画(如机器盖子打开),最高效和质量最好的方法是让美术师预先制作好动画。将模型在三维软件中切开,分别导出,然后在UE中通过骨骼动画或变形目标(Morph Target)来控制切开和闭合。这能获得最好的美术效果和性能,但完全丧失了动态性和程序化控制。

如何选择?

  • 需要完全动态、可交互的剖切,且要看到剖面->本文的PMC方案Geometry Processing插件方案
  • 只需要简单的视觉切割,性能至上,且不要剖面->裁剪平面方案
  • 剖切路径固定,追求最高视觉质量->预制作动画方案

我个人在多个工业可视化项目中采用了基于PMC的方案。它的优势在于平衡:无需额外插件依赖,功能完全自控,能生成剖面,性能在优化后可以满足实时交互需求(面对数千面级别的模型)。当遇到非常复杂的模型或需要更稳健的布尔运算时,我会考虑引入GeometryProcessing插件作为升级方案。