Skip to content

[REQ] Per-cable contact configuration for imported cable shapes #3734

Description

@mmichelis

Description

The USD cable importer builds a single ShapeConfig per curve and overrides only three fields on the import-wide default:

cable_cfg = replace(
    builder.default_shape_cfg,
    density=...,
    has_shape_collision=collision_enabled,
    has_particle_collision=collision_enabled,
)

newton/_src/utils/import_usd_deformable_cable.py:624-628 (single cable) and :397-401 (welded graph).

Every other contact field -- ke, kd, kf, ka, mu, margin, gap -- is inherited from ModelBuilder.default_shape_cfg, so contact attributes authored on a cable are silently ignored. The rigid path already resolves the same fields per shape:

  • newton:contactStiffness, newton:contactDamping, newton:contactFrictionGain, newton:contactAdhesion from the bound NewtonMaterialAPI material (import_usd.py:2483-2486, applied at :3389-3407)
  • newton:contactMargin, newton:contactGap from NewtonCollisionAPI on the collider prim (import_usd.py:3330-3344)

Request: resolve those same attributes in the cable path, at per-cable granularity. Concretely, one ShapeConfig per cable prim, derived from that cable's own bound material and collider prim, falling back to default_shape_cfg field by field when an attribute is unauthored. For welded graphs this means one config per component, matching how the importer already flattens a component to a representative material and warns when members disagree (import_usd_deformable_cable.py:352-372).

This keeps the existing structure: the two replace(builder.default_shape_cfg, ...) sites stay, they just gain the resolved contact fields alongside density. No change to add_rod / add_rod_graph is needed.

Reproduction script

import newton
from pxr import Sdf, Usd, UsdGeom, UsdPhysics, UsdShade

KE, KD, KF, KA, MU, MARGIN, GAP = 7.5e3, 42.0, 333.0, 0.01, 0.35, 0.02, 0.05


def author_material(stage, path, curves_material):
    prim = UsdShade.Material.Define(stage, path).GetPrim()
    UsdPhysics.MaterialAPI.Apply(prim)
    prim.AddAppliedSchema("NewtonMaterialAPI")
    attrs = {
        "physics:dynamicFriction": MU,
        "newton:contactStiffness": KE,
        "newton:contactDamping": KD,
        "newton:contactFrictionGain": KF,
        "newton:contactAdhesion": KA,
    }
    if curves_material:
        prim.AddAppliedSchema("PhysicsCurvesDeformableMaterialAPI")
        attrs |= {"physics:thickness": 0.02, "physics:density": 1000.0,
                  "physics:stretchStiffness": 1.0e6, "physics:bendStiffness": 2.0e4}
    for name, value in attrs.items():
        prim.CreateAttribute(name, Sdf.ValueTypeNames.Float).Set(value)
    return prim


def make_collider(prim):
    UsdPhysics.CollisionAPI.Apply(prim)
    prim.AddAppliedSchema("NewtonCollisionAPI")
    prim.CreateAttribute("newton:contactMargin", Sdf.ValueTypeNames.Float).Set(MARGIN)
    prim.CreateAttribute("newton:contactGap", Sdf.ValueTypeNames.Float).Set(GAP)


def bind(prim, material_prim):
    UsdShade.MaterialBindingAPI.Apply(prim).Bind(
        UsdShade.Material(material_prim), UsdShade.Tokens.weakerThanDescendants, "physics"
    )


def new_stage():
    stage = Usd.Stage.CreateInMemory()
    UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.z)
    UsdPhysics.Scene.Define(stage, "/World/PhysicsScene")
    return stage


def show(label, builder):
    shapes = range(builder.shape_count)
    print(f"--- {label} ({builder.shape_count} shape(s)) ---")
    for key in ("ke", "kd", "kf", "ka", "mu"):
        print(f"  {key:<6} = {[round(getattr(builder, f'shape_material_{key}')[s], 5) for s in shapes]}")
    print(f"  margin = {[round(builder.shape_margin[s], 5) for s in shapes]}")
    print(f"  gap    = {[round(builder.shape_gap[s], 5) for s in shapes]}")


stage = new_stage()
curves = UsdGeom.BasisCurves.Define(stage, "/World/Cable")
curves.CreateTypeAttr(UsdGeom.Tokens.linear)
curves.CreateWrapAttr(UsdGeom.Tokens.nonperiodic)
curves.CreatePointsAttr([(0.0, 0.0, 1.0), (0.1, 0.0, 1.0), (0.2, 0.0, 1.0), (0.3, 0.0, 1.0)])
curves.CreateCurveVertexCountsAttr([4])
curves.GetPrim().AddAppliedSchema("PhysicsCurvesDeformableSimAPI")
make_collider(curves.GetPrim())
bind(curves.GetPrim(), author_material(stage, "/World/CableMat", curves_material=True))

cable_builder = newton.ModelBuilder()
cable_builder.add_usd(stage)
show("CABLE capsules", cable_builder)

stage = new_stage()
cap = UsdGeom.Capsule.Define(stage, "/World/Body/Capsule")
UsdPhysics.RigidBodyAPI.Apply(stage.GetPrimAtPath("/World/Body"))
make_collider(cap.GetPrim())
bind(cap.GetPrim(), author_material(stage, "/World/RigidMat", curves_material=False))

rigid_builder = newton.ModelBuilder()
rigid_builder.add_usd(stage)
show("RIGID capsule", rigid_builder)

d = newton.ModelBuilder().default_shape_cfg
print(f"\nauthored: ke={KE} kd={KD} kf={KF} ka={KA} mu={MU} margin={MARGIN} gap={GAP}")
print(f"defaults: ke={d.ke} kd={d.kd} kf={d.kf} ka={d.ka} mu={d.mu} margin={d.margin} gap={d.gap}")

Output (newton 1.5.0.dev0). Identical contact authoring; the rigid capsule takes all seven values, the cable takes none:

--- CABLE capsules (3 shape(s)) ---
  ke     = [2500.0, 2500.0, 2500.0]
  kd     = [100.0, 100.0, 100.0]
  kf     = [1000.0, 1000.0, 1000.0]
  ka     = [0.0, 0.0, 0.0]
  mu     = [1.0, 1.0, 1.0]
  margin = [0.0, 0.0, 0.0]
  gap    = [0.1, 0.1, 0.1]
--- RIGID capsule (1 shape(s)) ---
  ke     = [7500.0]
  kd     = [42.0]
  kf     = [333.0]
  ka     = [0.01]
  mu     = [0.35]
  margin = [0.02]
  gap    = [0.05]

authored: ke=7500.0 kd=42.0 kf=333.0 ka=0.01 mu=0.35 margin=0.02 gap=0.05
defaults: ke=2500.0 kd=100.0 kf=1000.0 ka=0.0 mu=1.0 margin=0.0 gap=None

Motivation / Use Case

Cable behaviour is contact-dominated: routing through clips, sliding in a gripper, coiling on a surface. Those scenarios are tuned with friction and the contact penalty parameters, and none of them are reachable per cable today.

The only available lever is setting builder.default_shape_cfg before import. That default is shared with every other shape in the scene that has no bound material, so raising a cable's friction also raises it for unrelated colliders. Isaac Lab surfaces exactly this global as NewtonCfg.default_shape_cfg and runs into the same coupling.

Alternatives Considered

  • default_shape_cfg before import. Works, but is import-wide, so it cannot express two cables with different contact properties, nor a cable that differs from the rest of the scene.
  • Mutating model.shape_material_* after finalize(). Reachable via path_cable_map plus body_shapes, but it reaches past the importer and reimplements attribute resolution that already exists for rigid shapes.
  • PR Fix USD cable contact materials #3663 (open). Adds physics:dynamicFriction and physics:restitution from the base UsdPhysicsMaterialAPI to cable capsules. That covers mu, but not the penalty parameters or margin/gap, and it does not go through the NewtonMaterialAPI resolution the rigid path uses (added in Parse contact response attributes from NewtonMaterialAPI #3005). This request is the remaining set, and is complementary rather than a duplicate.
  • Per-segment granularity. Deliberately not requested: it would require a builder change, since add_rod and add_rod_graph accept a single cfg for the whole rod (builder.py:7600, :7829) and pass it to every capsule (:8020). Per-cable needs no such change and covers the use cases above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Needs Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions