Import USD deformable bodies (cable, cloth, volume) in add_usd() - #3192
Conversation
Recognize a GeomBasisCurves carrying PhysicsCurvesDeformableSimAPI and import it as a VBD cable via ModelBuilder.add_rod (capsule bodies + cable joints). Discovery is metadata-based (has_applied_api_schema) so it works without the deformable schema being registered with the USD runtime, matching the NewtonSDFCollisionAPI approach. This is the first seam of the USD deformable importer (issue newton-physics#3178, cable REQ newton-physics#3037): topology only (curveVertexCounts split, wrap -> closed), world-transformed centerline -> rod positions, and a path_cable_map entry exposing each cable's (body, joint) indices. Material -> stiffness, normals -> orientation, and rest-shape parsing are deferred to later phases; defaults are used for now.
Read the bound curve-deformable material (PhysicsCurvesDeformableMaterialAPI) and map it onto add_rod parameters: - thickness -> capsule radius (thickness / 2) - stretchStiffness [force/area] -> per-joint stretch_stiffness via E*A/L - bendStiffness [force/area] -> per-joint bend_stiffness via E*I/L with A = pi r^2, I = pi r^4 / 4 and the per-curve mean segment length, mirroring newton.utils.cable.create_cable_stiffness_from_elastic_moduli. Material values are resolved via a new newton.usd.get_curve_deformable_material() helper that reads omniphysics: / physxDeformableBody: / physics: namespaces and drops the -inf "simulator default" sentinel and non-positive values. shearStiffness and twistStiffness are warned-and-ignored until the VBD cable gains separate shear/twist constraints (PR newton-physics#3122).
Lock down the prim -> body/joint mapping (REQ newton-physics#3037 addressability): - two cables in one stage map to disjoint, fully-covering body/joint ranges via path_cable_map - each cable body origin matches its authored segment start point (Z-up stage so points are not axis-converted), proving the map points at the right bodies - the cable's articulation is labeled "<prim_path>_articulation", the replication-durable handle preserved through add_builder No production change — the mapping (path_cable_map + the cable's articulation) was already produced by the discovery scaffold; these tests pin the contract.
- density: bound material density overrides the builder default on the capsule ShapeConfig, so segment mass scales with authored density. Per-point `masses` (a simulation-mesh concept) do not map onto rigid capsule bodies and are not consumed. - normals: authored per-vertex curve normals set each segment's cross-section frame -> per-segment quaternion (local +Z to the segment tangent, local +Y to the normal), with a roll-free fallback for absent or degenerate normals. Tests: density doubling doubles segment mass; authored normals orient each segment (+Z -> tangent, +Y -> normal). Comments kept ASCII-only.
Replicate a parsed cable prototype across worlds and assert each world gets an independent, contiguous segment block (T5 from REQ newton-physics#3037): body count scales with world_count, the cable articulation label repeats once per env, and per-world body ranges are disjoint and cover the model - so state can be sliced as (num_envs, num_segments, ...). Validates the addressability decision (sub-articulation reuse) under replication.
Commit a hand-authored cable asset (newton/tests/assets/ cable_curve_deformable.usda): a GeomBasisCurves with PhysicsCurvesDeformableSimAPI and a bound curve-deformable material, i.e. the USD form of the cables the examples build programmatically. Tests load it through add_usd and (1) assert it parses to the expected rod bodies/joints (device-free), and (2) on CUDA, run it through SolverVBD for 20 steps and assert the cable stays finite and bounded - the "examples work after parsing" check. Also fix a RUF017 quadratic list-sum in the replication test.
Address Newton conventions (AGENTS.md): - Rename get_curve_deformable_material -> _get_curve_deformable_material. It is an internal _src helper with a single internal caller; a public name without a public export is the worst of both, and exposing it would commit to public API on an evolving deformable schema. Keep it private. - Add the missing [Unreleased] CHANGELOG entry for the user-facing USD cable import in add_usd().
A triangle GeomMesh carrying PhysicsSurfaceDeformableSimAPI is imported as Newton cloth (particles + FEM triangles + bending edges) via add_cloth_mesh (REQ newton-physics#3036). Discovery is metadata-based; the bound surface-deformable material maps stretch -> tri_ke, shear -> tri_ka, bend -> bending-edge stiffness, with density for mass. The returned dict gains path_cloth_map: prim path -> particle / triangle / bending-edge ranges ([start, end)) for per-cloth addressability. Non-triangulated meshes warn and skip. Adds _get_surface_deformable_material (private, mirroring the curve reader). Tests cover discovery + ranges, the material mapping, and the negative (plain mesh -> no cloth).
The TetMesh -> add_soft_mesh path now records path_soft_map: prim path -> particle / tet ranges ([start, end)) in the model arrays, so individual volume soft bodies can be located and sliced after import (REQ newton-physics#3038). Mirrors path_cable_map / path_cloth_map. Tests cover the single-body range and two-body disjoint/covering ranges. Deferred: k_damp / particle_radius material parity (newton-physics#3038) needs Newton-additive attribute names not in the base deformable schema; left for the Newton-layered schema decision rather than inventing names here.
Andrew's newton-physics#3178 review asks us to parse the public AOUSD deformable proposal as written, under the canonical physics: namespace, and to handle vendor namespaces (omniphysics, physxDeformableBody) the same way rigid bodies do -- through a schema resolver rather than hand-rolled in the parser. Read deformable material/geometry attributes from physics: first. Vendor namespaces become an opt-in fallback sourced from the active resolvers' extra_attr_namespaces (new SchemaResolverManager.compat_attr_namespaces), which SchemaResolverPhysx now declares for deformables. A default import reads only the canonical schema; passing the PhysX resolver re-enables the vendor-compat path. Migrate the test assets to physics:.
The deformable proposal defines a mass precedence that was previously unimplemented: per-point physics:masses > PhysicsDeformableBodyAPI.mass > body density > material density, with per-element volume weighting. Read PhysicsDeformableBodyAPI mass/density (walking up to the body root, which may be an ancestor Xform of the simulation geometry) and the simulation API's per-point physics:masses, then apply them across cable, cloth, and volume imports: - body density overrides the material density driving the builder's volume/area-weighted mass distribution (areal for cloth, via the surface thickness); - PhysicsDeformableBodyAPI.mass rescales that distribution to a target total (segment masses + inertia for the rigid cable model); - per-point masses are applied directly to the particle range, or, for the rigid cable model, summed to a total with a warning. Element volume/area weighting is delegated to the add_* builders.
Mark a TetMesh as a volume deformable when it carries PhysicsVolumeDeformableSimAPI or is the simulation child of a PhysicsDeformableBodyAPI, matching the AOUSD proposal's discovery rule. Only recognized volume deformables receive the deformable mass precedence (body mass/density, per-point physics:masses); a bare TetMesh keeps the legacy material-density import so existing assets still load.
…GELOG
Note in the curve-deformable importer that Newton-specific curve
parameters (cable damping, articulation wrapping) are intentionally left
to a future NewtonCurvesDeformable{Sim,Material}API extension layered via
a schema resolver, keeping the base parser on the public schema. Record
the canonical-namespace and mass-distribution behavior in the CHANGELOG.
The cable importer wrapped every imported cable in its own articulation. Add an optional newton:cableWrapInArticulation bool on the curve prim (default true): when false, the cable joints are left unwrapped so the caller can place them itself - e.g. close a loop or attach the cable to other bodies with extra joints before finalize, as the cross-slide-table example does with ball joints to the table. Flows to ModelBuilder.add_rod(wrap_in_articulation=...). Test asserts the cable is still created but no articulation is registered when false.
newton:cableWrapInArticulation is Newton-specific, not part of the public AOUSD curve-deformable schema. Read it through a dedicated helper that the base parser invokes only when the Newton schema resolver is active, mirroring how physx vendor attributes require SchemaResolverPhysx. A default import (Newton resolver on) is unchanged; a base-only import reads only the public schema and always wraps cables in their own articulation. This keeps the boundary clean for a future NewtonCurvesDeformableSimAPI.
The main-rebase left both the consolidated deformable-import entry and the original per-family bullets; remove the duplicates.
Cable was thoroughly tested while cloth had only discovery + material mapping. Add cloth tests for per-cloth addressability (disjoint ranges), per-point physics:masses precedence, and the volumetric->areal density conversion via surface thickness; plus parse-and-simulate (SolverVBD) tests for both cloth and a tet soft body, matching the existing cable round-trip test.
The curve-material modulus -> per-joint stiffness conversion divided by the straight-line endpoint distance / (n-1), which underestimates the segment rest length for curved cables (e.g. a zigzag) and inflated the stiffness (2x for the cable_twist example). Use the mean of the actual segment lengths instead. Straight cables are unaffected.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds deformable USD import support for cables, cloth, and soft-body volumes, plus attachment and collision-filter lowering, resolver-scoped legacy namespace compatibility, deformable result maps, and updated docs, assets, and tests. ChangesUSD Deformable Import Enhancements
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant parse_usd
participant _scout_deformable_prims
participant _DeformableImportContext
participant ModelBuilder
participant _deformable_import_cable
participant _deformable_import_cloth
participant _deformable_import_volume
participant _deformable_import_attachments
parse_usd->>_scout_deformable_prims: bucket deformable prims
parse_usd->>_DeformableImportContext: build shared context
_DeformableImportContext->>_deformable_import_cable: lower cables
_DeformableImportContext->>_deformable_import_cloth: lower cloth
_DeformableImportContext->>_deformable_import_volume: lower volumes
_DeformableImportContext->>_deformable_import_attachments: lower attachments and filters
parse_usd->>ModelBuilder: merge deformable groups and remap collapse indices
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (2)
newton/tests/test_usd_deformable.py (2)
301-302: ⚡ Quick winTrim inline comments that restate obvious test steps/results.
These comments mostly narrate what adjacent code already makes explicit, which adds noise in a long test file.
As per coding guidelines, in
**/*.pycode comments should be brief and reserved for non-obvious intent (“why”, not “what”).Also applies to: 312-317, 375-376
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@newton/tests/test_usd_deformable.py` around lines 301 - 302, Remove or replace the inline comments that restate what the adjacent code already makes explicit. In test_usd_deformable.py at lines 301-302, remove the comment about parsing the cable into prototype builder and replication across worlds since the code is self-explanatory. Similarly, at lines 312-317 and 375-376, trim any comments that merely narrate what the code does rather than explaining the non-obvious intent or reasoning behind it. Keep only comments that explain "why" the code is written that way, not "what" it does, in accordance with coding guidelines for Python files.Source: Coding guidelines
169-175: ⚡ Quick winAvoid hard-coding the importer default radius in this resolver-behavior test.
This assertion couples the test to an internal default (
0.05) rather than the behavior under test (vendor namespace ignored without resolver). Prefer asserting a difference between default and compat outcomes, while keeping the compat-radius check.Proposed test hardening
- self.assertAlmostEqual(default_radius, 0.05, places=5) + # Without resolver, vendor thickness should not be applied. + # Keep this independent of whatever the builder's fallback default is. + self.assertGreater(default_radius, 0.0) @@ builder_compat = newton.ModelBuilder() builder_compat.add_usd(str(usd_path), schema_resolvers=[SchemaResolverPhysx()]) compat_radius = builder_compat.shape_scale[builder_compat.body_shapes[0][0]][0] + self.assertNotAlmostEqual(default_radius, compat_radius, places=6) self.assertAlmostEqual(compat_radius, 0.01, places=5)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@newton/tests/test_usd_deformable.py` around lines 169 - 175, The test hard-codes an assertion that default_radius equals 0.05, which couples the test to an internal implementation detail rather than the behavior being tested. Instead of using assertAlmostEqual to check if default_radius equals 0.05, modify the assertion to compare the default_radius with the compat_radius (which should be computed earlier in the test) to verify they produce different results when the vendor namespace is ignored versus when it is handled. This keeps the test focused on the actual behavior difference without relying on magic numbers. Keep the existing compat-radius check intact to maintain validation of the compatibility behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@newton/_src/utils/import_usd.py`:
- Around line 4472-4474: The function returning the dictionary with
path_cable_map, path_cloth_map, and path_soft_map keys has a docstring with a
Returns section that does not document these three new keys. Locate the Returns
section of the docstring for the function containing this dictionary return
statement and add documentation entries for path_cable_map, path_cloth_map, and
path_soft_map that describe what each key represents and its addressability
contract, following the same format and style as the existing documented return
keys.
- Around line 3651-3653: The path_cable_map is populated with cable body and
joint references before collapse_fixed_joints() is called, but when
collapse_fixed_joints=True, the subsequent collapse operation compacts the body
and joint arrays. Currently, only path_body_map and path_joint_map are refreshed
after the collapse, leaving path_cable_map with stale pre-collapse indices.
After the collapse_fixed_joints() call completes (in the block that handles
collapse_fixed_joints=True), add logic to remapping path_cable_map so that its
stored cable body and joint ids reference the correct post-collapse indices,
similar to how path_body_map and path_joint_map are being refreshed.
- Around line 3415-3416: In the `_apply_particle_masses()` function, before
summing the point_masses array at the line where target is assigned, add
validation to ensure the length of point_masses matches the expected number of
particles in the cable. Without this length check, a mismatched physics:masses
array can silently override the cable's total mass with an incorrect value.
Validate the array length and raise an appropriate error or warning if it does
not match the cable's expected particle count before proceeding with the
float(sum(point_masses)) operation.
- Around line 3682-3708: The code currently forces cloth scale to 1.0 when it is
non-uniform but does not apply the actual non-uniform scale values to the
mesh_points vertices passed to builder.add_cloth_mesh. This causes the imported
cloth to lose its authored USD scale. Modify the code to bake the component-wise
scale into the mesh_points vertices when _is_uniform_scale(cloth_scale) returns
false, applying cloth_scale[0], cloth_scale[1], and cloth_scale[2] to each
respective coordinate of each vertex in mesh_points before passing the scaled
vertices to builder.add_cloth_mesh. Keep scale as 1.0 in the add_cloth_mesh call
for non-uniform cases, as the scaling will already be baked into the vertices.
---
Nitpick comments:
In `@newton/tests/test_usd_deformable.py`:
- Around line 301-302: Remove or replace the inline comments that restate what
the adjacent code already makes explicit. In test_usd_deformable.py at lines
301-302, remove the comment about parsing the cable into prototype builder and
replication across worlds since the code is self-explanatory. Similarly, at
lines 312-317 and 375-376, trim any comments that merely narrate what the code
does rather than explaining the non-obvious intent or reasoning behind it. Keep
only comments that explain "why" the code is written that way, not "what" it
does, in accordance with coding guidelines for Python files.
- Around line 169-175: The test hard-codes an assertion that default_radius
equals 0.05, which couples the test to an internal implementation detail rather
than the behavior being tested. Instead of using assertAlmostEqual to check if
default_radius equals 0.05, modify the assertion to compare the default_radius
with the compat_radius (which should be computed earlier in the test) to verify
they produce different results when the vendor namespace is ignored versus when
it is handled. This keeps the test focused on the actual behavior difference
without relying on magic numbers. Keep the existing compat-radius check intact
to maintain validation of the compatibility behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yml
Review profile: CHILL
Plan: Pro
Run ID: 4cf01dd6-41a4-4260-81e3-d408d6579c68
📒 Files selected for processing (8)
CHANGELOG.mdnewton/_src/usd/schema_resolver.pynewton/_src/usd/schemas.pynewton/_src/usd/utils.pynewton/_src/utils/import_usd.pynewton/tests/assets/cable_curve_deformable.usdanewton/tests/assets/tetmesh_with_material.usdanewton/tests/test_usd_deformable.py
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
get_tetmesh gained a compat_namespaces parameter and an updated namespace docstring; mirror both on TetMesh.create_from_usd so the docstring-parity test (test_api) passes. Fixes the CI unittest failure.
…oc return maps - _apply_cable_masses: warn and ignore physics:masses whose length != the curve point count, instead of summing a mismatched array. - Cloth import: bake a non-uniform xformOp:scale into the vertices (add_cloth_mesh only takes a uniform scale) so it is not silently dropped. - Document path_cable_map / path_cloth_map / path_soft_map in the parse_usd return contract. - Add regressions for both fixes.
- Periodic curves now build a body for the closing v[-1]->v[0] segment (close the polyline before add_rod, which makes len-1 bodies + the loop joint). A 4-point periodic curve now imports 4 bodies / 4 joints. - Remap path_cable_map body/joint indices through collapse_fixed_joints' body_remap/joint_remap so the map stays valid when collapsing reindexes bodies (cables parse after rigid bodies, so their indices would shift). - Add regressions for both.
The default-thickness fallback only triggered when the surface material authored cloth fields, but a volumetric density can also resolve from a PhysicsDeformableBodyAPI override or a base physics material. Neither source can author a surface thickness, so the volumetric value was passed to add_cloth_mesh() as areal density, inflating the mass by roughly 1/thickness (~500x at 1000 kg/m3 vs the 2 mm default). Resolve the density before the thickness fallback and apply the default (with the existing warning) whenever any volumetric source resolves without an authored thickness.
The scout bucketed prims and assigned body ownership before ignore_paths was applied, so an ignored first simulation child could still claim its deformable body. The lowering pass then skipped the ignored prim AND skipped its non-ignored sibling as additional simulation geometry, importing neither. Filter ignored prims at scout time so they are as-if-absent for bucketing and ownership. The check short-circuits on an empty ignore_paths, leaving the deformable-free import path unchanged (scout benchmark unchanged at 3.3 ms median on 3,000 Xforms).
A body-mass-only volume deformable built its particle masses from the TetMesh density or the builder's default_tet_density. When that fallback is zero, the particle masses are zero and the body-mass rescale in _apply_particle_masses() has no positive total to distribute, silently losing the authored mass. Pass the neutral weight density (1.0) to add_soft_mesh() when a body mass is authored without a positive density source, as the cable and cloth paths already do: the rescale turns the volume-proportional masses into the proposal's density-independent m_p = m_tot * V_p / V_tot. The resolved_density metadata keeps reporting the unmodified resolution instead of the neutral weight.
The flag only controls whether the extra deformable result entries are returned, not whether deformables import, and existing result-gating kwargs use the return_ prefix (return_uv_indices, return_diagnostics). Plain rename without a deprecation: the kwarg has not shipped in a release.
The legacy-default deprecation warning fired only for vendor-namespaced material attributes. A material that authors canonical physics: moduli without PhysicsVolumeDeformableMaterialAPI is the second case where the deprecated read-any-material default is load-bearing: the default reads the moduli, while canonical-only (compat_namespaces=()) scopes them to API-applied materials and silently drops them. Warn for that case too, keeping the no-warn guarantee for API-applied canonical and render-only materials. The add_usd() vendor-recovery gate is unchanged: it enables vendor namespaces when it fires, which would be wrong for canonical-attrs-only materials.
physics:filteredPairs was collected only from native colliders and applied with direct path_shape_map indexing before the deformable passes ran, so a pair targeting a cable raised KeyError, a pair authored on the deformable side or on a rigid-body prim was silently dropped, and a self-referencing pair produced an invalid self-filter. Collect canonicalized path pairs from native colliders, rigid-body prims (via path_body_map, covering every body creation path), and deformable participants (simulation geometry, deformable body prims, owned colliders), and apply them after deformable lowering through a set-valued endpoint resolver: a collider is one shape, a rigid body or cable is all of its shapes, and a deformable body resolves through its simulation geometry. Cloth and volume endpoints are particles Newton's shape filters cannot express, so those pairs warn with both paths and are kept out of the model, as are missing or non-participating targets. Application seeds its dedup set from the builder so pairs the element-filter pass already added are not appended twice.
State explicitly that deformable import is an initial implementation of a subset of the AOUSD proposal: not fully proposal-compliant and not compatible with native OmniPhysics/PhysX deformable assets. The vendor-namespace wording implied that a schema resolver makes Omni assets work; SchemaResolverPhysx only enables reading the same proposal-shaped material attributes under omniphysics: / physxDeformableBody: namespaces. It does not translate applied schemas, renamed attributes, attachments, pose purposes, or hierarchy conventions, so a native Omni deformable without the AOUSD sim APIs imports as ordinary static geometry. Say so in the parsing guide and the resolver docstring, and document UsdPhysicsCollisionGroup membership as not applied to deformables (per-pair filtering only).
adenzler-nvidia
left a comment
There was a problem hiding this comment.
One leftover to clean up before merge; otherwise this looks ready to me.
A main merge duplicated the two VBD damping entries and the MJCF/USD margin-gap entry from Changed into the Deprecated section; upstream main carries each exactly once under Changed.
jcarius-nv
left a comment
There was a problem hiding this comment.
Thanks @mzamoramora-nvidia , I just checked with Alain. Looks like everything is addressed, let's get this in!
|
Closes #3178 |
Description
Closes #3178
ModelBuilder.add_usd()now imports deformable bodies authored with the AOUSD UsdPhysics Deformables proposal.This is an initial implementation of a subset of the proposal: not fully proposal-compliant, and not compatible with native OmniPhysics/PhysX deformable assets (details in
docs/concepts/usd_parsing.rst).What imports as what
GeomBasisCurves+PhysicsCurvesDeformableSimAPIGeomMesh+PhysicsSurfaceDeformableSimAPIUsdGeom.TetMesh+PhysicsVolumeDeformableSimAPIPhysicsAttachment(hard)PhysicsElementCollisionFilterMaterials and mass
physics:namespace. A schema resolver (e.g.SchemaResolverPhysx) only adds the same proposal-shaped attributes under vendor namespaces on bound materials; it does not translate OmniPhysics applied schemas or asset structure, so a native Omni deformable without the AOUSD sim APIs imports as ordinary static geometry. Exception: old vendor TetMesh materials keep working for now, with aDeprecationWarning.physics:masses, then bodymass/density, then material density.Collision
PhysicsCollisionAPI. A dedicated collider in the body hierarchy also enables it, approximated by the simulation geometry with a warning.physics:filteredPairsworks for shape-backed participants (colliders, rigid bodies, cables, deformable bodies owning a cable); cloth/volume pairs warn.UsdPhysicsCollisionGroupis not applied to deformables.Edge cases warn, never silently change the model
PhysicsDeformableBodyAPI+RigidBodyAPIon one prim imports as rigid.Full details:
docs/concepts/usd_parsing.rst(part of this PR).API
One experimental, keyword-only option:
add_usd(..., return_deformable_results=True)(off by default). It adds prim-path index maps and authored material values to the returned mapping. These results may change or be removed without notice. Nothing else is added toModelorModelBuilder. Batched selection is the stacked follow-up #3326.Addresses the deformable-import scope of #3178 and the linked #3036 / #3037 / #3038.
Checklist
CHANGELOG.mdhas been updated (if user-facing change)Test plan
One filter runs the whole area: the per-family suites, attachments and filters, group bookkeeping, a mixed-scene CUDA smoke test, and the pre-existing rigid/TetMesh import tests.
Validation
mzamoramora/usd-deformable-examplesbuilds each cable/cloth/soft-body example twice, from code and from USD, and checks that both models match.New feature / API change
Summary by CodeRabbit
deformable_resultsreturning prim-path maps and build-time attribute snapshots.PhysicsAttachmentandPhysicsElementCollisionFilter, including collision-filter pairing behavior.compat_namespaces(keyword-only) for TetMesh deformable material parsing with canonical-first fallback behavior.DEFORMABLE_LEGACY_NAMESPACES.physics:attributes are preferred.