Skip to content

Commit 0d2d056

Browse files
andrewkaufmanclaude
andcommitted
Parse contact response attributes from NewtonMaterialAPI
Read ke/kd/kf/ka from NewtonMaterialAPI on bound materials during USD import. The schema resolver resolves these across Newton, PhysX (compliantContactStiffness/Damping), and MuJoCo (solref) at the MATERIAL level, with configurable resolver priority. Legacy newton:contact_ke/kd custom attributes on shape prims are retained as a fallback when no material value is authored, emitting a DeprecationWarning at resolve time directing users to the NewtonMaterialAPI attrs. MuJoCo mjc:solref on material prims is similarly deprecated. Explicitly authoring -inf on a material attr ("use engine default") blocks legacy fallback. Requires newton-usd-schemas>=0.3.0. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5dd30ef commit 0d2d056

8 files changed

Lines changed: 495 additions & 46 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- Add robotics tutorial notebook covering ModelBuilder, solvers, CUDA graphs, IK, and pick-and-place
1313
- Add `newton.utils.OnnxRuntime`, a graph-capturable ONNX inference engine backed solely by Warp kernels (no `onnxruntime` or `torch` runtime dependency); used by `ControllerNeuralMLP` and `ControllerNeuralLSTM` to load `.onnx` policies. To migrate a TorchScript policy, run `torch.onnx.export(model, dummy_input, "policy.onnx", opset_version=17)` once and point the controllers at the resulting `.onnx` file. The `onnx` package is now an optional extra (`pip install newton[onnx]`); install it explicitly to use the ONNX runtime.
1414
- Add USD parsing for `NewtonSiteAPI` to mark shapes as sites.
15+
- Add USD parsing for contact response attributes (`ke`, `kd`, `kf`, `ka`) from `NewtonMaterialAPI` on bound materials
1516

1617
### Changed
1718

@@ -22,6 +23,7 @@
2223
### Deprecated
2324

2425
- Deprecate loading `.pt` / `.pth` (TorchScript) checkpoints via `ControllerNeuralMLP`; the legacy TorchScript / dict-checkpoint path still works (with a `DeprecationWarning`) when PyTorch is installed but will be removed in a future release. `ControllerNeuralLSTM` requires re-exporting to ONNX with the metadata properties documented in its class docstring; pointing it at a `.pt` checkpoint now raises `NotImplementedError` with migration guidance. Convert the MLP checkpoint to ONNX once with `torch.onnx.export(model, dummy_input, "policy.onnx", opset_version=17)` and load the resulting `.onnx` file.
26+
- Deprecate `newton:contact_ke`/`kd` custom attributes on shape prims; author `newton:contactStiffness`/`newton:contactDamping` on the bound `NewtonMaterialAPI` material instead
2527

2628
### Removed
2729

docs/concepts/collisions.rst

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,20 +1654,72 @@ Example:
16541654
USD Integration
16551655
---------------
16561656

1657-
Custom collision properties can be authored in USD:
1657+
Newton provides several USD schema APIs for authoring collision and contact
1658+
properties directly in USD layers.
1659+
1660+
**NewtonCollisionAPI**
1661+
1662+
Applied to collision shapes to configure per-shape contact detection. The
1663+
``newton:contactMargin`` and ``newton:contactGap`` attributes map to
1664+
:attr:`~ModelBuilder.ShapeConfig.margin` and :attr:`~ModelBuilder.ShapeConfig.gap`
1665+
respectively.
1666+
1667+
.. code-block:: usda
1668+
1669+
def Cube "Collider" (
1670+
prepend apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI"]
1671+
) {
1672+
float newton:contactMargin = 0.001
1673+
float newton:contactGap = 0.02
1674+
}
1675+
1676+
**NewtonMaterialAPI**
1677+
1678+
Extends ``PhysicsMaterialAPI`` with torsional/rolling friction
1679+
(``newton:torsionalFriction``, ``newton:rollingFriction``) and contact response
1680+
attributes. The contact response attributes map to :class:`~ModelBuilder.ShapeConfig`
1681+
fields as follows: ``newton:contactStiffness`` → ``ke``,
1682+
``newton:contactDamping`` → ``kd``, ``newton:contactFrictionStiffness`` → ``kf``,
1683+
``newton:contactAdhesion`` → ``ka``. A value of ``-inf`` means "use the engine's
1684+
default" (see :ref:`Contact Material Properties`).
16581685

16591686
.. code-block:: usda
16601687
1661-
def Xform "Box" (
1662-
prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsCollisionAPI"]
1688+
def Material "RubberMaterial" (
1689+
prepend apiSchemas = ["PhysicsMaterialAPI", "NewtonMaterialAPI"]
1690+
) {
1691+
float physics:staticFriction = 1.0
1692+
float physics:dynamicFriction = 0.8
1693+
float newton:torsionalFriction = 0.1
1694+
float newton:rollingFriction = 0.01
1695+
float newton:contactStiffness = 5000.0
1696+
float newton:contactDamping = 200.0
1697+
float newton:contactFrictionStiffness = 800.0
1698+
float newton:contactAdhesion = 0.0
1699+
}
1700+
1701+
def Cube "Collider" (
1702+
prepend apiSchemas = ["PhysicsCollisionAPI"]
1703+
) {
1704+
rel material:binding:physics = </RubberMaterial>
1705+
}
1706+
1707+
**NewtonMeshCollisionAPI**
1708+
1709+
Applied on top of ``PhysicsMeshCollisionAPI`` to control mesh approximation.
1710+
Currently exposes ``newton:maxHullVertices`` for convex hull generation.
1711+
1712+
**Custom Properties**
1713+
1714+
Additional per-shape attributes that Newton reads:
1715+
1716+
.. code-block:: usda
1717+
1718+
def Cube "Collider" (
1719+
prepend apiSchemas = ["PhysicsCollisionAPI"]
16631720
) {
16641721
custom int newton:collision_group = 1
1665-
custom int newton:world = 0
1666-
custom float newton:contact_ke = 100000.0
1667-
custom float newton:contact_kd = 1000.0
1668-
custom float newton:contact_kf = 1000.0
1669-
custom float newton:contact_ka = 0.0
1670-
custom float newton:margin = 0.00001
1722+
custom bool newton:is_sensor = false
16711723
}
16721724
16731725
See :doc:`custom_attributes` and :doc:`usd_parsing` for details.

newton/_src/usd/schemas.py

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,42 @@ def _physx_gap_from_prim(prim: Usd.Prim) -> float | None:
3939
return float(contact_offset) - float(rest_offset)
4040

4141

42+
def _newton_legacy_contact_attr(legacy_name: str, material_attr: str):
43+
"""Return a getter that reads a legacy contact custom attr with a deprecation warning."""
44+
45+
def _getter(prim: Usd.Prim) -> float | None:
46+
value = usd.get_attribute(prim, legacy_name)
47+
if value is not None:
48+
warnings.warn(
49+
f"'{legacy_name}' on shape prim is deprecated; "
50+
f"author '{material_attr}' on the bound NewtonMaterialAPI material instead.",
51+
DeprecationWarning,
52+
stacklevel=4,
53+
)
54+
return float(value)
55+
return None
56+
57+
return _getter
58+
59+
60+
def _mjc_legacy_material_solref(converter, material_attr: str):
61+
"""Return a getter that reads legacy mjc:solref on a material prim with a deprecation warning."""
62+
63+
def _getter(prim: Usd.Prim) -> float | None:
64+
value = usd.get_attribute(prim, "mjc:solref")
65+
if value is not None:
66+
warnings.warn(
67+
f"'mjc:solref' on material prim is deprecated; "
68+
f"author '{material_attr}' on the bound NewtonMaterialAPI material instead.",
69+
DeprecationWarning,
70+
stacklevel=4,
71+
)
72+
return converter(value)
73+
return None
74+
75+
return _getter
76+
77+
4278
class SchemaResolverNewton(SchemaResolver):
4379
"""Schema resolver for Newton-authored USD attributes.
4480
@@ -84,9 +120,17 @@ class SchemaResolverNewton(SchemaResolver):
84120
# Collisions: newton margin == newton:contactMargin, newton gap == newton:contactGap
85121
"margin": SchemaAttribute("newton:contactMargin", 0.0),
86122
"gap": SchemaAttribute("newton:contactGap", float("-inf")),
87-
# Contact stiffness/damping
88-
"ke": SchemaAttribute("newton:contact_ke", None),
89-
"kd": SchemaAttribute("newton:contact_kd", None),
123+
# Legacy per-shape contact attrs (deprecated; use NewtonMaterialAPI instead)
124+
"ke": SchemaAttribute(
125+
"newton:contact_ke",
126+
None,
127+
usd_value_getter=_newton_legacy_contact_attr("newton:contact_ke", "newton:contactStiffness"),
128+
),
129+
"kd": SchemaAttribute(
130+
"newton:contact_kd",
131+
None,
132+
usd_value_getter=_newton_legacy_contact_attr("newton:contact_kd", "newton:contactDamping"),
133+
),
90134
},
91135
PrimType.BODY: {},
92136
PrimType.ARTICULATION: {
@@ -95,6 +139,10 @@ class SchemaResolverNewton(SchemaResolver):
95139
PrimType.MATERIAL: {
96140
"mu_torsional": SchemaAttribute("newton:torsionalFriction", 0.25),
97141
"mu_rolling": SchemaAttribute("newton:rollingFriction", 0.0005),
142+
"ke": SchemaAttribute("newton:contactStiffness", None),
143+
"kd": SchemaAttribute("newton:contactDamping", None),
144+
"kf": SchemaAttribute("newton:contactFrictionStiffness", None),
145+
"ka": SchemaAttribute("newton:contactAdhesion", None),
98146
},
99147
PrimType.ACTUATOR: {},
100148
}
@@ -181,8 +229,8 @@ class SchemaResolverPhysx(SchemaResolver):
181229
),
182230
},
183231
PrimType.MATERIAL: {
184-
"stiffness": SchemaAttribute("physxMaterial:compliantContactStiffness", 0.0),
185-
"damping": SchemaAttribute("physxMaterial:compliantContactDamping", 0.0),
232+
"ke": SchemaAttribute("physxMaterial:compliantContactStiffness", None),
233+
"kd": SchemaAttribute("physxMaterial:compliantContactDamping", None),
186234
},
187235
PrimType.BODY: {
188236
# Rigid body damping
@@ -344,8 +392,8 @@ class SchemaResolverMjc(SchemaResolver):
344392
),
345393
"gap": SchemaAttribute("mjc:gap", 0.0),
346394
# Contact stiffness/damping from per-geom solref
347-
"ke": SchemaAttribute("mjc:solref", [0.02, 1.0], solref_to_stiffness),
348-
"kd": SchemaAttribute("mjc:solref", [0.02, 1.0], solref_to_damping),
395+
"ke": SchemaAttribute("mjc:solref", None, solref_to_stiffness),
396+
"kd": SchemaAttribute("mjc:solref", None, solref_to_damping),
349397
},
350398
PrimType.MATERIAL: {
351399
# Materials
@@ -354,8 +402,16 @@ class SchemaResolverMjc(SchemaResolver):
354402
# Contact models
355403
"priority": SchemaAttribute("mjc:priority", 0),
356404
"weight": SchemaAttribute("mjc:solmix", 1.0),
357-
"stiffness": SchemaAttribute("mjc:solref", [0.02, 1.0], solref_to_stiffness),
358-
"damping": SchemaAttribute("mjc:solref", [0.02, 1.0], solref_to_damping),
405+
"ke": SchemaAttribute(
406+
"mjc:solref",
407+
None,
408+
usd_value_getter=_mjc_legacy_material_solref(solref_to_stiffness, "newton:contactStiffness"),
409+
),
410+
"kd": SchemaAttribute(
411+
"mjc:solref",
412+
None,
413+
usd_value_getter=_mjc_legacy_material_solref(solref_to_damping, "newton:contactDamping"),
414+
),
359415
},
360416
PrimType.ACTUATOR: {
361417
# Actuators

newton/_src/utils/import_usd.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import datetime
99
import inspect
1010
import itertools
11+
import math
1112
import os
1213
import posixpath
1314
import re
@@ -280,6 +281,10 @@ class PhysicsMaterial:
280281
rollingFriction: float = builder.default_shape_cfg.mu_rolling
281282
restitution: float = builder.default_shape_cfg.restitution
282283
density: float = builder.default_shape_cfg.density
284+
ke: float | None = None
285+
kd: float | None = None
286+
kf: float | None = None
287+
ka: float | None = None
283288

284289
# load joint defaults
285290
default_joint_friction = builder.default_joint_cfg.friction
@@ -1768,6 +1773,13 @@ def warn_invalid_desc(path, descriptor) -> bool:
17681773
if warn_invalid_desc(sdf_path, desc):
17691774
continue
17701775
prim = stage.GetPrimAtPath(sdf_path)
1776+
1777+
def _resolve_contact_attr(key, _prim=prim):
1778+
val = R.get_value(_prim, prim_type=PrimType.MATERIAL, key=key, verbose=verbose)
1779+
if val is None:
1780+
return None
1781+
return float(val)
1782+
17711783
material_specs[str(sdf_path)] = PhysicsMaterial(
17721784
staticFriction=desc.staticFriction,
17731785
dynamicFriction=desc.dynamicFriction,
@@ -1789,6 +1801,10 @@ def warn_invalid_desc(path, descriptor) -> bool:
17891801
# Treat non-positive/unauthored material density as "use importer default".
17901802
# Authored collider/body MassAPI mass+inertia is handled later.
17911803
density=desc.density if desc.density > 0.0 else default_shape_density,
1804+
ke=_resolve_contact_attr("ke"),
1805+
kd=_resolve_contact_attr("kd"),
1806+
kf=_resolve_contact_attr("kf"),
1807+
ka=_resolve_contact_attr("ka"),
17921808
)
17931809

17941810
if UsdPhysics.ObjectType.RigidBody in ret_dict:
@@ -2585,7 +2601,6 @@ def _build_mass_info_from_shape_geometry(
25852601
shape_density = material.density
25862602
else:
25872603
shape_density = default_shape_density
2588-
prim_and_scene = (prim, physics_scene_prim)
25892604
local_xform = wp.transform(shape_spec.localPos, usd.value_to_warp(shape_spec.localRot))
25902605
if body_id == -1:
25912606
shape_xform = incoming_world_xform * local_xform
@@ -2632,22 +2647,22 @@ def _build_mass_info_from_shape_geometry(
26322647
) and not hide_collider_for_body
26332648
collider_is_visible = collider_is_visible and _is_effectively_visible(prim)
26342649

2635-
shape_ke = R.get_value(
2636-
prim,
2637-
prim_type=PrimType.SHAPE,
2638-
key="ke",
2639-
verbose=verbose,
2640-
)
2641-
if shape_ke is None:
2642-
shape_ke = builder.default_shape_cfg.ke
2643-
shape_kd = R.get_value(
2644-
prim,
2645-
prim_type=PrimType.SHAPE,
2646-
key="kd",
2647-
verbose=verbose,
2648-
)
2649-
if shape_kd is None:
2650-
shape_kd = builder.default_shape_cfg.kd
2650+
# Contact response: material (if authored) > per-shape resolver > builder default.
2651+
_default = builder.default_shape_cfg
2652+
shape_ke = material.ke if material.ke is not None and math.isfinite(material.ke) else _default.ke
2653+
shape_kd = material.kd if material.kd is not None and math.isfinite(material.kd) else _default.kd
2654+
shape_kf = material.kf if material.kf is not None and math.isfinite(material.kf) else _default.kf
2655+
shape_ka = material.ka if material.ka is not None and math.isfinite(material.ka) else _default.ka
2656+
2657+
for attr_key in ("ke", "kd"):
2658+
if getattr(material, attr_key) is not None:
2659+
continue
2660+
per_shape_val = R.get_value(prim, prim_type=PrimType.SHAPE, key=attr_key, verbose=verbose)
2661+
if per_shape_val is not None and math.isfinite(float(per_shape_val)):
2662+
if attr_key == "ke":
2663+
shape_ke = float(per_shape_val)
2664+
else:
2665+
shape_kd = float(per_shape_val)
26512666

26522667
shape_color = material_props.get("color")
26532668
shape_params = {
@@ -2656,12 +2671,8 @@ def _build_mass_info_from_shape_geometry(
26562671
"cfg": ModelBuilder.ShapeConfig(
26572672
ke=shape_ke,
26582673
kd=shape_kd,
2659-
kf=usd.get_float_with_fallback(
2660-
prim_and_scene, "newton:contact_kf", builder.default_shape_cfg.kf
2661-
),
2662-
ka=usd.get_float_with_fallback(
2663-
prim_and_scene, "newton:contact_ka", builder.default_shape_cfg.ka
2664-
),
2674+
kf=shape_kf,
2675+
ka=shape_ka,
26652676
margin=margin_val,
26662677
gap=gap_val,
26672678
mu=material.dynamicFriction,

0 commit comments

Comments
 (0)