199 lines
7.4 KiB
Markdown
199 lines
7.4 KiB
Markdown
# 对碰撞检测的性能进行优化
|
||
|
||
## 问题描述
|
||
|
||
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倍提升!)
|
||
|
||
### 最终性能对比(实测数据)
|
||
|
||
| 场景 | 优化前 | 优化后 | 提升 |
|
||
|------|--------|--------|------|
|
||
| **首次动画生成** | ~288秒 | **~120秒** | **2.4倍** |
|
||
| **后续动画生成** | ~180秒 | **0.3秒** | **600倍** 🚀 |
|
||
|
||
**说明**:
|
||
- 首次生成包含:几何缓存构建(0.9s) + 过滤缓存构建(~100s) + 空间索引构建(~2.7s) + 动画预计算(~16s)
|
||
- 后续生成仅需:动画预计算(~0.3s),所有缓存直接复用
|
||
|
||
### 剩余瓶颈分析
|
||
|
||
首次动画生成的120秒主要消耗在:
|
||
|
||
1. **过滤缓存构建** (~100秒): `ToList()` 过滤374k对象
|
||
- 这是一次性成本,后续复用
|
||
- 无法进一步优化(需要遍历所有对象)
|
||
|
||
2. **空间索引构建** (~2.7秒): 调用 `BoundingBox()`
|
||
- 这是一次性成本,后续复用
|
||
- 已经是最优方案
|
||
|
||
3. **动画预计算** (~16秒): 碰撞检测计算
|
||
- 每次动画生成都需要(与路径和移动物体相关)
|
||
- 可能的优化:减少检测精度或帧数
|
||
|
||
**结论**: 首次120秒是合理的一次性成本。后续0.3秒的性能已经达到最优。
|
||
|
||
### 关键经验教训
|
||
|
||
1. **避免不必要的集合转换**: `ToList()` 在大数据集上代价极高
|
||
2. **使用原生 API 数据结构**: `ModelItemCollection` 比 `List<ModelItem>` 更高效
|
||
3. **智能缓存策略**: 区分一次性成本和重复成本
|
||
- 过滤后的列表应该缓存(避免重复 `ToList()`)
|
||
- 空间索引应该缓存(与移动物体无关)
|
||
4. **识别真正的瓶颈**: 使用日志和计时器定位性能热点
|
||
5. **理解缓存的适用范围**:
|
||
- 几何缓存:全局,模型不变就不变
|
||
- 过滤缓存:全局,通道对象不变就不变
|
||
- 空间索引:全局,场景对象位置不变就不变
|
||
- 动画预计算:局部,每次路径/移动物体变化都要重算
|
||
|
||
### 修改的文件
|
||
|
||
- `ClashDetectiveIntegration.cs`:
|
||
- 修改缓存类型为 `ModelItemCollection`
|
||
- 优化 `BuildAllGeometryItemsCache()` 使用 Search API
|
||
- 添加 `GetChannelObjectsCache()` 方法
|
||
- `SpatialIndexManager.cs`:
|
||
- 优化索引构建逻辑,在迭代中过滤而非预过滤
|
||
- 改进日志输出,显示跳过的通道对象数量 |