mjcPhysics MjcTendon path now has an indices attribute to allow repeated sites in tendon path.

Since relationships in USD can only contain a single instance of a path, it was previously impossible to have a tendon path that visits the same wrap target more than once. This is a fairly common occurence when dealing with tendons that have pulleys.

With this change, if mjc:path:indices is not defined then the behaviour is as it was and the mjc:path is considered to be ordered. If it is authored then the parser will use the indices into mjc:path as the order. This mirrors other similar concepts in USD such as face vertex indices.

PiperOrigin-RevId: 856339887
Change-Id: I6b9503973de377acdb0689be168c16e4aa19b70b
This commit is contained in:
Sam Haves
2026-01-14 13:23:41 -08:00
committed by Copybara-Service
parent 42c565d860
commit d816b38f4c
7 changed files with 69 additions and 12 deletions
+15 -8
View File
@@ -954,6 +954,12 @@ void ParseMjcPhysicsTendon(mjSpec* spec, const pxr::MjcPhysicsTendon& tendon) {
pxr::SdfPathVector wrap_targets;
tendon.GetMjcPathRel().GetTargets(&wrap_targets);
pxr::VtIntArray wrap_path_indices;
tendon.GetMjcPathIndicesAttr().Get(&wrap_path_indices);
auto n_targets = wrap_path_indices.empty() ? wrap_targets.size()
: wrap_path_indices.size();
pxr::SdfPathVector side_site_paths;
tendon.GetMjcSideSitesRel().GetTargets(&side_site_paths);
@@ -971,11 +977,11 @@ void ParseMjcPhysicsTendon(mjSpec* spec, const pxr::MjcPhysicsTendon& tendon) {
if (type == MjcPhysicsTokens->spatial) {
// Check that for N targets we have 0 or N elements in segments.
if (!segments.empty() && segments.size() != wrap_targets.size()) {
if (!segments.empty() && segments.size() != n_targets) {
mju_warning(
"Spatial tendon %s has %lu segments but %lu wrap targets, skipping.",
prim.GetPath().GetAsString().c_str(), segments.size(),
wrap_targets.size());
n_targets);
return;
}
// Check that if we have >1 segments that the user has specified how much
@@ -990,12 +996,12 @@ void ParseMjcPhysicsTendon(mjSpec* spec, const pxr::MjcPhysicsTendon& tendon) {
}
// Check that if we side site indices that we have N of them.
if (!side_site_indices.empty() &&
side_site_indices.size() != wrap_targets.size()) {
side_site_indices.size() != n_targets) {
mju_warning(
"Spatial tendon %s has %lu sideSite indices but %lu wrap targets, "
"skipping.",
prim.GetPath().GetAsString().c_str(), side_site_indices.size(),
wrap_targets.size());
n_targets);
return;
}
@@ -1008,17 +1014,18 @@ void ParseMjcPhysicsTendon(mjSpec* spec, const pxr::MjcPhysicsTendon& tendon) {
}
} else { // Fixed tendon.
// Check that for N targets we have 0 or N elements in coef:
if (!coefs.empty() && coefs.size() != wrap_targets.size()) {
if (!coefs.empty() && coefs.size() != n_targets) {
mju_warning(
"Spatial tendon %s has %lu coefs but %lu wrap targets, skipping.",
prim.GetPath().GetAsString().c_str(), coefs.size(),
wrap_targets.size());
n_targets);
}
}
int last_segment = 0;
for (int i = 0; i < wrap_targets.size(); ++i) {
auto wrap_target = wrap_targets[i];
for (int i = 0; i < n_targets; ++i) {
auto index = wrap_path_indices.empty() ? i : wrap_path_indices[i];
auto wrap_target = wrap_targets[index];
auto wrap_prim = stage->GetPrimAtPath(wrap_target);
// Important to check site before Imageable here because some Imageable
// prims are sites.