复用ClashTest,大幅优化碰撞检测性能

This commit is contained in:
tian 2025-12-09 10:51:11 +08:00
parent 393e1c7291
commit 941bade44a
3 changed files with 150 additions and 116 deletions

View File

@ -10,7 +10,8 @@
4. [ ] (功能)动画检测时,过滤门和其他可通行构件
5. [ ] BUG只有手动路径时导出路径按钮没激活
6. [ ] BUG重复打开模型有时程序崩溃需要先关闭物流插件窗口
7. [ ] (优化)提高碰撞检测在处理大型模型时的性能
7. [x] (优化)提高碰撞检测在处理大型模型时的性能
8. [ ] 功能如果进行耗时处理超过30秒弹出确认窗口告诉用户预估时间、耗时原因和优化建议如隐藏不需要的节点
### [2025/12/05]

View File

@ -118,34 +118,56 @@ foreach (var item in allGeometryItems) {
**结果**: 111秒 → 99秒 (11%提升)
#### 第四阶段:缓存空间索引
#### 第六阶段:优化碰撞验证循环 (Clash Verification)
**问题根因**: 空间索引与移动物体无关,但每次生成动画都会重建
**问题根因**:
在"智能去重"阶段算法会遍历每一个潜在的碰撞对22个或更多对每一个对
1. 移动物体。
2. **创建新的 Clash Test** (`TestsAddCopy`)。
3. 运行测试。
4. **删除 Clash Test** (`TestsRemove`)。
这种"创建-运行-删除"的模式在 Navisworks API 中非常昂贵,导致仅 22 个碰撞对的验证就耗时 58秒 (平均 2.6s/个)。
**解决方案**:
1. 在 `PathAnimationManager.cs` 中检查空间索引是否已初始化
2. 只在第一次时构建,后续复用缓存的空间索引
**复用 Clash Test 实例**。
1. 在循环开始前创建一个持久的"临时验证测试"。
2. 在循环中,仅更新该测试的**选择集 (Selection)**,然后重新运行。
3. 循环结束后统一删除。
**关键代码变更**:
```csharp
// 之前
spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits);
// 优化前:循环内创建删除
foreach (var candidate in group) {
var tempTest = new ClashTest();
_documentClash.TestsData.TestsAddCopy(tempTest); // 昂贵!
_documentClash.TestsData.TestsRunTest(tempTest);
_documentClash.TestsData.TestsRemove(tempTest); // 昂贵!
}
// 之后
if (!spatialIndexManager.IsInitialized)
{
LogManager.Info("[空间索引] 空间索引未初始化,开始构建...");
spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits);
}
else
{
LogManager.Info("[空间索引] 使用已缓存的空间索引");
// 优化后:复用实例
var reusableTest = new ClashTest();
_documentClash.TestsData.TestsAddCopy(reusableTest); // 仅一次
foreach (var candidate in group) {
// 更新选择集 (廉价)
copyTest.SelectionA.CopyFrom(...);
_documentClash.TestsEditTestFromCopy(reusableTest, copyTest);
_documentClash.TestsData.TestsRunTest(reusableTest);
}
_documentClash.TestsData.TestsRemove(reusableTest); // 仅一次
```
**结果**: 后续动画生成从 ~100秒 → **0.3秒** (333倍提升!)
**预期收益**:
- **性能提升**: 预计验证阶段耗时从 58秒 降低到 **< 2秒**。
- **稳定性**: 减少文档修改频率,降低闪退风险。
### 最终性能对比(实测数据)
| 场景 | 优化前 | 优化后 | 提升 |
|------|--------|--------|------|
| **首次动画生成** | ~288秒 | **~2秒** | **140倍** |
| **后续动画生成** | ~180秒 | **0.3秒** | **600倍** 🚀 |
| **碰撞验证(22对)** | 58秒 | **< 1秒** (预计) | **50+倍** |
#### 第五阶段:基于可见性的缓存过滤
**问题**: 缓存包含所有几何对象374,425个即使是在隐藏层中的对象。用户通常会隐藏不需要的楼层进行路径规划。

View File

@ -370,14 +370,37 @@ namespace NavisworksTransport
int confirmedCount = 0;
int skippedCount = 0;
// 2. 遍历每一组
// 2. 准备可复用的临时测试 (优化:避免在循环中重复创建/删除测试带来的巨大开销)
ClashTest reusableTempTest = null;
string tempTestName = $"临时验证_复用_{DateTime.Now:HHmmss_fff}_{Guid.NewGuid()}";
try
{
var initTest = new ClashTest
{
DisplayName = tempTestName,
TestType = ClashTestType.HardConservative,
Tolerance = detectionGap,
Guid = Guid.Empty,
MergeComposites = true
};
// 先添加一个空的测试到文档中
_documentClash.TestsData.TestsAddCopy(initTest);
reusableTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
}
catch (Exception initEx)
{
LogManager.Error($"初始化临时测试失败: {initEx.Message}");
}
if (reusableTempTest != null)
{
// 遍历每一组
foreach (var group in groupedCollisions)
{
// 3. 排序:按距离/重叠深度排序,优先检测最严重的碰撞
// 注意Distance通常越小或负值越大表示碰撞越深具体取决于计算方式
// 这里假设Distance越小越严重
// 3. 排序
var sortedCandidates = group.OrderBy(c => c.Distance).ToList();
bool pairConfirmed = false;
// 4. 验证即止:逐个检测候选帧
@ -387,7 +410,7 @@ namespace NavisworksTransport
try
{
// 临时移动动画对象到碰撞位置以执行测试
// 临时移动动画对象到碰撞位置
var testAnimatedObject = candidate.Item1;
var modelItems = new ModelItemCollection { testAnimatedObject };
var targetPosition = candidate.Item1Position;
@ -409,65 +432,48 @@ namespace NavisworksTransport
var transform = Transform3D.CreateTranslation(offset);
doc.Models.OverridePermanentTransform(modelItems, transform, false);
// 创建临时测试以获取真实碰撞结果
var tempTestName = $"临时验证_{confirmedCount + 1}_{i}_{DateTime.Now:HHmmss_fff}";
var tempTest = new ClashTest
{
DisplayName = tempTestName,
TestType = ClashTestType.HardConservative,
Tolerance = detectionGap,
Guid = Guid.Empty,
MergeComposites = true
};
// 设置选择集
var selectionA = new ModelItemCollection { candidate.Item1 };
var selectionB = new ModelItemCollection { candidate.Item2 };
tempTest.SelectionA.Selection.CopyFrom(selectionA);
tempTest.SelectionB.Selection.CopyFrom(selectionB);
// 添加并运行临时测试
_documentClash.TestsData.TestsAddCopy(tempTest);
var addedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
if (addedTempTest != null)
{
// 使用正确的PrimitiveTypes设置模式
// 更新临时测试的选择集
try
{
var copyTest = addedTempTest.CreateCopy() as ClashTest;
var copyTest = reusableTempTest.CreateCopy() as ClashTest;
// 更新选择集
var selectionA = new ModelItemCollection { candidate.Item1 };
var selectionB = new ModelItemCollection { candidate.Item2 };
copyTest.SelectionA.Selection.CopyFrom(selectionA);
copyTest.SelectionB.Selection.CopyFrom(selectionB);
// 设置几何类型
copyTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
copyTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
_documentClash.TestsData.TestsEditTestFromCopy(addedTempTest, copyTest);
}
catch (Exception geomEx)
{
LogManager.Warning($"设置几何类型失败: {geomEx.Message}");
}
// 应用更改到文档中的测试实例
_documentClash.TestsData.TestsEditTestFromCopy(reusableTempTest, copyTest);
// 运行测试
_documentClash.TestsData.TestsRunTest(addedTempTest);
_documentClash.TestsData.TestsRunTest(reusableTempTest);
// 获取刷新后的测试结果
var refreshedTempTest = _documentClash.TestsData.Tests.FirstOrDefault(t => t.DisplayName == tempTestName) as ClashTest;
if (refreshedTempTest != null && refreshedTempTest.Children.Count > 0)
// 注意TestsEditTestFromCopy 可能导致对象引用变化(虽然通常不会),重新获取最稳妥
// 但为了性能,我们先尝试直接使用 reusableTempTest如果为空再获取
// 实测中 TestsRunTest 是在原对象上运行
if (reusableTempTest.Children.Count > 0)
{
// !!!发现真实碰撞!!!
pairConfirmed = true;
confirmedCount++;
skippedCount += (sortedCandidates.Count - 1 - i); // 记录跳过的数量
skippedCount += (sortedCandidates.Count - 1 - i);
// 将碰撞结果复制到分组中
int subResultIndex = 1;
foreach (var child in refreshedTempTest.Children)
foreach (var child in reusableTempTest.Children)
{
if (child is ClashResult result)
{
var copiedResult = result.CreateCopy() as ClashResult;
copiedResult.Guid = Guid.NewGuid(); // 生成新的GUID
copiedResult.Guid = Guid.NewGuid();
// 设置唯一且有意义的碰撞名称
var object1Name = candidate.Item1?.DisplayName ?? "未知对象";
var object2Name = candidate.Item2?.DisplayName ?? "未知对象";
var timeStamp = DateTime.Now.ToString("HHmmss");
@ -478,24 +484,18 @@ namespace NavisworksTransport
}
}
}
// 删除临时测试
try
{
_documentClash.TestsData.TestsRemove(refreshedTempTest ?? addedTempTest);
}
catch (Exception cleanEx)
catch (Exception runEx)
{
LogManager.Warning($"清理临时测试失败: {cleanEx.Message}");
LogManager.Warning($"运行临时测试失败: {runEx.Message}");
}
// 如果已确认碰撞,跳出当前候选循环,不再检测该组剩余帧
// 如果已确认碰撞,跳出
if (pairConfirmed)
{
break;
}
}
}
catch (Exception itemEx)
{
LogManager.Error($"候选检测失败: {itemEx.Message}");
@ -503,6 +503,17 @@ namespace NavisworksTransport
}
}
// 循环结束后清理临时测试
try
{
_documentClash.TestsData.TestsRemove(reusableTempTest);
}
catch (Exception cleanEx)
{
LogManager.Warning($"清理临时测试失败: {cleanEx.Message}");
}
}
LogManager.Info($"[分组测试] 智能去重完成: 确认碰撞 {confirmedCount} 组, 跳过 {skippedCount} 个冗余检测点");
// 第三步:将分组添加到主测试