Skip to content

Commit e1fabb3

Browse files
committed
Add NewtonJointAPI schema for joint solver configuration
Single-apply schema on PhysicsJoint prims providing six attributes that broadcast uniformly to every DOF: - newton:armature: artificial per-DOF inertia for solver stability - newton:damping: passive velocity-proportional damping (always active) - newton:friction: dry (Coulomb) friction effort opposing motion - newton:velocityLimit: maximum DOF velocity (inf = unlimited) - newton:limitStiffness: limit spring stiffness (-inf = engine default) - newton:limitDamping: limit spring damping (-inf = engine default) All attributes use degrees for angular DOFs. Hard minimum of 0 enforced on armature, damping, friction, and velocityLimit. Limit spring attributes activate when DOF position exceeds the range defined by the joint limits; position-based solvers may ignore them. Registered as NewtonPhysicsJointAPI in plugInfo.json with apiSchemaCanOnlyApplyTo: ["PhysicsJoint"].
1 parent cb76500 commit e1fabb3

3 files changed

Lines changed: 288 additions & 0 deletions

File tree

newton_usd_schemas/generatedSchema.usda

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,123 @@ class NewtonArticulationRootAPI "NewtonArticulationRootAPI" (
368368
)
369369
}
370370

371+
class NewtonJointAPI "NewtonJointAPI" (
372+
doc = """`NewtonJointAPI` applies on top of a `PhysicsJoint`, providing
373+
joint configuration for solver behavior, passive dynamics, and limit spring response.
374+
375+
All scalar attributes broadcast uniformly to every DOF of the joint.
376+
Angular attributes use degrees."""
377+
)
378+
{
379+
float newton:armature = 0 (
380+
doc = """Artificial inertia added to each degree of freedom for solver stability.
381+
382+
In reduced-coordinate solvers this augments the joint-space mass
383+
matrix diagonal. In maximal-coordinate solvers it regularizes the
384+
constraint system along the joint's DOF directions.
385+
386+
The value is broadcast to all DOFs of the joint.
387+
388+
Range: [0, inf)
389+
Units: mass * distance * distance (angular DOFs) or mass (linear DOFs)."""
390+
limits = {
391+
dictionary hard = {
392+
float minimum = 0
393+
}
394+
}
395+
)
396+
397+
float newton:damping = 0 (
398+
doc = """Passive velocity-proportional damping applied to each DOF.
399+
400+
Produces a resistive effort proportional to joint velocity:
401+
`effort = -damping * velocity`. This damping is always active,
402+
regardless of whether limits are violated or drive targets are set.
403+
404+
The value is broadcast to all DOFs of the joint.
405+
406+
Range: [0, inf)
407+
Units: effort * seconds / degrees (angular DOFs) or effort * seconds / distance (linear DOFs)."""
408+
limits = {
409+
dictionary hard = {
410+
float minimum = 0
411+
}
412+
}
413+
)
414+
415+
float newton:friction = 0 (
416+
doc = """Dry (Coulomb) friction effort opposing joint motion.
417+
418+
A constant resistive effort that opposes the direction of motion,
419+
independent of velocity magnitude. The joint must be moving for
420+
this effort to apply.
421+
422+
The value is broadcast to all DOFs of the joint.
423+
424+
Range: [0, inf)
425+
Units: effort."""
426+
limits = {
427+
dictionary hard = {
428+
float minimum = 0
429+
}
430+
}
431+
)
432+
433+
float newton:velocityLimit = inf (
434+
doc = """Maximum allowable DOF velocity.
435+
436+
The solver clamps each DOF's velocity to the range
437+
`[-velocityLimit, +velocityLimit]` at every time step.
438+
A value of `inf` means no velocity clamping is applied.
439+
440+
The value is broadcast to all DOFs of the joint.
441+
442+
Range: (0, inf]
443+
Units: degrees / seconds (angular DOFs) or distance / seconds (linear DOFs)."""
444+
limits = {
445+
dictionary hard = {
446+
float minimum = 0
447+
}
448+
}
449+
)
450+
451+
float newton:limitStiffness = -inf (
452+
doc = """Stiffness of the joint limit spring.
453+
454+
When a DOF's position exceeds the range defined by the joint limits,
455+
a restoring effort is applied: `effort = limitStiffness * penetration`,
456+
where penetration is the signed distance beyond the limit boundary.
457+
458+
Not all solvers use soft limit springs. Position-based solvers may
459+
enforce limits as hard positional constraints and ignore this attribute.
460+
461+
A value of `-inf` means the engine's own default stiffness is used.
462+
463+
The value is broadcast to all DOFs of the joint.
464+
465+
Range: [0, inf) when authored (sentinel `-inf` defers to engine default).
466+
Units: effort / degrees (angular DOFs) or effort / distance (linear DOFs)."""
467+
)
468+
469+
float newton:limitDamping = -inf (
470+
doc = """Damping of the joint limit spring.
471+
472+
When a DOF's position exceeds the range defined by the joint limits,
473+
a velocity-dependent dissipative effort is applied:
474+
`effort = -limitDamping * velocity`.
475+
476+
Not all solvers use soft limit springs. Position-based solvers may
477+
enforce limits as hard positional constraints and ignore this attribute.
478+
479+
A value of `-inf` means the engine's own default damping is used.
480+
481+
The value is broadcast to all DOFs of the joint.
482+
483+
Range: [0, inf) when authored (sentinel `-inf` defers to engine default).
484+
Units: effort * seconds / degrees (angular DOFs) or effort * seconds / distance (linear DOFs)."""
485+
)
486+
}
487+
371488
class NewtonMassAPI "NewtonMassAPI" (
372489
apiSchemas = ["PhysicsMassAPI"]
373490
doc = """`NewtonMassAPI` applies on top of an `Xformable`, providing extra mass attributes for Newton.

newton_usd_schemas/plugInfo.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,20 @@
5656
],
5757
"schemaKind": "singleApplyAPI"
5858
},
59+
"NewtonPhysicsJointAPI": {
60+
"schemaIdentifier": "NewtonJointAPI",
61+
"alias": {
62+
"UsdSchemaBase": "NewtonJointAPI"
63+
},
64+
"autoGenerated": false,
65+
"bases": [
66+
"UsdAPISchemaBase"
67+
],
68+
"schemaKind": "singleApplyAPI",
69+
"apiSchemaCanOnlyApplyTo": [
70+
"PhysicsJoint"
71+
]
72+
},
5973
"NewtonPhysicsMassAPI": {
6074
"schemaIdentifier": "NewtonMassAPI",
6175
"alias": {

tests/test_joint.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import math
5+
import unittest
6+
7+
from pxr import Plug, Usd, UsdPhysics
8+
9+
import newton_usd_schemas # noqa: F401
10+
11+
USD_HAS_LIMITS = Usd.GetVersion() >= (0, 25, 11)
12+
13+
14+
class TestNewtonJointAPI(unittest.TestCase):
15+
def setUp(self):
16+
self.stage: Usd.Stage = Usd.Stage.CreateInMemory()
17+
self.revolute: Usd.Prim = UsdPhysics.RevoluteJoint.Define(self.stage, "/Revolute").GetPrim()
18+
19+
def test_api_registered(self):
20+
plug_type = Plug.Registry().FindTypeByName("NewtonPhysicsJointAPI")
21+
self.assertEqual(plug_type.typeName, "NewtonPhysicsJointAPI")
22+
schema_type = Usd.SchemaRegistry().GetSchemaTypeName("NewtonPhysicsJointAPI")
23+
self.assertEqual(schema_type, "NewtonJointAPI")
24+
25+
def test_api_application(self):
26+
self.assertFalse(self.revolute.HasAPI("NewtonJointAPI"))
27+
self.revolute.ApplyAPI("NewtonJointAPI")
28+
self.assertTrue(self.revolute.HasAPI("NewtonJointAPI"))
29+
30+
self.assertTrue(self.revolute.HasAttribute("newton:armature"))
31+
self.assertTrue(self.revolute.HasAttribute("newton:damping"))
32+
self.assertTrue(self.revolute.HasAttribute("newton:friction"))
33+
self.assertTrue(self.revolute.HasAttribute("newton:velocityLimit"))
34+
self.assertTrue(self.revolute.HasAttribute("newton:limitStiffness"))
35+
self.assertTrue(self.revolute.HasAttribute("newton:limitDamping"))
36+
37+
def test_api_application_prismatic(self):
38+
prismatic = UsdPhysics.PrismaticJoint.Define(self.stage, "/Prismatic").GetPrim()
39+
self.assertTrue(prismatic.CanApplyAPI("NewtonJointAPI"))
40+
prismatic.ApplyAPI("NewtonJointAPI")
41+
self.assertTrue(prismatic.HasAPI("NewtonJointAPI"))
42+
43+
def test_api_application_spherical(self):
44+
spherical = UsdPhysics.SphericalJoint.Define(self.stage, "/Spherical").GetPrim()
45+
self.assertTrue(spherical.CanApplyAPI("NewtonJointAPI"))
46+
spherical.ApplyAPI("NewtonJointAPI")
47+
self.assertTrue(spherical.HasAPI("NewtonJointAPI"))
48+
49+
def test_api_application_d6(self):
50+
d6 = UsdPhysics.Joint.Define(self.stage, "/D6").GetPrim()
51+
self.assertTrue(d6.CanApplyAPI("NewtonJointAPI"))
52+
d6.ApplyAPI("NewtonJointAPI")
53+
self.assertTrue(d6.HasAPI("NewtonJointAPI"))
54+
55+
def test_api_limitations(self):
56+
xform: Usd.Prim = self.stage.DefinePrim("/NotJoint", "Xform")
57+
self.assertFalse(xform.CanApplyAPI("NewtonJointAPI"))
58+
59+
def test_armature(self):
60+
self.revolute.ApplyAPI("NewtonJointAPI")
61+
attr = self.revolute.GetAttribute("newton:armature")
62+
self.assertIsNotNone(attr)
63+
self.assertFalse(attr.HasAuthoredValue())
64+
self.assertAlmostEqual(attr.Get(), 0.0)
65+
66+
success = attr.Set(0.01)
67+
self.assertTrue(success)
68+
self.assertTrue(attr.HasAuthoredValue())
69+
self.assertAlmostEqual(attr.Get(), 0.01)
70+
71+
if USD_HAS_LIMITS:
72+
hard = attr.GetHardLimits()
73+
self.assertTrue(hard.IsValid())
74+
self.assertAlmostEqual(hard.GetMinimum(), 0.0)
75+
self.assertIsNone(hard.GetMaximum())
76+
77+
def test_damping(self):
78+
self.revolute.ApplyAPI("NewtonJointAPI")
79+
attr = self.revolute.GetAttribute("newton:damping")
80+
self.assertIsNotNone(attr)
81+
self.assertFalse(attr.HasAuthoredValue())
82+
self.assertAlmostEqual(attr.Get(), 0.0)
83+
84+
success = attr.Set(5.0)
85+
self.assertTrue(success)
86+
self.assertTrue(attr.HasAuthoredValue())
87+
self.assertAlmostEqual(attr.Get(), 5.0)
88+
89+
if USD_HAS_LIMITS:
90+
hard = attr.GetHardLimits()
91+
self.assertTrue(hard.IsValid())
92+
self.assertAlmostEqual(hard.GetMinimum(), 0.0)
93+
self.assertIsNone(hard.GetMaximum())
94+
95+
def test_friction(self):
96+
self.revolute.ApplyAPI("NewtonJointAPI")
97+
attr = self.revolute.GetAttribute("newton:friction")
98+
self.assertIsNotNone(attr)
99+
self.assertFalse(attr.HasAuthoredValue())
100+
self.assertAlmostEqual(attr.Get(), 0.0)
101+
102+
success = attr.Set(0.5)
103+
self.assertTrue(success)
104+
self.assertTrue(attr.HasAuthoredValue())
105+
self.assertAlmostEqual(attr.Get(), 0.5)
106+
107+
if USD_HAS_LIMITS:
108+
hard = attr.GetHardLimits()
109+
self.assertTrue(hard.IsValid())
110+
self.assertAlmostEqual(hard.GetMinimum(), 0.0)
111+
self.assertIsNone(hard.GetMaximum())
112+
113+
def test_velocity_limit(self):
114+
self.revolute.ApplyAPI("NewtonJointAPI")
115+
attr = self.revolute.GetAttribute("newton:velocityLimit")
116+
self.assertIsNotNone(attr)
117+
self.assertFalse(attr.HasAuthoredValue())
118+
self.assertEqual(attr.Get(), math.inf)
119+
120+
success = attr.Set(360.0)
121+
self.assertTrue(success)
122+
self.assertTrue(attr.HasAuthoredValue())
123+
self.assertAlmostEqual(attr.Get(), 360.0)
124+
125+
if USD_HAS_LIMITS:
126+
hard = attr.GetHardLimits()
127+
self.assertTrue(hard.IsValid())
128+
self.assertAlmostEqual(hard.GetMinimum(), 0.0)
129+
self.assertIsNone(hard.GetMaximum())
130+
131+
def test_limit_stiffness(self):
132+
self.revolute.ApplyAPI("NewtonJointAPI")
133+
attr = self.revolute.GetAttribute("newton:limitStiffness")
134+
self.assertIsNotNone(attr)
135+
self.assertFalse(attr.HasAuthoredValue())
136+
self.assertEqual(attr.Get(), -math.inf)
137+
138+
success = attr.Set(174.5)
139+
self.assertTrue(success)
140+
self.assertTrue(attr.HasAuthoredValue())
141+
self.assertAlmostEqual(attr.Get(), 174.5)
142+
143+
def test_limit_damping(self):
144+
self.revolute.ApplyAPI("NewtonJointAPI")
145+
attr = self.revolute.GetAttribute("newton:limitDamping")
146+
self.assertIsNotNone(attr)
147+
self.assertFalse(attr.HasAuthoredValue())
148+
self.assertEqual(attr.Get(), -math.inf)
149+
150+
success = attr.Set(0.1745)
151+
self.assertTrue(success)
152+
self.assertTrue(attr.HasAuthoredValue())
153+
self.assertAlmostEqual(attr.Get(), 0.1745)
154+
155+
156+
if __name__ == "__main__":
157+
unittest.main()

0 commit comments

Comments
 (0)