Newton 保留 OpenUSD 碰撞组过滤语义:inverted filters、merged groups 与多成员关系导入解析
【免费下载链接】newtonAn open-source, GPU-accelerated physics simulation engine built upon NVIDIA Warp, specifically targeting roboticists and simulation researchers.项目地址: https://gitcode.com/GitHub_Trending/newton9/newton
导读
本文围绕 Newton(基于 NVIDIA Warp 的 GPU 加速物理仿真引擎)的 USD 导入管线展开,深入讲解其如何将 OpenUSD(UsdPhysics)中通过CollisionGroup、physics:filteredPairs表达的碰撞过滤规则完整地保留到ModelBuilder中。文章覆盖反向过滤(inverted filters)、合并组(merged groups)、多重成员关系(multiple memberships)与未分组碰撞体(ungrouped colliders)四类关键语义,并结合 import_usd.py 源码与 test_import_usd_collision_groups.py 测试用例,给出可直接复用的导入验证方法。读完本文,你将理解 OpenUSD 碰撞过滤与 Newton 碰撞组 ID、过滤配对(filter pairs)两套机制之间的映射原理,并能自行排查资产导入后的碰撞丢失问题。
OpenUSD 碰撞过滤的两套表达机制
在 USD 资产中,碰撞过滤可以出现在两个层级,Newton 的导入器对两者都做了处理:
UsdPhysics.CollisionGroup(碰撞组):把多个碰撞体归入一个命名组,再通过组与组之间的filteredGroups关系声明哪些组之间禁止碰撞。这是一个"默认允许、显式禁用"(allow-by-default)的模型。physics:filteredPairs(关系):直接在两个图元(collider、body 或 articulation prim)之间声明"这对对象不碰撞",属于按实体精确过滤,可标注在任一端点上,且语义是对称的。
Newton 自身则提供了一套基于整数碰撞组 ID的机制(见 builder.py 中ShapeConfig.collision_group,默认值为1,即默认组;设为0则禁用该形状的所有碰撞),以及形状级过滤配对shape_collision_filter_pairs(无序的(shape_a, shape_b)索引对,见 builder.py)。两套语义并不一一对应,因此导入器必须做一次显式的"降级(lowering)":把 OpenUSD 组级过滤展开成 Newton 的逐形状过滤配对。
changelog 记录(changelog/4145.fixed.md)正是这一修复:在 USD 导入期间完整保留碰撞组过滤,包括反向过滤、合并组、多重成员关系以及未分组碰撞体。
Newton 碰撞组 ID 的判定逻辑
要理解导入器为什么要"显式展开",先看 Newton 本地碰撞组的判定规则。ModelBuilder._test_group_pair(group_a, group_b)(builder.py)与 GPU 端broad_phase_common.test_group_pairkernel 保持完全一致:
- 任一组为
0:不碰撞(0是"禁用"哨兵值); - 组为正数:仅当
group_a == group_b或对方为负数时碰撞(同组或与"任意组"交互); - 组为负数:仅当
group_a != group_b时碰撞(负组表示"与其它所有非本组对象碰撞")。
可见该模型是等值匹配的整数语义,无法直接表达 OpenUSD 中"组 A 与组 B 禁止碰撞,但 A 与 C 允许碰撞"这种任意图结构。因此导入器必须把 OpenUSD 的禁用关系物化为显式的add_shape_collision_filter_pair配对,而不是试图把组名翻译成整数 ID。源码注释也明确指出:"OpenUSD groups are allow-by-default filters and cannot be represented by Newton's equality-based collision group IDs, so their disabled pairs are lowered explicitly after all rigid shapes exist"(import_usd.py)。
与此同时,导入器会把 builder 的默认collision_group(来自builder.default_shape_cfg.collision_group)保留到每个导入形状上,使得调用方仍然可以用0或共享负组来整体关闭碰撞(测试test_nonpositive_builder_collision_groups验证了默认组为0或-1时,所有导入形状保留该默认值且任意两形状之间_test_group_pair均返回 False,见 test_import_usd_collision_groups.py)。
导入期的核心流程:从组到配对
整个降级流程位于 import_usd.py 的导入主体中,可以分为四个阶段:
阶段一:收集每个碰撞体的组成员身份。解析每个刚体碰撞体时记录其完整成员签名tuple(sorted(str(group) ...))(import_usd.py),存入imported_rigid_collider_groups: dict[path, tuple[str, ...]]。之所以保留完整签名而非单个组,是因为一个碰撞体可能同时属于多个组。
阶段二:计算 OpenUSD 的碰撞组判定表。通过UsdPhysics.CollisionGroup.ComputeCollisionGroupTable(stage)一次性算出任意两个组之间是否允许碰撞(import_usd.py)。ComputeCollisionGroupTable是 OpenUSD 提供的标准 API,它会综合filteredGroups关系与invertFilteredGroups属性给出权威结论,导入器直接复用,避免自己重写组图遍历逻辑。
阶段三:按成员签名分桶后物化禁用配对。所有碰撞体按其完整成员签名分组(colliders_by_groups),这样表查询次数只与"不同组组合的数量"成正比,而只物化 OpenUSD 真正禁用的配对(import_usd.py)。对每一对组签名调用_groups_collide(groups_a, groups_b):
- 两侧都有组时,要求所有组对在
collision_group_table.IsCollisionEnabled(...)上全部为 True 才允许碰撞(全真才真); - 只有一侧有组(另一侧是未分组碰撞体)时,退化为检查该侧组成员——若其中任一成员组(或其合并组集合)属于 inverted 组,则不允许碰撞(见下节)。
只要判定为"不碰撞",就把签名内所有碰撞体两两组合(组内用itertools.combinations,跨组用itertools.product),以规范化顺序(min, max)调用builder.add_shape_collision_filter_pair加入过滤配对,并先去重已存在的配对(existing_filter_pairs)。
阶段四:处理physics:filteredPairs关系。导入器会遍历所有 prim,收集标注了physics:filteredPairs关系的图元(_collect_filtered_pairs),将源-目标路径排序后去重存入集合(关系可能只在单端标注,Newton 的过滤配对是对称的,自配对无效被跳过,见 import_usd.py)。由于physics:filteredPairs可以标注在 body 甚至 articulation prim 上(collider 循环不会访问 body prim),导入器在 body 路径表(path_body_map)上再做一遍收集(import_usd.py)。
随后,通过_resolve_collision_shape_ids把路径解析为实际 shape 索引:
- 原生 collider 路径 → 单个 shape;
- body 路径 → 其下所有 shape;
- cable 路径 → 其全部胶囊 shape;
- cloth / volume 粒子形变体 → 明确返回"不支持"(标准粒子碰撞过滤无法表达,import_usd.py)。
解析失败(目标不存在、未产生碰撞参与者等)时会输出带具体原因的warnings.warn,提示该filteredPairs未被导入(import_usd.py)。只有在所有刚体 shape 与 cable 胶囊都已创建后才会应用这些配对(shape 索引此后保持稳定,collapse_fixed_joints只重映射 body),且与阶段三共用同一个去重集合,避免重复追加。
四类关键语义的逐一解析
反向过滤(Inverted Filters)
OpenUSD 允许在组上设置invertFilteredGroups属性(UsdPhysics.CollisionGroup.CreateInvertFilteredGroupsAttr()),含义为"与该组被过滤的组之外的其它组都禁止碰撞"。导入器在遍历 stage 时把所有反向组收集进inverted_groups集合(import_usd.py),并在_groups_collide的单侧分支中生效:只要未分组碰撞体那一侧涉及的任一组成员(或其合并组)属于反向组,就判定为不碰撞。
测试用例test_normal_and_inverted_filtering(test_import_usd_collision_groups.py)构造了 GroupC 反向过滤 GroupB 的场景,验证最终过滤配对包含:A0-A1(组内自过滤)、A0-B、A1-B(正常跨组过滤)、A0-C、A1-C(反向组与正常组的交互)以及C-Ungrouped(反向组对未分组碰撞体生效)——反向过滤会把"未被显式过滤的组"也纳入禁用范围,这正是反向语义的关键体现。
合并组(Merged Groups)
OpenUSD 的mergeGroupName属性(UsdPhysics.CollisionGroup.CreateMergeGroupNameAttr())可以把多个命名组"合并"为一个逻辑实体:同名的合并组共享同一过滤行为。导入器构建了groups_by_merge_name: dict[merge_name, set[group_path]]与group_merge_names: dict[group_path, merge_name](import_usd.py),在_groups_collide中,若一个组的merge_name非空,其"有效组成员"会展开为该合并名下所有组;只要其中任一有效组属于反向组,即不允许碰撞。
测试用例test_merged_groups_and_multiple_memberships(test_import_usd_collision_groups.py)中,MergedGroupA 与 MergedGroupB 共享merge_group="shared",且 MergedGroupA 过滤了 FilteredGroup,于是合并语义下 MergedB 同样与 Filtered 不碰撞,验证结果("MergedA", "Filtered"), ("MergedB", "Filtered")都出现在过滤配对中。
多重成员关系(Multiple Memberships)
一个碰撞体可以同时属于多个组,导入器以完整成员签名(全部组路径的排序元组)作为分桶键,天然支持多成员场景:签名相同的碰撞体共享同一判定结果。测试中Multi同时加入 MultiGroupA 与 MultiGroupB,MultiGroupA 过滤了 FilteredGroup,因此Multi-Filtered被禁用;而 MultiGroupB 中的Other不受影响。
未分组碰撞体(Ungrouped Colliders)
没有加入任何组的碰撞体属于"默认允许"状态,但仍受反向过滤约束。_groups_collide的if groups_a and groups_b:分支在任一侧为空时走单侧逻辑:对空侧之外的那组做反向组检查,从而保证"未分组碰撞体与反向组碰撞"被正确禁用,而普通组与未分组碰撞体之间保持允许(test_unfiltered_and_ungrouped_colliders验证普通组 A、B 与未分组碰撞体之间无任何过滤配对,见 test_import_usd_collision_groups.py)。
组过滤与配对过滤的合成语义
当 OpenUSD 资产同时使用CollisionGroup过滤和physics:filteredPairs时,两者的禁用效果是叠加的:任一机制要求禁用某对 shape,该对即被禁用。测试test_group_filters_compose_with_filtered_pairs(test_import_usd_collision_groups.py)同时构造了基于physics:filteredPairs的 PairA-PairB 与基于组的 GroupA-GroupB 过滤,并断言两类配对都出现在builder.shape_collision_filter_pairs中。从实现看,阶段三与阶段四共用existing_filter_pairs去重集合,且在导入完成后,ModelBuilder.finalize()会对组判定(_test_group_pair)与过滤配对做与运算来确定最终是否碰撞(测试的_assert_filtered_pairs断言逻辑正是group_pair_enabled and pair not in filtered_pairs,见 test_import_usd_collision_groups.py)。
与 MJCF 混用:紧凑过滤配对的保留
shape_collision_filter_pairs在 Newton 内部存在一种紧凑表示(_BuilderShapeCollisionFilterPairs),用于合并 builder 时保持对象身份稳定以命中模板缓存(_materialized_filter_template,见 builder.py)。这保证了"先导入 USD 再追加 MJCF"的混合场景下,USD 侧产生的过滤配对不会被破坏。测试test_mjcf_import_after_usd(test_import_usd_collision_groups.py)演示了标准用法:注册 MuJoCo 求解器自定义属性后先builder.add_usd(stage)再builder.add_mjcf(...),最终两个 shape 都正确进入 builder。
如何验证与排查
仓库提供了完整的回归测试,可直接运行验证导入语义:
python -m unittest newton.tests.test_import_usd_collision_groups(需要安装usd-core,否则测试会被@unittest.skipUnless(USD_AVAILABLE, "Requires usd-core")跳过,见 test_import_usd_collision_groups.py。)
测试中的_assert_filtered_pairs是一个值得借鉴的验证模板:它把期望的禁用形状对与builder.shape_collision_filter_pairs的实际内容比对,并同时用_test_group_pair校验组判定,确保"组与配对两个维度都符合预期"。排查真实资产时,可以按同样思路遍历builder.shape_collision_filter_pairs,并留意导入期warnings.warn输出的physics:filteredPairs was not imported提示——那通常意味着某个过滤端点无法解析到实际 shape(如粒子形变体或不存在的路径)。
小结
Newton 对 OpenUSD 碰撞过滤的导入遵循"组默认允许、禁用显式物化"的原则:用UsdPhysics.CollisionGroup.ComputeCollisionGroupTable获得权威组判定,把完整成员签名分桶后降级为逐 shape 的shape_collision_filter_pairs,并对反向过滤、合并组、多成员与未分组碰撞体四类边界语义做了完整覆盖;physics:filteredPairs关系则通过路径解析到 shape 后按对称去重追加。这套实现使基于 OpenUSD 生态(Isaac、USD 资产库等)制作的碰撞配置能在 Newton 中得到一致复现,相关行为均有 import_usd.py 源码与 test_import_usd_collision_groups.py 测试的双重背书。
【免费下载链接】newtonAn open-source, GPU-accelerated physics simulation engine built upon NVIDIA Warp, specifically targeting roboticists and simulation researchers.项目地址: https://gitcode.com/GitHub_Trending/newton9/newton
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考