NavisworksTransport/doc/working/碰撞检测性能优化_20251208.md

369 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 对碰撞检测的性能进行优化
## 问题描述
1. 当前性能瓶颈:
- 动画生成建立时间长,对于某大型模型的日志:
[2025-12-08 10:02:31.838] [INFO] [动画生成] 开始构建碰撞检测缓存
[2025-12-08 10:05:28.571] [INFO] 几何对象列表缓存构建完成,耗时: 176730ms
[2025-12-08 10:05:28.571] [INFO] - 缓存对象总数: 374425 个
[2025-12-08 10:05:28.750] [INFO] 通道对象缓存构建完成,耗时: 174ms
[2025-12-08 10:05:28.751] [INFO] - 可通行物流根对象: 2 个
[2025-12-08 10:05:28.751] [INFO] - 缓存总对象数: 5 个
[2025-12-08 10:05:28.751] [INFO] [动画生成] 碰撞检测缓存构建完成,耗时: 176911.7ms
[2025-12-08 10:05:28.774] [INFO] === 构建全局空间索引 ===
[2025-12-08 10:05:28.776] [INFO] [空间索引] 物体宽度: 1.00米 → 格子大小: 3.28模型单位
[2025-12-08 10:05:28.781] [INFO] [空间索引] 开始构建全局空间索引
[2025-12-08 10:05:28.782] [INFO] [空间索引] 格子大小: 3.28 模型单位
[2025-12-08 10:05:28.807] [INFO] [空间索引] 从缓存获取 374422 个非通道几何对象(已过滤通道)
[2025-12-08 10:05:31.538] [INFO] [空间索引] 构建完成
[2025-12-08 10:05:31.538] [INFO] - 成功索引: 374422 个对象(通道已在缓存阶段过滤)
[2025-12-08 10:05:31.538] [INFO] - 失败: 0 个对象
[2025-12-08 10:05:31.538] [INFO] - 格子数量: 58986 个
[2025-12-08 10:05:31.540] [INFO] - 耗时: 2756 ms
[2025-12-08 10:05:31.782] [INFO] [动画生成] 动画生成完成,总耗时: 179942.9ms
- 如果碰撞检测点过多检测时间长。用户实际模型中有2000多个检测点耗时近300秒。
## 解决方案
- 优化碰撞检测算法
分组去重:代码现在会先将所有预计算的碰撞事件按 (移动物体, 被撞物体) 进行分组。
验证即止:对于每一对物体,按干涉深度排序。
逐帧验证:将物体移动到该帧的位置,进行精确 Clash Detective 检测。
智能跳过:一旦确认了该对物体发生了真实碰撞,就记录结果并立即停止对该对物体的后续检测。
统计信息:日志中现在会显示"确认碰撞 X 组,跳过 Y 个冗余检测点",方便评估优化效果。
---
## 缓存构建性能优化 (2025-12-08)
### 问题分析
初始性能测试显示缓存构建耗时过长:
- **几何对象缓存**: 177秒 (374,425个对象)
- **空间索引构建**: 111秒
- **总耗时**: ~288秒
### 优化过程
#### 第一阶段:使用 Search API 替代遍历
**问题根因**: `RootItemDescendantsAndSelf.Where(item => item.HasGeometry)` 遍历整个模型树效率低下
**解决方案**: 使用 Navisworks Search API
```csharp
var search = new Search();
search.SearchConditions.Add(
SearchCondition.HasCategoryByName(PropertyCategoryNames.Geometry));
search.Selection.SelectAll();
search.Locations = SearchLocations.DescendantsAndSelf;
var allItems = search.FindAll(Application.ActiveDocument, false);
```
**结果**: 177秒 → 113秒 (36%提升)
#### 第二阶段:消除 ToList() 转换瓶颈
**问题根因**: `allItems.ToList()` 强制枚举并分配内存给374k个对象
**解决方案**:
1. 将缓存类型从 `List<ModelItem>` 改为 `ModelItemCollection`
2. 直接存储 Search API 返回的 `ModelItemCollection`
3. 更新 getter 方法返回 `ModelItemCollection` 而非创建 List 副本
**关键代码变更**:
```csharp
// 之前
private static List<ModelItem> _allGeometryItemsCache = null;
_allGeometryItemsCache = allItems.ToList(); // 耗时100+秒
// 之后
private static ModelItemCollection _allGeometryItemsCache = null;
_allGeometryItemsCache = search.FindAll(Application.ActiveDocument, false); // <1秒
```
**结果**: 113秒 → 0.9秒 (**129倍提升!**)
#### 第三阶段:优化空间索引构建
**问题根因**: `GetNonChannelGeometryItemsCache()` 内部调用 `.Where().ToList()` 预过滤通道对象
**解决方案**:
1. 直接迭代 `ModelItemCollection`
2. 在循环中使用 `HashSet.Contains()` 过滤通道对象
3. 避免创建中间 List
**关键代码变更**:
```csharp
// 之前
var nonChannelItems = ClashIntegration.GetNonChannelGeometryItemsCache(); // ToList()
foreach (var item in nonChannelItems) { ... }
// 之后
var allGeometryItems = ClashIntegration.GetAllGeometryItemsCache();
var channelObjectsCache = ClashIntegration.GetChannelObjectsCache();
foreach (var item in allGeometryItems) {
if (channelObjectsCache != null && channelObjectsCache.Contains(item)) {
skippedChannelCount++;
continue;
}
// 索引对象...
}
```
**结果**: 111秒 → 99秒 (11%提升)
#### 第四阶段:缓存空间索引
**问题根因**: 空间索引与移动物体无关,但每次生成动画都会重建
**解决方案**:
1.`PathAnimationManager.cs` 中检查空间索引是否已初始化
2. 只在第一次时构建,后续复用缓存的空间索引
**关键代码变更**:
```csharp
// 之前
spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits);
// 之后
if (!spatialIndexManager.IsInitialized)
{
LogManager.Info("[空间索引] 空间索引未初始化,开始构建...");
spatialIndexManager.BuildGlobalIndex(cellSizeInModelUnits);
}
else
{
LogManager.Info("[空间索引] 使用已缓存的空间索引");
}
```
**结果**: 后续动画生成从 ~100秒 → **0.3秒** (333倍提升!)
#### 第五阶段:基于可见性的缓存过滤
**问题**: 缓存包含所有几何对象374,425个即使是在隐藏层中的对象。用户通常会隐藏不需要的楼层进行路径规划。
**解决方案**:
采用**深度优先搜索 (DFS) + 自动剪枝**。
Search API 的 `Hidden` 属性和 `IsHidden` 属性(在扁平列表上)都无法正确处理继承的可见性(父节点隐藏)。
我们改用手动实现的迭代式遍历(使用 Stack 模拟递归),从根节点开始遍历:
1. 如果遇到隐藏节点 (`IsHidden == true`)**直接跳过该分支**(剪枝)。
2. 如果节点可见且有几何体,则加入缓存。
3. 继续遍历子节点。
**关键代码变更**:
```csharp
// 使用 Stack 模拟递归遍历 (Top-Down DFS)
var stack = new Stack<ModelItem>(1000);
foreach (var model in Application.ActiveDocument.Models)
{
stack.Push(model.RootItem); // 从模型根节点开始
}
while (stack.Count > 0)
{
var item = stack.Pop();
// 自动剪枝:父节点隐藏 -> 跳过整个子树
if (item.IsHidden) continue;
if (item.HasGeometry)
_allGeometryItemsCache.Add(item);
foreach (var child in item.Children)
stack.Push(child);
}
```
**预期收益**:
- **正确性**: 完美处理层级隐藏(如隐藏楼层文件夹,下面的墙体自动排除)。
- **性能**: 相比全量遍历隐藏分支被直接跳过。如果隐藏了90%的模型遍历开销也减少90%。
**预期收益**:
- **缓存大小减少**: 取决于隐藏对象的数量如隐藏50%楼层,对象数减半)
- **性能提升**:
- 过滤缓存构建: 与可见对象数量成正比 (如 50% 可见 -> 耗时减半)
- 空间索引构建: 与可见对象数量成正比
- 内存占用降低
**注意**: 修改可见性后需要刷新缓存(可以通过重新打开插件或重启 Navisworks或者实现手动刷新功能
### 最终性能对比(实测数据)
| 场景 | 优化前 | 优化后 | 提升 |
|------|--------|--------|------|
| **首次动画生成** | ~288秒 | **~2秒** | **140倍** |
| **后续动画生成** | ~180秒 | **0.3秒** | **600倍** 🚀 |
**说明**:
- 首次生成包含:几何缓存构建(1.3s,含过滤) + 过滤缓存构建(~0.1s) + 空间索引构建(~1.5s) + 动画预计算(~0.3s)
- 后续生成仅需:动画预计算(~0.3s),所有缓存直接复用
### 剩余瓶颈分析
首次动画生成的2-3秒主要消耗在
1. **几何缓存构建** (~1.3秒): 深度优先遍历与自动剪枝
- 这取决于可见模型的复杂度1.3秒是正常的速度
- **优化非常成功** (之前是 100+秒)
2. **空间索引构建** (~1.5秒): 调用 `BoundingBox()`
- 这是一次性成本,后续复用
- 已经是最优方案
3. **动画预计算** (~0.3秒): 碰撞检测计算
- 已经极快,无优化必要
**结论**: 性能优化目标已超额完成。首次生成从近5分钟缩短到约2秒后续操作实时响应。
### 关键经验教训
1. **避免不必要的集合转换**: `ToList()` 在大数据集上代价极高
2. **使用原生 API 数据结构**: `ModelItemCollection``List<ModelItem>` 更高效
3. **智能缓存策略**: 区分一次性成本和重复成本
- 过滤后的列表应该缓存(避免重复 `ToList()`
- 空间索引应该缓存(与移动物体无关)
4. **识别真正的瓶颈**: 使用日志和计时器定位性能热点
5. **理解缓存的适用范围**:
- 几何缓存:全局,模型不变就不变
- 过滤缓存:全局,通道对象不变就不变
- 空间索引:全局,场景对象位置不变就不变
- 动画预计算:局部,每次路径/移动物体变化都要重算
### 修改的文件
- `ClashDetectiveIntegration.cs`:
- 修改缓存类型为 `ModelItemCollection`
- 优化 `BuildAllGeometryItemsCache()` 使用 Search API
- **新增:添加 Search API 可见性过滤**
- 添加 `_nonChannelGeometryItemsCache` 缓存过滤后的列表
- 添加 `GetChannelObjectsCache()` 方法
- 更新 `ClearAllCaches()` 清除所有缓存
- `SpatialIndexManager.cs`:
- 恢复使用 `GetNonChannelGeometryItemsCache()` 获取缓存的过滤列表
- 简化索引构建逻辑
- 改进日志输出
- `PathAnimationManager.cs`:
- 添加空间索引初始化检查
- 只在首次构建空间索引,后续复用缓存
## 碰撞检测算法性能优化 (2025-12-10)
### 问题分析
智能去重策略实现后,通过详细的子步骤调试日志发现,单个碰撞测试的执行时间仍然较长:
```log
[2025-12-10 14:02:16.650] [DEBUG] [分组测试] 智能去重-测试添加: 临时测试添加完成, 耗时: 27ms
[2025-12-10 14:02:16.651] [DEBUG] [分组测试] 智能去重-设置几何类型: 耗时: 0ms
[2025-12-10 14:02:16.887] [DEBUG] [分组测试] 智能去重-测试执行: 临时测试运行完成, 耗时: 235ms
[2025-12-10 14:02:16.888] [DEBUG] [分组测试] 智能去重-获取测试结果: 耗时: 0ms
```
其中 `TestsRunTest()` 方法调用平均耗时约 200-250ms/次,占总检测时间的 90% 以上,成为新的性能瓶颈。
### 根本原因
1. **测试类型选择**:使用了 `ClashTestType.HardConservative`(保守硬碰撞)模式
- 比普通 `Hard` 模式更复杂,会额外检测体积重叠但几何体不相交的情况
- 需要处理四种特殊情况:面接触边缘、共面面重叠、边接触端点、共轴边重叠
- 增加了大量额外的计算逻辑
2. **几何类型设置**:包含了 `PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points`
- 检测所有类型的几何体,增加了检测的全面性但降低了性能
- 点和线的碰撞检测在实际应用中很少产生有意义的结果
### 优化方案
1. **降低测试类型复杂度**:将 `HardConservative` 改为普通 `Hard` 模式
- 普通 `Hard` 模式只检测实际相交的几何体
- 可以显著提高性能,同时保持足够的检测精度
2. **减少几何类型**:仅保留 `PrimitiveTypes.Triangles`
- 大多数实际碰撞都通过三角形检测就能发现
- 去掉点和线的检测可以减少计算量
### 优化结果
优化后,单个碰撞测试的执行时间显著降低:
```log
[2025-12-10 14:15:39.277] [DEBUG] [分组测试] 智能去重-测试添加: 临时测试添加完成, 耗时: 18ms
[2025-12-10 14:15:39.278] [DEBUG] [分组测试] 智能去重-设置几何类型: 耗时: 0ms
[2025-12-10 14:15:39.412] [DEBUG] [分组测试] 智能去重-测试执行: 临时测试运行完成, 耗时: 133ms
[2025-12-10 14:15:39.412] [DEBUG] [分组测试] 智能去重-获取测试结果: 耗时: 0ms
```
**性能提升对比**
- 测试执行时间235ms → 133ms约 43% 提升)
- 智能去重总耗时:~20,000ms → 16,709ms约 16.5% 提升)
- 检测效率:确认碰撞 16 组,跳过 296 个冗余检测点(与优化前相同)
### 优化的关键代码变更
`ClashDetectiveIntegration.cs` 中进行了以下修改:
1. **修改测试类型**
```csharp
// 之前
TestType = ClashTestType.HardConservative,
// 之后
TestType = ClashTestType.Hard,
```
2. **减少几何类型**
```csharp
// 之前
copyTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
copyTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles | PrimitiveTypes.Lines | PrimitiveTypes.Points;
// 之后
copyTest.SelectionA.PrimitiveTypes = PrimitiveTypes.Triangles;
copyTest.SelectionB.PrimitiveTypes = PrimitiveTypes.Triangles;
```
### 后续优化建议
1. **配置化优化参数**
- 添加容差值(Tolerance)配置,可根据项目需求调整检测精度
- 提供碰撞测试类型选择选项,让用户在精度和性能间平衡
2. **批量检测优化**
- 实现批量碰撞检测API调用减少Navisworks API调用开销
- 合并相似检测任务,提高处理效率
3. **并行处理**
- 对独立的碰撞检测任务实现并行处理
- 利用多核CPU资源加速检测过程
4. **碰撞预判**
- 添加简单的边界框碰撞预判
- 对明显不碰撞的对象对提前过滤,避免不必要的精确检测
5. **结果缓存**
- 缓存相同对象对的碰撞检测结果
- 对重复检测任务直接返回缓存结果