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

14 KiB
Raw Blame History

对碰撞检测的性能进行优化

问题描述

  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

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 副本

关键代码变更:

// 之前
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

关键代码变更:

// 之前
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. 只在第一次时构建,后续复用缓存的空间索引

关键代码变更:

// 之前
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. 继续遍历子节点。

关键代码变更:

// 使用 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 数据结构: ModelItemCollectionList<ModelItem> 更高效
  3. 智能缓存策略: 区分一次性成本和重复成本
    • 过滤后的列表应该缓存(避免重复 ToList()
    • 空间索引应该缓存(与移动物体无关)
  4. 识别真正的瓶颈: 使用日志和计时器定位性能热点
  5. 理解缓存的适用范围:
    • 几何缓存:全局,模型不变就不变
    • 过滤缓存:全局,通道对象不变就不变
    • 空间索引:全局,场景对象位置不变就不变
    • 动画预计算:局部,每次路径/移动物体变化都要重算

修改的文件

  • ClashDetectiveIntegration.cs:
    • 修改缓存类型为 ModelItemCollection
    • 优化 BuildAllGeometryItemsCache() 使用 Search API
    • 新增:添加 Search API 可见性过滤
    • 添加 _nonChannelGeometryItemsCache 缓存过滤后的列表
    • 添加 GetChannelObjectsCache() 方法
    • 更新 ClearAllCaches() 清除所有缓存
  • SpatialIndexManager.cs:
    • 恢复使用 GetNonChannelGeometryItemsCache() 获取缓存的过滤列表
    • 简化索引构建逻辑
    • 改进日志输出
  • PathAnimationManager.cs:
    • 添加空间索引初始化检查
    • 只在首次构建空间索引,后续复用缓存

碰撞检测算法性能优化 (2025-12-10)

问题分析

智能去重策略实现后,通过详细的子步骤调试日志发现,单个碰撞测试的执行时间仍然较长:

[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

    • 大多数实际碰撞都通过三角形检测就能发现
    • 去掉点和线的检测可以减少计算量

优化结果

优化后,单个碰撞测试的执行时间显著降低:

[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. 修改测试类型

    // 之前
    TestType = ClashTestType.HardConservative,
    
    // 之后
    TestType = ClashTestType.Hard,
    
  2. 减少几何类型

    // 之前
    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. 结果缓存

    • 缓存相同对象对的碰撞检测结果
    • 对重复检测任务直接返回缓存结果