Unity中Quickhull算法实战:高效生成凸包与性能优化指南
1. 项目概述Unity中的Quickhull算法在Unity开发中尤其是涉及到物理模拟、碰撞检测、地形生成或者程序化建模时我们常常会遇到一个核心需求如何从一堆无序的、三维空间中的点快速生成一个包裹它们的最小凸多面体这个多面体在几何学上被称为“凸包”。想象一下你有一把散落在桌上的弹珠凸包就是那个能装下所有弹珠、且表面没有凹陷的最紧实的橡皮膜。在游戏里这可以用来为复杂的岩石模型生成一个简化的碰撞体或者为一片森林生成一个动态的边界体积。手动计算凸包是极其繁琐的好在有成熟的算法。其中Quickhull算法因其在三维空间中的高效表现而备受青睐。它得名于著名的快速排序算法思路也类似通过递归地“丢弃”明显位于凸包内部的点来快速逼近最终结果。OskarSigvardsson在GitHub上开源的unity-quickhull项目就是一个专为Unity环境优化、纯C#实现的Quickhull算法库。它不依赖任何Unity引擎特有的API除了最终的Mesh生成因此计算部分甚至可以放到子线程中执行这对于性能敏感的游戏来说是个福音。然而就像任何集成到具体项目中的第三方工具一样直接拿来用和用得好、用得稳是两回事。我在多个涉及程序化生成和物理优化的项目中使用过这个库过程中踩过不少坑也总结出了一套让它在Unity里跑得又快又稳的“组合拳”。这篇文章我就来聊聊unity-quickhull项目在实际应用中那些最常见的问题以及经过实战检验的解决方案。无论你是想用它来生成随机岩石还是为动态物体创建近似碰撞体这些经验都能帮你省下大量调试时间。2. 核心原理与常见问题根源剖析要解决问题首先得理解问题从哪来。unity-quickhull的核心是一个名为ConvexHullCalculator的类。你喂给它一个ListVector3的点云它吐给你三个列表顶点、三角形索引和法线直接用来构造Unity的Mesh。听起来很简单但魔鬼藏在细节里。2.1 算法输入的门槛与“异常”库的README里明确提到了两个会导致抛出异常的条件点数少于4个或者所有点共面。这背后有坚实的数学原因在三维空间中一个具有体积的凸多面体至少需要4个不共面的顶点即一个四面体。如果点都在同一个平面上那生成的就是一个二维多边形而不是三维凸包。算法内部的一些几何计算比如计算有向体积在共面情况下会失效或产生歧义。在实际项目中这两个条件比你想象中更容易触发。比如动态点云你的点云可能来自实时采样如角色周围一定范围内的物体顶点。当角色处于空旷地带时采样点可能瞬间少于4个。数据清洗不彻底从模型顶点导入的点云可能包含大量重复或极其接近的点经过某些处理后可能意外地变得“近似共面”。生成算法缺陷你自己生成随机点的方法如果没有妥善处理分布可能会产生所有点都落在同一个平面附近的情况例如在一个极扁的椭球体内生成点。当异常抛出时如果不做处理游戏就会崩溃。因此输入数据的预处理和校验是使用unity-quickhull的第一道也是最重要的防线。2.2 性能与内存管理的隐形陷阱这个库的一个设计亮点是注重性能它内部重用ListT等缓冲区来避免每次计算都分配新内存垃圾回收GC。但是这个优点需要开发者以正确的方式配合才能发挥。常见误区一频繁创建新的ConvexHullCalculator实例。有些开发者会在每次需要计算凸包时都new ConvexHullCalculator()。这完全违背了库的设计初衷。创建这个对象本身开销不大但失去了缓冲区复用的优势。更糟的是如果你在每帧都这么做比如为大量动态物体计算碰撞体虽然库本身不产生垃圾但你频繁创建和销毁计算器实例的行为本身就会引发GC。常见误区二不重用输出列表。ConvexHullCalculator.GenerateHull方法要求你传入三个ListVector3或Listint来接收结果。最偷懒的做法是每次调用都new三个新列表传进去。这会导致海量的短生命周期列表对象给GC带来巨大压力。正确的做法是创建一次列表然后在每次计算前调用List.Clear()来清空它们再传入进行复用。常见误区三在主线程计算大型点云。尽管计算器本身是纯算法不阻塞Unity主线程但计算一个包含成千上万个点的凸包仍然是CPU密集型任务。如果在主线程执行势必会造成游戏卡顿。库文档提到了可以在后台线程使用但如何安全、便捷地将Unity的Vector3数据传递到子线程并将结果Mesh安全地应用回主线程这里面有一整套线程安全的最佳实践。2.3 生成Mesh的后续处理问题即使算法成功运行并输出了顶点和三角形列表将其转化为Unity中可用的Mesh对象时仍有几个坑等着你。顶点重复与焊接Quickhull算法输出的顶点列表中理论上每个顶点都是唯一的。但如果你需要将这个Mesh用于物理碰撞如MeshCollider有时为了得到更“坚固”的碰撞体或者进行进一步的简化可能需要进行顶点焊接确保没有位置极其接近的重复顶点。法线朝向库同时输出了法线这很棒。但如果你生成的Mesh用于渲染你需要确保三角形绕序是统一的通常是顺时针或逆时针以便法线朝向正确。虽然库会计算法线但在某些极端或退化情况下可能需要手动重新计算或反转法线。缩放与变换算法处理的是原始点数据。如果你的点云是在世界空间采集的但你想将生成的Mesh作为一个子物体的网格就需要考虑坐标变换。通常更好的做法是在局部空间计算凸包这样生成的Mesh可以直接使用。3. 实战解决方案从数据准备到Mesh生成理解了问题根源我们就可以构建一套完整的解决方案。下面我将分步骤拆解并提供可直接粘贴使用的代码片段。3.1 输入数据的预处理与健壮性封装我们不能相信任何传入的点云数据。必须建立一个防御层。这里我封装了一个ConvexHullHelper静态类它负责处理所有脏活累活。using System.Collections.Generic; using UnityEngine; public static class ConvexHullHelper { // 一个可全局复用的计算器实例 private static ConvexHullCalculator _calculator; static ConvexHullHelper() { _calculator new ConvexHullCalculator(); } /// summary /// 安全地生成凸包Mesh。如果点云不合法返回null。 /// /summary /// param nameinputPoints原始点云/param /// param nameresultVertices复用顶点列表/param /// param nameresultTriangles复用三角形索引列表/param /// param nameresultNormals复用法线列表/param /// returns成功生成的Mesh或null/returns public static Mesh GenerateConvexHullMeshSafe( IListVector3 inputPoints, ListVector3 resultVertices, Listint resultTriangles, ListVector3 resultNormals) { // 1. 基础检查 if (inputPoints null || inputPoints.Count 4) { Debug.LogWarning($[QuickHull] 输入点数量不足4个: {inputPoints?.Count}); return null; } // 2. 数据清洗去除重复或过于接近的点 ListVector3 cleanedPoints RemoveDuplicatePoints(inputPoints, 0.001f); if (cleanedPoints.Count 4) { Debug.LogWarning($[QuickHull] 清洗后点数量不足4个。); return null; } // 3. 共面性检查简化版 if (ArePointsCoplanar(cleanedPoints, 0.01f)) { Debug.LogWarning($[QuickHull] 点云近似共面无法生成3D凸包。); // 这里可以降级处理例如生成一个2D凸包或返回一个极薄的四面体 // 为了简单本例返回null return null; } // 4. 清空复用列表准备接收数据 resultVertices.Clear(); resultTriangles.Clear(); resultNormals.Clear(); try { // 5. 执行计算 _calculator.GenerateHull(cleanedPoints, true, ref resultVertices, ref resultTriangles, ref resultNormals); // 6. 构建Mesh Mesh mesh new Mesh(); // 对于顶点数非常多的情况考虑使用Mesh API的更高性能版本 mesh.SetVertices(resultVertices); mesh.SetTriangles(resultTriangles, 0); mesh.SetNormals(resultNormals); // 可选重新计算边界对碰撞体很重要 mesh.RecalculateBounds(); return mesh; } catch (System.Exception e) { // 捕获算法可能抛出的其他异常 Debug.LogError($[QuickHull] 生成凸包时发生异常: {e.Message}); return null; } } /// summary /// 移除距离过近的重复点 /// /summary private static ListVector3 RemoveDuplicatePoints(IListVector3 points, float threshold) { ListVector3 uniquePoints new ListVector3(); threshold * threshold; // 使用平方距离比较避免开方运算 for (int i 0; i points.Count; i) { bool isDuplicate false; Vector3 current points[i]; for (int j 0; j uniquePoints.Count; j) { if ((current - uniquePoints[j]).sqrMagnitude threshold) { isDuplicate true; break; } } if (!isDuplicate) { uniquePoints.Add(current); } } return uniquePoints; } /// summary /// 简单共面性检查。通过计算前四个点构成的体积是否接近零来判断。 /// 注意这不是严格的数学证明但对于大多数游戏应用足够了。 /// /summary private static bool ArePointsCoplanar(ListVector3 points, float tolerance) { if (points.Count 4) return true; // 取前四个点 Vector3 a points[0]; Vector3 b points[1]; Vector3 c points[2]; Vector3 d points[3]; // 计算四面体的有向体积 (AB, AC, AD)的标量三重积 / 6 Vector3 ab b - a; Vector3 ac c - a; Vector3 ad d - a; float volume Vector3.Dot(Vector3.Cross(ab, ac), ad) / 6.0f; // 如果体积的绝对值非常小则认为点共面 return Mathf.Abs(volume) tolerance; } }这个封装提供了几个关键保障自动清洗数据去除距离过近的点避免算法因数值精度问题产生不稳定结果。提前进行共面检查在调用可能抛出异常的算法前先进行一轮粗略判断避免崩溃。统一的错误处理所有问题都以日志警告或错误的形式反馈并返回null让调用方决定下一步比如使用一个默认的立方体碰撞体。内部复用计算器静态构造函数确保了全局只有一个ConvexHullCalculator实例最大化缓冲区复用。注意上面的共面检查ArePointsCoplanar只是一个启发式方法。它只检查了前四个点理论上可能存在前四个点不共面但整体点云仍共面的情况。对于要求极高的场景你需要更严格的检查例如使用主成分分析判断点云分布是否近似二维。但对于绝大多数游戏应用这个简单检查已经能拦截99%的问题。3.2 多线程计算与主线程同步对于点数量超过几百个的情况强烈建议在子线程中进行计算。Unity中可以使用System.Threading或更现代的Unity.Collections和Unity.Jobs系统。这里展示一个使用ThreadPool的经典模式它兼容性较好。首先我们需要一个包装类来传递数据和结果using System.Collections.Generic; using UnityEngine; using System.Threading; public class ConvexHullCalculationTask { public Vector3[] InputPoints; public Mesh ResultMesh; public string ErrorMessage; public bool IsDone false; private object _lock new object(); private ListVector3 _reusableVertices new ListVector3(); private Listint _reusableTriangles new Listint(); private ListVector3 _reusableNormals new ListVector3(); public void Execute() { try { // 在子线程中执行计算 ResultMesh ConvexHullHelper.GenerateConvexHullMeshSafe( InputPoints, _reusableVertices, _reusableTriangles, _reusableNormals ); if (ResultMesh null) { ErrorMessage 凸包生成失败点云无效或共面。; } } catch (System.Exception e) { ErrorMessage $凸包计算异常: {e.Message}; ResultMesh null; } finally { lock (_lock) { IsDone true; } } } // 主线程调用检查任务是否完成并获取结果 public bool TryGetResult(out Mesh mesh, out string error) { lock (_lock) { mesh ResultMesh; error ErrorMessage; return IsDone; } } }然后在你的MonoBehaviour管理类中可以这样发起和轮询任务public class DynamicColliderGenerator : MonoBehaviour { private ConvexHullCalculationTask _currentTask; private MeshFilter _targetMeshFilter; void Start() { _targetMeshFilter GetComponentMeshFilter(); } public void StartGenerateHullFromPoints(Vector3[] points) { // 如果已有任务在运行可以选择等待或取消这里简单忽略新请求 if (_currentTask ! null !_currentTask.IsDone) { Debug.Log(上一个凸包计算任务仍在进行中。); return; } _currentTask new ConvexHullCalculationTask(); _currentTask.InputPoints points; // 注意这里进行了浅拷贝。如果points会被修改需要深拷贝。 // 将任务提交到线程池 ThreadPool.QueueUserWorkItem(_ _currentTask.Execute()); } void Update() { // 在主线程每帧检查任务是否完成 if (_currentTask ! null _currentTask.IsDone) { Mesh resultMesh; string error; if (_currentTask.TryGetResult(out resultMesh, out error)) { if (resultMesh ! null) { // 在主线程安全地设置Mesh _targetMeshFilter.mesh resultMesh; Debug.Log(凸包Mesh生成并设置成功。); } else { Debug.LogWarning($凸包生成失败: {error}); // 失败处理例如设置一个默认的球体Mesh } _currentTask null; // 清理任务 } } } void OnDestroy() { // 确保在销毁时没有残留的任务引用 _currentTask null; } }重要提示多线程编程必须小心。确保传入子线程的数据InputPoints在计算完成前不会被主线程修改。上面的例子是浅拷贝数组引用如果原始数组会被改变你需要手动深度拷贝一份Vector3[]再传给任务。此外对Unity引擎对象如Mesh,GameObject的创建和修改必须在主线程进行。我们的代码只在子线程进行纯计算Mesh的实例化是在ConvexHullHelper返回后在主线程的Update中完成的这是线程安全的。3.3 生成Mesh的优化与后处理拿到算法输出的顶点和三角形列表后直接创建Mesh有时还不够。特别是用于MeshCollider时一个优化过的Mesh能显著提升物理性能。1. 顶点焊接可选但推荐虽然Quickhull算法理论上不产生重复顶点但由于浮点数精度问题或者你的输入点云本身就有极其接近的点输出中可能存在“视觉上重复”的顶点。进行顶点焊接可以减少顶点数量简化网格。public static Mesh WeldVertices(Mesh inputMesh, float threshold) { // 这是一个简化的焊接示例。实际项目可能需要更高效的算法如使用空间哈希。 Vector3[] oldVertices inputMesh.vertices; int[] oldTriangles inputMesh.triangles; ListVector3 newVerticesList new ListVector3(); DictionaryVector3, int vertexToIndexMap new DictionaryVector3, int(); int[] newTriangles new int[oldTriangles.Length]; threshold * threshold; // 平方阈值 for (int i 0; i oldTriangles.Length; i) { int oldIndex oldTriangles[i]; Vector3 vertex oldVertices[oldIndex]; int newIndex; bool found false; // 遍历已有新顶点查找是否已存在接近的顶点 // 注意这个循环在顶点很多时效率低仅作演示。生产环境应优化。 foreach (var kvp in vertexToIndexMap) { if ((kvp.Key - vertex).sqrMagnitude threshold) { newIndex kvp.Value; found true; break; } } if (!found) { newIndex newVerticesList.Count; newVerticesList.Add(vertex); vertexToIndexMap[vertex] newIndex; } newTriangles[i] newIndex; } Mesh newMesh new Mesh(); newMesh.SetVertices(newVerticesList); newMesh.SetTriangles(newTriangles, 0); newMesh.RecalculateNormals(); // 焊接后需要重新算法线 newMesh.RecalculateBounds(); return newMesh; }2. 为MeshCollider优化MeshCollider是Unity中性能开销较大的组件。对于凸包生成的Mesh用作凸碰撞体时可以尝试进行轻量级的网格简化但注意过度简化可能改变其凸性。一个更直接有效的优化是仅在必要时更新碰撞体。public class OptimizedConvexHullCollider : MonoBehaviour { private MeshCollider _meshCollider; private Vector3[] _lastPoints; private float _updateInterval 0.5f; // 更新间隔 private float _timer; void Start() { _meshCollider gameObject.AddComponentMeshCollider(); _meshCollider.convex true; // 必须设置为凸的 } void Update() { _timer Time.deltaTime; if (_timer _updateInterval) { _timer 0; Vector3[] currentPoints SamplePointsAroundObject(); // 你的采样方法 if (!HasPointsChangedSignificantly(_lastPoints, currentPoints)) { return; // 点云变化不大跳过更新 } _lastPoints currentPoints; // 使用前面提到的多线程方法生成Mesh并更新Collider StartGenerateHullAndUpdateCollider(currentPoints); } } private bool HasPointsChangedSignificantly(Vector3[] oldPoints, Vector3[] newPoints) { if (oldPoints null || newPoints null) return true; if (oldPoints.Length ! newPoints.Length) return true; // 简单的比较计算点云的中心位移 // 更复杂的比较可以计算包围盒变化或点对平均距离 Vector3 oldCenter CalculateCenter(oldPoints); Vector3 newCenter CalculateCenter(newPoints); return (oldCenter - newCenter).sqrMagnitude 1.0f; // 阈值 } private Vector3 CalculateCenter(Vector3[] points) { Vector3 sum Vector3.zero; foreach (var p in points) sum p; return sum / points.Length; } // ... 集成之前的生成和更新逻辑 ... }这个策略避免了每一帧都重新计算凸包和更新物理引擎对于动态但变化不频繁的物体比如缓慢移动的岩石群非常有效。4. 典型应用场景与避坑指南unity-quickhull库的用武之地很多但每个场景都有其特定的注意事项。4.1 场景一程序化岩石/小行星生成这是README里提到的例子也是最直观的应用。用随机点生成凸包Mesh再贴上岩石材质。操作步骤在一个球体空间内或使用其他形状生成一堆随机点。使用ConvexHullHelper生成凸包Mesh。创建GameObject附加MeshFilter和MeshRenderer并赋值Mesh和材质。如果需要碰撞附加MeshCollider并设置convex true。避坑指南随机点分布不要在球体内均匀随机采样位置这会导致生成的岩石看起来过于“圆润”或“对称”。更好的方法是采用泊松圆盘采样或在球面上叠加噪声让点分布更自然产生更有棱角的岩石。点数量控制点的数量直接决定了生成Mesh的面数。面数太多影响渲染和物理性能太少则不像岩石。建议根据岩石大小LOD细节层次来动态调整点数。例如远景岩石用50个点近景用300个点。缩放与旋转生成基础Mesh后可以对顶点进行一些非线性缩放例如在Y轴上稍微压扁和随机旋转以增加岩石形态的多样性。4.2 场景二为复杂模型生成简化凸碰撞体Unity的MeshCollider在设置为凸Convex时物理引擎如PhysX内部会为其生成一个凸包近似体。但对于极其复杂的网格这个生成过程可能很慢或者结果不理想。我们可以用unity-quickhull在预处理阶段如编辑时或加载时手动生成一个简化版的凸包Mesh然后赋给MeshCollider。操作步骤从原始复杂Mesh的顶点数组中采样。不要直接用所有顶点顶点数太多计算慢且生成的凸包可能过于复杂。可以每隔N个顶点取一个点或者使用更复杂的网格简化算法获取代表性顶点。对采样后的点云计算凸包。将生成的凸包Mesh赋值给一个MeshCollider并勾选Convex。避坑指南采样策略是关键均匀采样可能导致重要特征如尖刺、凹陷的边缘被忽略。可以考虑基于曲率采样在模型弯曲程度大的地方采集更多点。凸性保证unity-quickhull生成的一定是凸包。但如果你在生成后对Mesh进行了变形如顶点着色器动画可能会破坏其凸性导致物理引擎出错。确保最终用于碰撞的Mesh是凸的。内存与性能为每个复杂模型预计算一个凸包Mesh并保存为资产比运行时计算要好。可以将生成的Mesh保存为.asset文件。4.3 场景三动态包围体生成例如你需要实时计算一群鸟Boids模拟的集体包围盒或者为一堆动态堆叠的物体计算一个整体的、近似凸的边界来进行粗略的碰撞检测或视锥体裁剪。操作步骤每一帧或每几帧获取所有目标物体的位置或包围盒的角点。将这些点作为输入计算凸包。使用凸包的顶点列表可以快速计算出它的轴向包围盒AABB或朝向包围盒OBB用于后续逻辑。避坑指南性能热点如果物体数量很多比如成百上千每一帧都计算凸包是不可接受的。必须采用增量更新或延迟更新策略。例如只有当物体的最大位移超过某个阈值时才重新计算凸包。点集稳定性动态场景中物体可能频繁进入或离开群体。这会导致点集剧烈变化可能引发上一节提到的“点数不足4”的异常。务必在你的ConvexHullHelper中做好健壮性处理在群体物体很少时可以回退到使用一个简单的球体或AABB作为包围体。线程安全由于是动态更新多线程计算几乎是必须的。请务必严格遵守前面提到的多线程数据传递规范。5. 进阶技巧与疑难杂症排查即使按照上面的最佳实践操作在某些边缘情况下还是会遇到问题。这里记录一些我踩过的“深坑”和解决办法。5.1 问题生成的Mesh有破面或法线错误现象渲染出来的模型有黑色缝隙或者光照看起来不对劲。排查与解决检查输入点首先确认输入点云是否包含NaN或Infinity值的Vector3。这些非法值会彻底破坏几何计算。在预处理阶段加入检查。验证三角形绕序Unity默认使用顺时针绕序来判定正面。unity-quickhull生成的三角形绕序和法线方向在绝大多数情况下是正确的。但如果出现问题可以尝试在生成Mesh后手动重新计算法线。mesh.RecalculateNormals(); // 这会根据三角形顶点顺序统一计算法线 // 或者如果你确信法线方向全反了 // ReverseNormals(mesh);一个反转所有法线的辅助函数void ReverseNormals(Mesh mesh) { Vector3[] normals mesh.normals; for (int i 0; i normals.Length; i) normals[i] -normals[i]; mesh.normals normals; // 同时需要反转三角形顶点顺序以保持正面朝向 int[] triangles mesh.triangles; for (int i 0; i triangles.Length; i 3) { int temp triangles[i]; triangles[i] triangles[i 2]; triangles[i 2] temp; } mesh.triangles triangles; }检查顶点重复使用前面提到的WeldVertices函数处理一下生成的Mesh有时能解决因浮点精度导致的微小黑缝。5.2 问题计算速度慢卡顿明显现象点云规模较大如超过2000点时即使放在子线程计算一帧的时间也很长影响游戏流畅度。优化策略降低输入点数这是最有效的方法。评估你的应用是否真的需要那么多点来定义凸包。通常200-500个点已经能生成视觉上相当精确的凸包用于碰撞。使用空间划分预筛选在将点传给Quickhull前先进行一次粗略的筛选。例如可以使用八叉树或网格空间划分只保留每个小格子中的一个代表点比如中心点这能在基本保持外形的前提下大幅减少点数。分帧计算如果凸包不需要每帧都极其精确可以实现一个渐进式更新。例如每帧只计算一部分点用多帧的时间来更新最终的凸包。这对于追踪缓慢移动的物体群很有用。算法参数微调有限unity-quickhull的实现本身是固定算法没有暴露可调节的参数来平衡速度与精度。如果速度是瓶颈可以考虑寻找其他凸包算法库有些算法在点集具有特定结构如近似于球体分布时可能更快。5.3 问题与物理引擎PhysX的兼容性问题现象将生成的Mesh赋给MeshCollider并设置为Convex后物体无法发生碰撞或者碰撞行为异常如穿透。排查步骤确认凸性PhysX对凸碰撞体有严格定义。使用MeshCollider的Convex选项时Unity/PhysX内部会进行验证。你可以通过代码简单验证Mesh.isReadable必须为true并且顶点数不能超过PhysX的限制通常是255个顶点。虽然unity-quickhull生成的是数学凸包但PhysX可能有自己的验证规则。确保生成的Mesh顶点数在合理范围例如少于256。检查Mesh数据确保传递给MeshCollider.sharedMesh的Mesh数据是完整的并且mesh.vertices和mesh.triangles不为空或null。缩放问题如果生成Mesh的顶点坐标值非常大例如世界坐标下的值可能会引起PhysX的数值精度问题。尽量在局部空间或归一化的坐标下生成凸包然后通过GameObject的Transform进行缩放。重启Collider有时动态更换Mesh后PhysX的内部状态可能没有立即更新。可以尝试在设置Mesh后禁用再启用MeshCollider组件。MeshCollider mc GetComponentMeshCollider(); mc.sharedMesh generatedMesh; mc.enabled false; mc.enabled true; // 强制刷新物理状态使用PhysX Cooking对于静态或很少变化的凸碰撞体最高效的方式是使用PhysX的“Cooking”功能预烘焙碰撞数据。Unity的Physics.BakeMesh方法可以用于此目的它能生成一个优化过的、PhysX内部格式的碰撞体性能更好。你可以用unity-quickhull生成Mesh然后用Physics.BakeMesh来处理它再将结果赋给MeshCollider。5.4 一个完整的、健壮的用例代码框架最后我将上面所有要点整合成一个更完整、更健壮的ConvexHullManager单例类框架。它管理一个全局计算器、处理多线程、包含后处理并提供了简单的API。using System.Collections.Generic; using UnityEngine; using System.Threading; using System.Collections.Concurrent; public class ConvexHullManager : MonoBehaviour { public static ConvexHullManager Instance { get; private set; } private ConvexHullCalculator _calculator; private ConcurrentQueueHullTaskResult _resultQueue new ConcurrentQueueHullTaskResult(); [Tooltip(点云中两点距离小于此值将被视为重复点)] public float duplicateThreshold 0.001f; [Tooltip(共面性检查容忍度)] public float coplanarTolerance 0.01f; [Tooltip(是否对生成的Mesh进行顶点焊接)] public bool weldVertices true; [Tooltip(顶点焊接阈值)] public float weldThreshold 0.0001f; [System.Serializable] public class HullTaskResult { public int taskId; public Mesh mesh; public string error; public System.ActionMesh, string callback; // 完成回调 } private int _taskCounter 0; private Dictionaryint, HullTaskResult _pendingTasks new Dictionaryint, HullTaskResult(); void Awake() { if (Instance ! null Instance ! this) { Destroy(this); return; } Instance this; _calculator new ConvexHullCalculator(); DontDestroyOnLoad(this.gameObject); // 可选跨场景使用 } void Update() { // 在主线程处理完成的任务 HullTaskResult result; while (_resultQueue.TryDequeue(out result)) { if (result.callback ! null) { result.callback(result.mesh, result.error); } _pendingTasks.Remove(result.taskId); } } /// summary /// 提交一个凸包计算任务异步 /// /summary public int RequestConvexHull(Vector3[] points, System.ActionMesh, string callback) { int taskId _taskCounter; var taskResult new HullTaskResult { taskId taskId, callback callback }; _pendingTasks.Add(taskId, taskResult); ThreadPool.QueueUserWorkItem(_ { ExecuteHullTask(taskId, points); }); return taskId; } private void ExecuteHullTask(int taskId, Vector3[] points) { HullTaskResult result new HullTaskResult { taskId taskId }; ListVector3 verts new ListVector3(); Listint tris new Listint(); ListVector3 norms new ListVector3(); try { // 1. 预处理点云 ListVector3 cleanedPoints PreprocessPoints(points); if (cleanedPoints.Count 4) { result.error $清洗后点数不足: {cleanedPoints.Count}; _resultQueue.Enqueue(result); return; } // 2. 计算凸包 _calculator.GenerateHull(cleanedPoints, true, ref verts, ref tris, ref norms); // 3. 构建Mesh Mesh mesh new Mesh(); mesh.SetVertices(verts); mesh.SetTriangles(tris, 0); mesh.SetNormals(norms); mesh.RecalculateBounds(); // 4. 后处理焊接 if (weldVertices weldThreshold 0) { mesh WeldVertices(mesh, weldThreshold); } result.mesh mesh; } catch (System.Exception e) { result.error $计算过程异常: {e.Message}; result.mesh null; } finally { _resultQueue.Enqueue(result); } } private ListVector3 PreprocessPoints(Vector3[] points) { // 这里可以集成之前写的RemoveDuplicatePoints和ArePointsCoplanar检查 // 为简洁省略具体实现 ListVector3 cleaned RemoveDuplicatePoints(new ListVector3(points), duplicateThreshold); // 如果cleaned点数4且怀疑共面可以在这里记录日志或进行降级处理 return cleaned; } // ... 将之前定义的 RemoveDuplicatePoints, ArePointsCoplanar, WeldVertices 等方法移至此 ... }使用这个管理器你可以在任何地方安全地请求凸包计算// 在某个需要生成岩石的脚本中 void GenerateRandomRock() { Vector3[] randomPoints GeneratePointsInSphere(200, 2.0f); ConvexHullManager.Instance.RequestConvexHull(randomPoints, OnHullGenerated); } void OnHullGenerated(Mesh mesh, string error) { if (!string.IsNullOrEmpty(error)) { Debug.LogError($生成失败: {error}); return; } GetComponentMeshFilter().mesh mesh; GetComponentMeshCollider().sharedMesh mesh; }这个框架将输入检查、多线程计算、主线程回调、错误处理和基本的后处理流程都封装了起来让你能更专注于业务逻辑而不是凸包计算的底层细节。在实际项目中你可以根据需求进一步扩展它比如加入任务取消、优先级队列、更复杂的点云预处理算法等功能。

相关新闻

GDSDecomp实战指南:从Godot游戏包逆向恢复可编辑项目

GDSDecomp实战指南:从Godot游戏包逆向恢复可编辑项目

1. 项目概述:当你的Godot项目“消失”时 作为一名在游戏开发一线摸爬滚打了十多年的老手,我经历过无数次“项目灾难”——硬盘突然暴毙、版本管理混乱导致关键提交丢失,或者更常见的,接手一个只有最终发布包(.pck或.ap…

2026/7/8 17:57:29阅读更多 →
NachOS 实验五二级索引实现:从单级到二级的 3 步代码重构与性能影响

NachOS 实验五二级索引实现:从单级到二级的 3 步代码重构与性能影响

NachOS 二级索引实现:从单级到二级的代码重构与性能优化实战在操作系统课程设计中,文件系统的实现一直是核心难点之一。NachOS 作为一个教学用操作系统,其文件系统设计简洁明了,非常适合用于理解文件系统的基本原理。本文将聚焦于…

2026/7/8 17:57:29阅读更多 →
Unity后处理核心:OnRenderImage与RenderTexture的7种组合实战

Unity后处理核心:OnRenderImage与RenderTexture的7种组合实战

1. 项目概述:为什么我们要深入OnRenderImage与RenderTexture的组合? 在Unity里做后处理,就像给游戏画面做“数字化妆”。你肯定用过Unity内置的Post-Processing Stack v2,或者URP/HDRP里的Volume系统,它们功能强大&…

2026/7/8 17:57:29阅读更多 →
微信小程序云开发+混元Lite实战:零服务器构建AI增强应用

微信小程序云开发+混元Lite实战:零服务器构建AI增强应用

1. 项目概述:这不是“薅羊毛”,而是一次微信生态内真实可用的云资源实践“微信小程序送补贴!手把手教你薅免费云开发资源 混元 Token(附使用教程)”——这个标题里藏着三个被严重误读的关键词:“送补贴”“…

2026/7/8 19:03:22阅读更多 →
3分钟掌握Windows窗口强制调整:告别无法缩放的应用窗口

3分钟掌握Windows窗口强制调整:告别无法缩放的应用窗口

3分钟掌握Windows窗口强制调整:告别无法缩放的应用窗口 【免费下载链接】WindowResizer 一个可以强制调整应用程序窗口大小的工具 项目地址: https://gitcode.com/gh_mirrors/wi/WindowResizer 还在为那些顽固的Windows应用窗口而烦恼吗?有些程序…

2026/7/8 19:03:22阅读更多 →
终极指南:如何零成本从3ds Max迁移到Blender?BsMax插件让3D艺术家无缝过渡

终极指南:如何零成本从3ds Max迁移到Blender?BsMax插件让3D艺术家无缝过渡

终极指南:如何零成本从3ds Max迁移到Blender?BsMax插件让3D艺术家无缝过渡 【免费下载链接】BsMax BsMax Blender Addon (UI simulator/ Modeling/ Rigg & Animation/ Render Tools and ... 项目地址: https://gitcode.com/gh_mirrors/bs/BsMax …

2026/7/8 19:03:22阅读更多 →
10种常见作物叶片病害YOLO检测数据包:含划分好的训练验证集、类别说明和标注可视化工具

10种常见作物叶片病害YOLO检测数据包:含划分好的训练验证集、类别说明和标注可视化工具

本文还有配套的精品资源,点击获取 简介:这个数据包专为农业病害目标检测模型训练准备,覆盖水稻、玉米、小麦等作物的10类典型叶片病害,包括格鲁病、早叶斑病、黑斑病等。所有图像统一处理为416416 RGB格式,标注严格…

2026/7/8 19:03:22阅读更多 →
MATLAB实操包:BPSK调制+Turbo编译码全流程误码率仿真(含中文逐行注释与运行视频)

MATLAB实操包:BPSK调制+Turbo编译码全流程误码率仿真(含中文逐行注释与运行视频)

本文还有配套的精品资源,点击获取 简介:直接运行就能出BER-SNR曲线的MATLAB通信仿真工具包,覆盖从随机比特生成、Turbo编码、BPSK调制、AWGN信道加噪、BPSK解调到Turbo迭代译码的完整链路。核心函数turbo_encode.m和turbo_decode.m封装编码…

2026/7/8 19:03:22阅读更多 →
KMS_VL_ALL_AIO:一站式智能激活工具,彻底解决Windows和Office激活难题

KMS_VL_ALL_AIO:一站式智能激活工具,彻底解决Windows和Office激活难题

KMS_VL_ALL_AIO:一站式智能激活工具,彻底解决Windows和Office激活难题 【免费下载链接】KMS_VL_ALL_AIO Smart Activation Script 项目地址: https://gitcode.com/gh_mirrors/km/KMS_VL_ALL_AIO 还在为Windows系统频繁弹出激活提示而烦恼&#xf…

2026/7/8 18:52:52阅读更多 →
从GitHub安全案例解析常见漏洞与防护实践

从GitHub安全案例解析常见漏洞与防护实践

1. 项目概述:从GitHub Trending看安全实战 最近在GitHub Trending上看到一个项目,叫 skills4/skills ,它因为一些安全漏洞案例被大家讨论。这其实是一个挺典型的场景:一个旨在展示或教授某种技能的仓库,本身却成了安…

2026/7/8 5:12:14阅读更多 →
MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

MLT 2026启示:因果推理与概率建模驱动下一代LLM应用

# MLT 2026启示:因果推理与概率建模驱动下一代LLM应用## 一、背景与挑战:从“黑箱预测”到“可信推理”2026年6月,第7届机器学习与趋势国际会议(MLT 2026)将在悉尼召开。会议议程中,“因果与可解释机器学习…

2026/7/8 7:00:12阅读更多 →
通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

通达OA SQL注入漏洞深度剖析:从手工注入到自动化利用与防御

1. 项目概述与漏洞背景最近在梳理一些历史OA系统的安全风险时,通达OA v11.6版本中的一个老漏洞又进入了我的视线。这个漏洞位于/general/bi_design/appcenter/report_bi.func.php文件中,是一个典型的SQL注入点。虽然这个漏洞的利用方式看起来并不复杂&am…

2026/7/8 2:26:06阅读更多 →
作为一个给团队打绩效的人,我想说几句

作为一个给团队打绩效的人,我想说几句

我每半年都会给团队成员打绩效,也会参与和 CTO 的绩效校准,所以从管理者的视角,说说这件事 首先,我先把结论告诉你:接受结果,但一定要把原因问清楚。 因为当绩效公布到你这里的时候,结果基本已…

2026/7/8 0:01:17阅读更多 →
A股股指期货:全维度解析(多表格结构化完整版)

A股股指期货:全维度解析(多表格结构化完整版)

一、基础定义与核心本质股指期货全称股票价格指数期货,是中国金融期货交易所(中金所)上市的标准化金融期货合约,交易标的为 A 股大盘指数,约定未来特定时间按约定价格现金交割指数涨跌差价,不交割一篮子股票…

2026/7/8 0:01:17阅读更多 →
iOS越狱新手指南:从困惑到掌控,3天解锁iPhone无限潜能的真实故事

iOS越狱新手指南:从困惑到掌控,3天解锁iPhone无限潜能的真实故事

iOS越狱新手指南:从困惑到掌控,3天解锁iPhone无限潜能的真实故事 【免费下载链接】Jailbreak iOS 26.4 - 26, 17 - 17.7.5 & iOS 18 - 18.7.3 Jailbreak Tools, Cydia/Sileo/Zebra Tweaks & Jailbreak News Updates || AI Jailbreak Finder &…

2026/7/8 0:01:17阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/8 6:59:54阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/8 13:42:39阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/8 13:42:39阅读更多 →