Skip to content

usdPhysics: add public getters for joint/collider body resolution and refactor the joint parser onto them - #4165

Open
andrewkaufman wants to merge 13 commits into
PixarAnimationStudios:devfrom
andrewkaufman:add-collision-prims-api
Open

andrewkaufman wants to merge 13 commits into
PixarAnimationStudios:devfrom
andrewkaufman:add-collision-prims-api

Conversation

@andrewkaufman

@andrewkaufman andrewkaufman commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Description of Change(s)

Adds a set of public getters that expose the body-resolution and mass-collider traversals that previously lived only inside usdPhysics internals, and refactors the joint parser to use them so the resolution logic has a single implementation.

New API:

  • UsdPhysicsRigidBodyAPI::GetCollisionPrims() returns the collider prims belonging to a rigid body, the same traversal ComputeMassProperties() uses internally to decide which colliders' mass is accumulated into the body. Starting at the rigid body's prim the subtree is traversed (including instance proxies) and every prim with UsdPhysicsCollisionAPI applied is collected, with two exceptions: a collider whose physics:collisionEnabled resolves to false is skipped, and the subtree rooted at a nested prim with UsdPhysicsRigidBodyAPI is pruned (those colliders belong to that other body). ComputeMassProperties() is refactored to call it.
  • UsdPhysicsCollisionAPI::GetBody() resolves a collider to its owning enabled rigid body via the ancestor walk, returning an invalid prim when there is none.
  • UsdPhysicsJoint::GetBody0() / GetBody1() return the enabled rigid body each side of the joint attaches to, or an invalid prim.
  • UsdPhysicsJoint::GetLocalPose0() / GetLocalPose1() return the joint-local anchor pose in that body's frame, with the frame scale baked into the translation (physics is scale-free) and an optional UsdGeomXformCache for nested hierarchies.

Refactor: the joint parser's private _GetBodyPrim / _GetLocalPose are deleted and _FinalizeJoint now calls the public getters, so joint body resolution has a single implementation shared between the parser and clients.

Behavior change: a joint side with no enclosing enabled body now gives an empty body0/body1, where the previous parser fell back to the nearest collider. This is called out for review in the PR discussion.

Each getter has Python wrapping and regression tests.

Motivation: the traversals that map a rigid body to its colliders, and a joint or collider to its owning body, previously existed only inside ComputeMassProperties and the joint parser. Clients that need this had to re-implement it (instance-proxy traversal and enabled-body resolution are easy to get subtly wrong). Exposing the canonical logic lets clients reuse it and lets the parser drop its private copy.

Link to proposal

N/A, additive public API, no schema change.

Dependency

Based on top of #4187 (treat disabled rigid bodies as unapplied in all ancestor traversals); these getters share that enabled-body filter. Restacked from #4164, which #4187 supersedes.

AI Assistance

Developed with AI assistance (Anthropic Claude, primarily Opus 5): the code, tests, and this description were AI-drafted and then reviewed by me before submission.

Checklist

  • I have created this PR based on the dev branch
  • I have followed the coding conventions
  • I have added unit tests that exercise this functionality
  • I have verified that all unit tests pass with the proposed changes
  • I have submitted a signed Contributor License Agreement

@jesschimein

Copy link
Copy Markdown
Collaborator

Filed as internal issue #USD-12474

(This is an automated message. See here for more information.)

@andrewkaufman
andrewkaufman force-pushed the add-collision-prims-api branch 2 times, most recently from fbce7bc to 5969977 Compare July 27, 2026 17:59
@dsyu-pixar dsyu-pixar moved this to On Deck in Public Backlog Aug 16, 2026
UsdPhysicsRigidBodyAPI::ComputeMassProperties gathered every descendant
prim with PhysicsCollisionAPI applied, regardless of whether the collider
was enabled. A collider with physics:collisionEnabled = false was still
contributing its mass, center of mass, and inertia to the body.

physics:collisionEnabled determines whether the PhysicsCollisionAPI is
enabled; a collider whose collision is disabled takes no part in
simulation and so should not contribute to a body's aggregated mass
properties. Skip colliders whose resolved physics:collisionEnabled is
false when gathering shapes for the mass computer.

Also clarifies in the PhysicsMassAPI documentation and the usdPhysics
overview that a disabled collider or disabled rigid body takes no part in
simulation, so it is excluded from mass computation and any MassAPI
applied to it has no effect.

Adds a regression test: a rigid body with one enabled unit cube and one
disabled cube offset along z now reports the mass, center of mass, and
inertia of the single enabled cube only.
A nested UsdPhysicsRigidBodyAPI forms the root of its own subtree only while
it is enabled. ComputeMassProperties pruned the subtree of every nested prim
with RigidBodyAPI applied without consulting physics:rigidBodyEnabled, so the
enabled colliders beneath a disabled nested body were excluded from that body's
mass, while no mass was computed for the disabled body itself. Their mass was
therefore attributed to no body at all and silently lost.

Prune only at enabled nested bodies, so those colliders continue to belong to
the nearest enabled rigid body above them. Documentation is updated to state
that disabling a prim ignores MassAPI on that prim without discarding the mass
of the enabled colliders beneath it.
The physics parsing utilities stopped walking the ancestors at the first prim
with RigidBodyAPI applied, whether or not it was enabled, and reported that
prim as the collider's body. A collider below a disabled nested body was
therefore assigned to a body that takes no part in simulation, even when an
enabled body existed further up the hierarchy, and disagreed with the subtree
that ComputeMassProperties aggregates.

Continue searching the ancestors past a disabled body so the collider is
assigned to the nearest enabled rigid body. When no enabled body exists above
it, the nearest disabled body is still reported, as before.
A disabled rigid body takes no part in simulation and is equivalent to the API
not being applied at all, so every ancestor traversal must look past it.

Two traversals still stopped at the nearest body regardless of whether it was
enabled, disagreeing with the collider attribution the preceding commits
established:

- _GetBodyPrim, which resolves a joint body relationship, returned a disabled
  body and never consulted physics:rigidBodyEnabled at all.
- PhysicsJointChecker's HasDynamicBodyParent reported a joint below a disabled
  body as having no enabled rigid body, even when an enabled body sat above it.

Both now continue past a disabled body to the nearest enabled one, so joint
relationship resolution, collider attribution, and validation agree on which
body owns a given prim.
@andrewkaufman
andrewkaufman force-pushed the add-collision-prims-api branch from 5969977 to 7faa4fb Compare August 18, 2026 21:49
@andrewkaufman

Copy link
Copy Markdown
Contributor Author

Restacked onto #4187, which supersedes #4164. Only the GetCollisionPrims commit is mine on top; the rest are #4187 unchanged.

@AlesBorovicka on your question about how far to take this: you are right that it invites symmetric accessors, a get rigid body on a collider and get body 0/1 on a joint. #4187 is relevant to that now, because it makes all of those the same traversal. It resolves which body owns a prim consistently across mass computation, collider attribution, joint relationship resolution and validation, so symmetric accessors would be thin wrappers over one shared walk rather than four separate ones. We would like to discuss the scope before adding them, so this PR stays limited to the collider getter.

andrewkaufman and others added 3 commits September 4, 2026 09:07
Co-authored-by: nvmkuruc <122411138+nvmkuruc@users.noreply.github.com>
…d case

The suggestions applied in the previous commit inverted the polarity at both
the recording and the reporting site of the nearest disabled body, in
_HasDynamicBodyParent and in the validators' HasDynamicBodyParent:

- The recording site only assigned disabledBodyPrim when it was already
  non-null, so it was never assigned at all.
- The reporting site wrote to *outBodyPrimPath only when disabledBodyPrim was
  null, clobbering the out parameter with a null prim.

Restore the intended conditions. A collider below a disabled body with no
enabled body above it is once again reported as belonging to that disabled
body, rather than degrading to a static collision.

Also apply the UsdPrim default construction cleanup to parseUtils.cpp, which
only landed in validators.cpp.

No existing test covered a disabled body without an enabled body above it, so
the whole physics and validation suite passed with the polarity inverted. Add
that case to testUsdPhysicsParsing; it fails without this change.
- overview.dox: rephrase the nested-body exception in terms of the prim's
  physics:rigidBodyEnabled state, and reword the implicit-mass note to apply
  to any rigid body (not only nested ones), naming physics:rigidBodyEnabled.
- validators.cpp: default isAPISchemaEnabled to true to match the schema's
  rigidBodyEnabled fallback.
- testUsdPhysicsValidation.py: use assertEqual(len(errors), N) instead of
  assertTrue(len(errors) == N) for better failure diagnostics.
@tallytalwar

Copy link
Copy Markdown
Contributor

@andrewkaufman could you rebase this on top of #4187 to remove commits which are already in 4187? Thanks

Or if its just 7faa4fb which needs to be included, I can extract that out internaly, lmk. Thanks

@andrewkaufman
andrewkaufman force-pushed the add-collision-prims-api branch from 7faa4fb to 8fdd386 Compare September 16, 2026 06:53
@andrewkaufman

Copy link
Copy Markdown
Contributor Author

@tallytalwar I've rebased on #4187 to incorporate the code review changes there. The commits still show here though... I can't seem to stack the PRs for some reason, perhaps because the source branch lives on my fork, not quite sure.... Its just the last commit that is unique here.

I have one addition planned before this is ready: alongside GetCollisionPrims I'll add GetRigidBody() on colliders and GetBody0()/GetBody1() on joints. These expose the same non-obvious resolution the parser handles (the rel can point at a collider and the parser walks the ancestor hierarchy to the nearest enabled body). I've seen multiple client side mistakes caused by this non-obvious traversal behaviour, so I think its worth exposing the helpers utils.

A collider below a disabled rigid body with no enabled body above it was
reported as belonging to that disabled body. The schema says a disabled
rigid body takes no part in simulation and owns no colliders: such a
collider is a static collision, and its body path is empty.

_HasDynamicBodyParent no longer records a disabled body as a fallback
owner. It continues walking past disabled bodies to find an enabled body,
and returns no body when none is found. _GetRigidBody then yields an empty
path so the collider is treated as static.

Behavior change: a collider under a disabled body with no enabled ancestor
now resolves to an empty rigid body path instead of the disabled body's
path. In the common case with a default simulation owner the collider is
still parsed, now as a static collision; with an explicit non-default
simulation owner filter it is excluded, matching static collision handling.

The nested case is unchanged: a collider below a disabled body that has an
enabled body above it still belongs to the enabled body.
Exposes the traversal that ComputeMassProperties uses to gather the
colliders belonging to a rigid body as a public method. Starting at the
body prim, the subtree is traversed (including instance proxies) and
every enabled UsdPhysicsCollisionAPI prim is collected; subtrees rooted
at nested rigid bodies are pruned since those colliders belong to a
different body, and colliders whose physics:collisionEnabled is false are
skipped.

ComputeMassProperties is refactored to call GetCollisionPrims so the
body-ownership and enabled-collider semantics live in one place, and
clients (e.g. physics engines consuming USD) can reproduce them without
re-implementing the traversal.

Adds a Python wrapping returning the list of collider prims and a
regression test covering the enabled/disabled/nested-body cases.
@andrewkaufman
andrewkaufman force-pushed the add-collision-prims-api branch from 89347b3 to 8b5b5e9 Compare September 17, 2026 02:00
Replace the parser's private _GetBodyPrim / _GetLocalPose helpers with
calls to UsdPhysicsJoint::GetBody0/1 and GetLocalPose0/1, so the body
resolution and local-pose computation have a single implementation shared
by the parser and the public API.

Two intended behavior changes fall out of adopting the public semantics,
both for malformed or body-less joint relationships:

- A joint side whose relationship resolves to no enclosing enabled body
  now reports an empty body path instead of falling back to the nearest
  collider. A collider is not a body and was never a correct answer here.

- A joint side whose relationship target does not resolve to a prim on the
  stage (a dangling relationship) now contributes no anchor pose (identity)
  instead of silently passing the authored local pose through. Dangling
  relationships are malformed authoring and are no longer honored.
@andrewkaufman
andrewkaufman force-pushed the add-collision-prims-api branch from 8b5b5e9 to e63f00c Compare September 17, 2026 02:19
@andrewkaufman

Copy link
Copy Markdown
Contributor Author

@AlesBorovicka @tallytalwar, pushed the new public getters and refactored the joint parser onto them.

New API:

  • UsdPhysicsCollisionAPI::GetBody() resolves a collider to its owning enabled rigid body via the ancestor walk, returning an invalid prim when there is none.
  • UsdPhysicsJoint::GetBody0() / GetBody1() return the enabled rigid body each side of the joint attaches to, or an invalid prim.
  • UsdPhysicsJoint::GetLocalPose0() / GetLocalPose1() return the joint-local anchor pose in that body's frame, with the frame scale baked into the translation (physics is scale-free) and an optional UsdGeomXformCache for nested hierarchies.

The joint parser's private _GetBodyPrim / _GetLocalPose are deleted. _FinalizeJoint now calls the public getters, so joint body resolution has a single implementation.

One behavior change worth a check: a joint side with no enclosing enabled body now gives an empty body0/body1, where the previous parser fell back to the nearest collider. A collider is not a body, so anchoring a joint to one looks wrong to me, but I would rather confirm than assume there was a reason for it.

@AlesBorovicka, does this match your expectation for the body-resolution semantics, and do you agree the joint parser is more correct now?

PS. CollisionAPI::GetBody() and the parser's internal collider resolution now use the same enabled-body walk, but I deliberately left the parser's path in place rather than routing it through GetBody(). The parser is scope aware (only bodies within the parsed range) while GetBody() is a plain stage-ancestry walk, so they can differ at a parse-scope boundary. test_collider_get_body_matches_parser asserts they agree in the simple and disabled-body cases.

@andrewkaufman andrewkaufman changed the title usdPhysics: add UsdPhysicsRigidBodyAPI::GetCollisionPrims usdPhysics: add public getters for joint/collider body resolution and refactor the joint parser onto them Sep 17, 2026
@AlesBorovicka

Copy link
Copy Markdown
Contributor

One behavior change worth a check: a joint side with no enclosing enabled body now gives an empty body0/body1, where the previous parser fell back to the nearest collider. A collider is not a body, so anchoring a joint to one looks wrong to me, but I would rather confirm than assume there was a reason for it.

@AlesBorovicka, does this match your expectation for the body-resolution semantics, and do you agree the joint parser is more correct now?

Well a standalone collider is a static body, it boils down to the transformation used in the end of the day that you compute from the joint local pose. So its going to be different based on what you return. This will create different joints in PhysX for sure. So changes like this will have some consequences.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: On Deck

Development

Successfully merging this pull request may close these issues.

5 participants