Skip to content

Commit 825f0c9

Browse files
jieseattleclaude
andauthored
Warn on mirrored (negative-determinant) rigid body transforms in USD import (#3654)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 70765c1 commit 825f0c9

3 files changed

Lines changed: 58 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
- Add opt-in DVI forward dynamics to `SolverKamino` through `SolverKamino.Config(dynamics_solver="dvi")`, with sparse and dense execution, DVI-specific diagnostics, and warm-starting. PADMM remains the default.
2929
- Add opt-in DVI forward dynamics to `SolverKamino` through `SolverKamino.Config(dynamics_solver="dvi")`, with sparse and dense execution, DVI-specific convergence diagnostics, warm-starting, bounded contact-recovery controls, and RCM-reordered bilateral factorization with reusable ordering and panel-parallel numeric factorization for large systems. PADMM remains the default.
3030
- Add SDF contact support for convex-hull shapes with mesh-attached SDFs and opt-in SDF contact generation for box shapes.
31+
- Warn in `ModelBuilder.add_usd()` when a rigid body prim has a mirrored (negative-determinant) world transform. Improper transforms have no unique rotation decomposition, so imported body and joint frames can acquire a spurious constant rotation (common with mirror-scaled CAD exports); the warning recommends baking the reflection into the mesh geometry before import.
3132
- Add opt-in filtering of static-static, static-kinematic, and kinematic-kinematic contacts during broad-phase collision detection. Set `CollisionPipeline(include_static_kinematic_pairs=False)` to enable filtering; the default preserves existing contact generation. `Model.shape_contact_pairs` remains an unfiltered superset for direct consumers such as `SolverKamino` and hydroelastic SDF setup.
3233
- Add opt-in `body_frame_origin="com"` to `ModelBuilder.add_rod()` and `ModelBuilder.add_rod_graph()` for COM-centered cable capsule body frames.
3334
- Add `sign_method` argument to `Mesh.build_sdf` and `SDF.create_from_mesh` support for a `"normal"` (angle-weighted pseudo-normal) sign strategy, for selecting the inside/outside sign of the baked SDF (`"auto"`, `"parity"`, `"winding"`, or `"normal"`).

newton/_src/utils/import_usd.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,31 @@ def _cache_path_for_absolute_usd_reference(url: str) -> str:
154154
return posixpath.join("_external_usd", digest, basename)
155155

156156

157+
def _warn_mirrored_body_transform(usd_prim, key: str, xform_cache) -> None:
158+
"""Warn when a rigid body prim has an improper (mirrored) world transform.
159+
160+
Improper transforms (negative determinant) have no unique rotation
161+
decomposition: the USD physics parser's ``rotation`` and
162+
``usd.get_transform()`` may absorb the reflection on different axes, and
163+
their disagreement becomes a spurious constant rotation injected into the
164+
imported body and joint frames via the incoming-xform rebase.
165+
166+
Args:
167+
usd_prim: The rigid body ``Usd.Prim``.
168+
key: Prim path string used in the warning message.
169+
xform_cache: ``UsdGeom.XformCache`` for world transform lookup.
170+
"""
171+
if xform_cache.GetLocalToWorldTransform(usd_prim).GetDeterminant() < 0.0:
172+
warnings.warn(
173+
f"Rigid body prim {key} has a mirrored (negative-determinant) "
174+
"world transform. Imported body and joint frames may acquire a "
175+
"spurious rotation. Bake the reflection into the mesh geometry "
176+
"(negate vertices, flip triangle winding) and re-author the body "
177+
"with a proper transform before import.",
178+
stacklevel=_external_stacklevel(),
179+
)
180+
181+
157182
def _external_stacklevel() -> int:
158183
"""Return a ``stacklevel`` that points past all ``newton._src`` frames."""
159184
frame = inspect.currentframe()
@@ -1519,6 +1544,7 @@ def parse_body(
15191544
if incoming_xform is not None:
15201545
origin = wp.mul(incoming_xform, origin)
15211546
path = str(prim.GetPath())
1547+
_warn_mirrored_body_transform(prim, path, xform_cache)
15221548

15231549
is_kinematic = rigid_body_desc.kinematicBody
15241550
linear_velocity = wp.transform_vector(origin, wp.vec3(*rigid_body_desc.linearVelocity))

newton/tests/test_import_usd.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,37 @@ def test_import_articulation(self):
123123
]
124124
self.assertEqual(len(collision_shapes), 13)
125125

126+
@unittest.skipUnless(USD_AVAILABLE, "Requires usd-core")
127+
def test_mirrored_body_transform_warns(self):
128+
"""A rigid body with a negative-determinant (mirrored) transform warns.
129+
130+
Improper transforms have no unique rotation decomposition, so the
131+
incoming-xform rebase can inject a spurious constant rotation into
132+
body and joint frames (common with mirror-scaled CAD exports).
133+
"""
134+
from pxr import Gf, Usd, UsdGeom, UsdPhysics
135+
136+
stage = Usd.Stage.CreateInMemory()
137+
UsdGeom.SetStageUpAxis(stage, UsdGeom.Tokens.z)
138+
UsdPhysics.Scene.Define(stage, "/physicsScene")
139+
140+
body = UsdGeom.Xform.Define(stage, "/World/Body")
141+
body.AddScaleOp().Set(Gf.Vec3f(-1.0, -1.0, -1.0))
142+
UsdPhysics.RigidBodyAPI.Apply(body.GetPrim())
143+
UsdPhysics.ArticulationRootAPI.Apply(body.GetPrim())
144+
mass = UsdPhysics.MassAPI.Apply(body.GetPrim())
145+
mass.GetMassAttr().Set(1.0)
146+
mass.GetCenterOfMassAttr().Set(Gf.Vec3f(0.0, 0.0, 0.0))
147+
mass.GetDiagonalInertiaAttr().Set(Gf.Vec3f(1.0, 1.0, 1.0))
148+
149+
joint = UsdPhysics.RevoluteJoint.Define(stage, "/World/Joint")
150+
joint.CreateBody1Rel().SetTargets([body.GetPath()])
151+
joint.CreateAxisAttr().Set("Z")
152+
153+
builder = newton.ModelBuilder()
154+
with self.assertWarnsRegex(UserWarning, "mirrored"):
155+
builder.add_usd(stage, load_visual_shapes=False)
156+
126157
@unittest.skipUnless(USD_AVAILABLE, "Requires usd-core")
127158
def test_import_body_newton_armature_ignored(self):
128159
# Body-level newton:armature was removed: an authored value must be

0 commit comments

Comments
 (0)