Skip to content

Commit 8c6cd87

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 3cd0fe8 commit 8c6cd87

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
@@ -19,6 +19,7 @@
1919
- Add `ViewerRTX`, a real-time ray-traced viewer powered by NVIDIA OVRTX.
2020
- Support negative (mirrored) scale on mesh, convex hull, and SDF shapes, so a single `Mesh` instance can be shared across shapes with different signed scales without re-baking
2121
- Add `newton.utils.ColorSpace`, `color_srgb_to_linear()`, `color_linear_to_srgb()`, and `SensorTiledCamera.RenderConfig.output_color_space` for color-space boundaries
22+
- Add USD parsing for contact response attributes (`ke`, `kd`, `kf`, `ka`) from `NewtonMaterialAPI` on bound materials
2223

2324
### Changed
2425

@@ -35,6 +36,7 @@
3536

3637
- 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.
3738
- Deprecate implicit positive Dahl defaults in `SolverVBD.register_custom_attributes()`. Pass `dahl_defaults_enabled=False` and explicitly author positive `model.vbd.dahl_eps_max` and `model.vbd.dahl_tau` values when Dahl cable friction is desired, instead of relying on registered default values.
39+
- Deprecate `newton:contact_ke`/`newton:contact_kd` custom attributes on shape prims; author `newton:contactStiffness`/`newton:contactDamping` on the bound `NewtonMaterialAPI` material instead
3840

3941
### Removed
4042

docs/concepts/collisions.rst

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1672,20 +1672,72 @@ Example:
16721672
USD Integration
16731673
---------------
16741674

1675-
Custom collision properties can be authored in USD:
1675+
Newton provides several USD schema APIs for authoring collision and contact
1676+
properties directly in USD layers.
1677+
1678+
**NewtonCollisionAPI**
1679+
1680+
Applied to collision shapes to configure per-shape contact detection. The
1681+
``newton:contactMargin`` and ``newton:contactGap`` attributes map to
1682+
:attr:`~ModelBuilder.ShapeConfig.margin` and :attr:`~ModelBuilder.ShapeConfig.gap`
1683+
respectively.
1684+
1685+
.. code-block:: usda
1686+
1687+
def Cube "Collider" (
1688+
prepend apiSchemas = ["PhysicsCollisionAPI", "NewtonCollisionAPI"]
1689+
) {
1690+
float newton:contactMargin = 0.001
1691+
float newton:contactGap = 0.02
1692+
}
1693+
1694+
**NewtonMaterialAPI**
1695+
1696+
Extends ``PhysicsMaterialAPI`` with torsional/rolling friction
1697+
(``newton:torsionalFriction``, ``newton:rollingFriction``) and contact response
1698+
attributes. The contact response attributes map to :class:`~ModelBuilder.ShapeConfig`
1699+
fields as follows: ``newton:contactStiffness`` → ``ke``,
1700+
``newton:contactDamping`` → ``kd``, ``newton:contactFrictionStiffness`` → ``kf``,
1701+
``newton:contactAdhesion`` → ``ka``. A value of ``-inf`` means "use the engine's
1702+
default" (see :ref:`Contact Material Properties`).
16761703

16771704
.. code-block:: usda
16781705
1679-
def Xform "Box" (
1680-
prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsCollisionAPI"]
1706+
def Material "RubberMaterial" (
1707+
prepend apiSchemas = ["PhysicsMaterialAPI", "NewtonMaterialAPI"]
1708+
) {
1709+
float physics:staticFriction = 1.0
1710+
float physics:dynamicFriction = 0.8
1711+
float newton:torsionalFriction = 0.1
1712+
float newton:rollingFriction = 0.01
1713+
float newton:contactStiffness = 5000.0
1714+
float newton:contactDamping = 200.0
1715+
float newton:contactFrictionStiffness = 800.0
1716+
float newton:contactAdhesion = 0.0
1717+
}
1718+
1719+
def Cube "Collider" (
1720+
prepend apiSchemas = ["PhysicsCollisionAPI"]
1721+
) {
1722+
rel material:binding:physics = </RubberMaterial>
1723+
}
1724+
1725+
**NewtonMeshCollisionAPI**
1726+
1727+
Applied on top of ``PhysicsMeshCollisionAPI`` to control mesh approximation.
1728+
Currently exposes ``newton:maxHullVertices`` for convex hull generation.
1729+
1730+
**Custom Properties**
1731+
1732+
Additional per-shape attributes that Newton reads:
1733+
1734+
.. code-block:: usda
1735+
1736+
def Cube "Collider" (
1737+
prepend apiSchemas = ["PhysicsCollisionAPI"]
16811738
) {
16821739
custom int newton:collision_group = 1
1683-
custom int newton:world = 0
1684-
custom float newton:contact_ke = 100000.0
1685-
custom float newton:contact_kd = 1000.0
1686-
custom float newton:contact_kf = 1000.0
1687-
custom float newton:contact_ka = 0.0
1688-
custom float newton:margin = 0.00001
1740+
custom bool newton:is_sensor = false
16891741
}
16901742
16911743
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
@@ -281,6 +282,10 @@ class PhysicsMaterial:
281282
rollingFriction: float = builder.default_shape_cfg.mu_rolling
282283
restitution: float = builder.default_shape_cfg.restitution
283284
density: float = builder.default_shape_cfg.density
285+
ke: float | None = None
286+
kd: float | None = None
287+
kf: float | None = None
288+
ka: float | None = None
284289

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

17951811
if UsdPhysics.ObjectType.RigidBody in ret_dict:
@@ -2611,7 +2627,6 @@ def _build_mass_info_from_shape_geometry(
26112627
shape_density = material.density
26122628
else:
26132629
shape_density = default_shape_density
2614-
prim_and_scene = (prim, physics_scene_prim)
26152630
local_xform = wp.transform(shape_spec.localPos, usd.value_to_warp(shape_spec.localRot))
26162631
if body_id == -1:
26172632
shape_xform = incoming_world_xform * local_xform
@@ -2658,22 +2673,22 @@ def _build_mass_info_from_shape_geometry(
26582673
) and not hide_collider_for_body
26592674
collider_is_visible = collider_is_visible and _is_effectively_visible(prim)
26602675

2661-
shape_ke = R.get_value(
2662-
prim,
2663-
prim_type=PrimType.SHAPE,
2664-
key="ke",
2665-
verbose=verbose,
2666-
)
2667-
if shape_ke is None:
2668-
shape_ke = builder.default_shape_cfg.ke
2669-
shape_kd = R.get_value(
2670-
prim,
2671-
prim_type=PrimType.SHAPE,
2672-
key="kd",
2673-
verbose=verbose,
2674-
)
2675-
if shape_kd is None:
2676-
shape_kd = builder.default_shape_cfg.kd
2676+
# Contact response: material (if authored) > per-shape resolver > builder default.
2677+
_default = builder.default_shape_cfg
2678+
shape_ke = material.ke if material.ke is not None and math.isfinite(material.ke) else _default.ke
2679+
shape_kd = material.kd if material.kd is not None and math.isfinite(material.kd) else _default.kd
2680+
shape_kf = material.kf if material.kf is not None and math.isfinite(material.kf) else _default.kf
2681+
shape_ka = material.ka if material.ka is not None and math.isfinite(material.ka) else _default.ka
2682+
2683+
for attr_key in ("ke", "kd"):
2684+
if getattr(material, attr_key) is not None:
2685+
continue
2686+
per_shape_val = R.get_value(prim, prim_type=PrimType.SHAPE, key=attr_key, verbose=verbose)
2687+
if per_shape_val is not None and math.isfinite(float(per_shape_val)):
2688+
if attr_key == "ke":
2689+
shape_ke = float(per_shape_val)
2690+
else:
2691+
shape_kd = float(per_shape_val)
26772692

26782693
shape_color = material_props.get("color")
26792694
shape_params = {
@@ -2682,12 +2697,8 @@ def _build_mass_info_from_shape_geometry(
26822697
"cfg": ModelBuilder.ShapeConfig(
26832698
ke=shape_ke,
26842699
kd=shape_kd,
2685-
kf=usd.get_float_with_fallback(
2686-
prim_and_scene, "newton:contact_kf", builder.default_shape_cfg.kf
2687-
),
2688-
ka=usd.get_float_with_fallback(
2689-
prim_and_scene, "newton:contact_ka", builder.default_shape_cfg.ka
2690-
),
2700+
kf=shape_kf,
2701+
ka=shape_ka,
26912702
margin=margin_val,
26922703
gap=gap_val,
26932704
mu=material.dynamicFriction,

0 commit comments

Comments
 (0)