From d816b38f4c5b1b133a8541935cfd6ebcabfc86ae Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 14 Jan 2026 13:23:41 -0800 Subject: [PATCH] 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 --- .../experimental/usd/mjcPhysics/tendon.h | 29 +++++++++++++++++-- .../experimental/usd/mjcPhysics/tokens.h | 4 +++ plugin/usd_decoder/usd_decoder.cc | 23 ++++++++++----- .../usd/mjcPhysics/generatedSchema.usda | 5 +++- src/experimental/usd/mjcPhysics/schema.usda | 6 +++- src/experimental/usd/mjcPhysics/tendon.cpp | 12 ++++++++ src/experimental/usd/mjcPhysics/tokens.cpp | 2 ++ 7 files changed, 69 insertions(+), 12 deletions(-) diff --git a/include/mujoco/experimental/usd/mjcPhysics/tendon.h b/include/mujoco/experimental/usd/mjcPhysics/tendon.h index ce4ed192..9e72ed50 100644 --- a/include/mujoco/experimental/usd/mjcPhysics/tendon.h +++ b/include/mujoco/experimental/usd/mjcPhysics/tendon.h @@ -158,6 +158,31 @@ class MjcPhysicsTendon : public UsdTyped { UsdAttribute CreateTypeAttr(VtValue const& defaultValue = VtValue(), bool writeSparsely = false) const; + public: + // --------------------------------------------------------------------- // + // MJCPATHINDICES + // --------------------------------------------------------------------- // + /// This list represents the order in which the tendon wraps the sites in + /// mjc:path. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int[] mjc:path:indices = []` | + /// | C++ Type | VtArray | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->IntArray | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetMjcPathIndicesAttr() const; + + /// See GetMjcPathIndicesAttr(), and also + /// \ref Usd_Create_Or_Get_Property for when to use Get vs Create. + /// If specified, author \p defaultValue as the attribute's default, + /// sparsely (when it makes sense to do so) if \p writeSparsely is \c true - + /// the default for \p writeSparsely is \c false. + MJCPHYSICS_API + UsdAttribute CreateMjcPathIndicesAttr(VtValue const& defaultValue = VtValue(), + bool writeSparsely = false) const; + public: // --------------------------------------------------------------------- // // MJCSIDESITESINDICES @@ -771,8 +796,8 @@ class MjcPhysicsTendon : public UsdTyped { // --------------------------------------------------------------------- // // MJCPATH // --------------------------------------------------------------------- // - /// For spatial tendons, this describes a list of sites and geoms the tendon - /// wraps. For fixed tendons, this is instead a list of joints. + /// For spatial tendons, this describes a list of unique of sites and geoms + /// the tendon wraps. For fixed tendons, this is instead a list of joints. /// MJCPHYSICS_API UsdRelationship GetMjcPathRel() const; diff --git a/include/mujoco/experimental/usd/mjcPhysics/tokens.h b/include/mujoco/experimental/usd/mjcPhysics/tokens.h index 362b3b75..f1e2d28e 100644 --- a/include/mujoco/experimental/usd/mjcPhysics/tokens.h +++ b/include/mujoco/experimental/usd/mjcPhysics/tokens.h @@ -572,6 +572,10 @@ struct MjcPhysicsTokensType { /// /// MjcPhysicsTendon const TfToken mjcPathDivisors; + /// \brief "mjc:path:indices" + /// + /// MjcPhysicsTendon + const TfToken mjcPathIndices; /// \brief "mjc:path:segments" /// /// MjcPhysicsTendon diff --git a/plugin/usd_decoder/usd_decoder.cc b/plugin/usd_decoder/usd_decoder.cc index 231031fc..59179e99 100644 --- a/plugin/usd_decoder/usd_decoder.cc +++ b/plugin/usd_decoder/usd_decoder.cc @@ -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. diff --git a/src/experimental/usd/mjcPhysics/generatedSchema.usda b/src/experimental/usd/mjcPhysics/generatedSchema.usda index c82f35e9..41ad2ac7 100644 --- a/src/experimental/usd/mjcPhysics/generatedSchema.usda +++ b/src/experimental/usd/mjcPhysics/generatedSchema.usda @@ -593,7 +593,7 @@ class MjcTendon "MjcTendon" ( doc = "The limit constraint becomes active when the absolute value of the difference between the tendon length and either limit of the specified range falls below this margin. Similar to contacts, the margin parameter is subtracted from the difference between the range limit and the tendon length. The resulting constraint distance is always negative when the constraint is active. This quantity is used to compute constraint impedance as a function of distance." ) rel mjc:path ( - doc = "For spatial tendons, this describes a list of sites and geoms the tendon wraps. For fixed tendons, this is instead a list of joints." + doc = "For spatial tendons, this describes a list of unique of sites and geoms the tendon wraps. For fixed tendons, this is instead a list of joints." ) uniform double[] mjc:path:coef = [] ( doc = "For fixed tendons passing through joints this represents a multiplicative factor on the position or angle of the targeted joint." @@ -601,6 +601,9 @@ class MjcTendon "MjcTendon" ( uniform double[] mjc:path:divisors = [] ( doc = "For spatial tendons, this represents an indexed array of divisors. A tendon path segments' length contribution to the overall tendon length is divided by its divisor." ) + uniform int[] mjc:path:indices = [] ( + doc = "This list represents the order in which the tendon wraps the sites in mjc:path." + ) uniform int[] mjc:path:segments = [] ( doc = "For spatial tendons, this holds the index of the segment each tendon path wrap point belongs to." ) diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda index b52d6c39..1e0904dc 100644 --- a/src/experimental/usd/mjcPhysics/schema.usda +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -1077,13 +1077,17 @@ class MjcTendon "MjcTendon" ) rel mjc:path ( - doc = "For spatial tendons, this describes a list of sites and geoms the tendon wraps. For fixed tendons, this is instead a list of joints." + doc = "For spatial tendons, this describes a list of unique of sites and geoms the tendon wraps. For fixed tendons, this is instead a list of joints." ) rel mjc:sideSites ( doc = "For spatial tendons, a geom wrapped by the tendon may specify which side of the geom the tendon wraps around via a site prim. This is a list of sites that are used as side sites in mjc:path." ) + uniform int[] mjc:path:indices = [] ( + doc = "This list represents the order in which the tendon wraps the sites in mjc:path." + ) + uniform int[] mjc:sideSites:indices = [] ( doc = "For spatial tendons, if mjc:sideSites has targets then index 'i' in this list represents the position in the relationship targets of mjc:sideSites that the geom at index 'i' in mjc:path uses as a side site. It is considered an authoring error to assign a side site to something other than a geom. Geoms that do not use a side site should use index value '-1'." ) diff --git a/src/experimental/usd/mjcPhysics/tendon.cpp b/src/experimental/usd/mjcPhysics/tendon.cpp index d88951f5..6f0b2e21 100644 --- a/src/experimental/usd/mjcPhysics/tendon.cpp +++ b/src/experimental/usd/mjcPhysics/tendon.cpp @@ -90,6 +90,17 @@ UsdAttribute MjcPhysicsTendon::CreateTypeAttr(VtValue const& defaultValue, /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); } +UsdAttribute MjcPhysicsTendon::GetMjcPathIndicesAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcPathIndices); +} + +UsdAttribute MjcPhysicsTendon::CreateMjcPathIndicesAttr( + VtValue const& defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcPathIndices, SdfValueTypeNames->IntArray, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + UsdAttribute MjcPhysicsTendon::GetMjcSideSitesIndicesAttr() const { return GetPrim().GetAttribute(MjcPhysicsTokens->mjcSideSitesIndices); } @@ -377,6 +388,7 @@ const TfTokenVector& MjcPhysicsTendon::GetSchemaAttributeNames( bool includeInherited) { static TfTokenVector localNames = { MjcPhysicsTokens->mjcType, + MjcPhysicsTokens->mjcPathIndices, MjcPhysicsTokens->mjcSideSitesIndices, MjcPhysicsTokens->mjcPathSegments, MjcPhysicsTokens->mjcPathDivisors, diff --git a/src/experimental/usd/mjcPhysics/tokens.cpp b/src/experimental/usd/mjcPhysics/tokens.cpp index 806766ed..afb4afc1 100644 --- a/src/experimental/usd/mjcPhysics/tokens.cpp +++ b/src/experimental/usd/mjcPhysics/tokens.cpp @@ -148,6 +148,7 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcPath("mjc:path", TfToken::Immortal), mjcPathCoef("mjc:path:coef", TfToken::Immortal), mjcPathDivisors("mjc:path:divisors", TfToken::Immortal), + mjcPathIndices("mjc:path:indices", TfToken::Immortal), mjcPathSegments("mjc:path:segments", TfToken::Immortal), mjcPriority("mjc:priority", TfToken::Immortal), mjcQpos("mjc:qpos", TfToken::Immortal), @@ -323,6 +324,7 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() mjcPath, mjcPathCoef, mjcPathDivisors, + mjcPathIndices, mjcPathSegments, mjcPriority, mjcQpos,