From 541c56b46fb3e8125ee5d980bec7a067babe40e3 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Wed, 23 Jul 2025 11:47:10 -0700 Subject: [PATCH] Create mjcPhysics ImageableAPI for visual, non physical visual entities. PiperOrigin-RevId: 786366034 Change-Id: Ifb72d2daf8f5963bb7007dda1685da94b456ff41 --- .../usd/mjcPhysics/imageableAPI.h | 184 ++++++++++++++++++ .../experimental/usd/mjcPhysics/tokens.h | 8 +- src/experimental/usd/CMakeLists.txt | 1 + .../usd/mjcPhysics/generatedSchema.usda | 10 + .../usd/mjcPhysics/imageableAPI.cpp | 124 ++++++++++++ src/experimental/usd/mjcPhysics/plugInfo.json | 10 + src/experimental/usd/mjcPhysics/schema.usda | 19 ++ src/experimental/usd/mjcPhysics/tokens.cpp | 2 + .../usd/plugins/mjcf/mujoco_to_usd.cc | 16 +- src/experimental/usd/usd_to_mjspec.cc | 22 ++- .../usd/plugins/mjcf/mjcf_file_format_test.cc | 27 +++ 11 files changed, 414 insertions(+), 9 deletions(-) create mode 100644 include/mujoco/experimental/usd/mjcPhysics/imageableAPI.h create mode 100644 src/experimental/usd/mjcPhysics/imageableAPI.cpp diff --git a/include/mujoco/experimental/usd/mjcPhysics/imageableAPI.h b/include/mujoco/experimental/usd/mjcPhysics/imageableAPI.h new file mode 100644 index 00000000..4dbd9a24 --- /dev/null +++ b/include/mujoco/experimental/usd/mjcPhysics/imageableAPI.h @@ -0,0 +1,184 @@ +// Copyright 2025 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef MJCPHYSICS_GENERATED_IMAGEABLEAPI_H +#define MJCPHYSICS_GENERATED_IMAGEABLEAPI_H + +/// \file mjcPhysics/imageableAPI.h + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +class SdfAssetPath; + +// -------------------------------------------------------------------------- // +// MJCIMAGEABLEAPI // +// -------------------------------------------------------------------------- // + +/// \class MjcPhysicsImageableAPI +/// +/// API describing attributes for visual entities in MuJoCo. +/// +class MjcPhysicsImageableAPI : public UsdAPISchemaBase { + public: + /// Compile time constant representing what kind of schema this class is. + /// + /// \sa UsdSchemaKind + static const UsdSchemaKind schemaKind = UsdSchemaKind::SingleApplyAPI; + + /// Construct a MjcPhysicsImageableAPI on UsdPrim \p prim . + /// Equivalent to MjcPhysicsImageableAPI::Get(prim.GetStage(), prim.GetPath()) + /// for a \em valid \p prim, but will not immediately throw an error for + /// an invalid \p prim + explicit MjcPhysicsImageableAPI(const UsdPrim &prim = UsdPrim()) + : UsdAPISchemaBase(prim) {} + + /// Construct a MjcPhysicsImageableAPI on the prim held by \p schemaObj . + /// Should be preferred over MjcPhysicsImageableAPI(schemaObj.GetPrim()), + /// as it preserves SchemaBase state. + explicit MjcPhysicsImageableAPI(const UsdSchemaBase &schemaObj) + : UsdAPISchemaBase(schemaObj) {} + + /// Destructor. + MJCPHYSICS_API + virtual ~MjcPhysicsImageableAPI(); + + /// Return a vector of names of all pre-declared attributes for this schema + /// class and all its ancestor classes. Does not include attributes that + /// may be authored by custom/extended methods of the schemas involved. + MJCPHYSICS_API + static const TfTokenVector &GetSchemaAttributeNames( + bool includeInherited = true); + + /// Return a MjcPhysicsImageableAPI holding the prim adhering to this + /// schema at \p path on \p stage. If no prim exists at \p path on + /// \p stage, or if the prim at that path does not adhere to this schema, + /// return an invalid schema object. This is shorthand for the following: + /// + /// \code + /// MjcPhysicsImageableAPI(stage->GetPrimAtPath(path)); + /// \endcode + /// + MJCPHYSICS_API + static MjcPhysicsImageableAPI Get(const UsdStagePtr &stage, + const SdfPath &path); + + /// Returns true if this single-apply API schema can be applied to + /// the given \p prim. If this schema can not be a applied to the prim, + /// this returns false and, if provided, populates \p whyNot with the + /// reason it can not be applied. + /// + /// Note that if CanApply returns false, that does not necessarily imply + /// that calling Apply will fail. Callers are expected to call CanApply + /// before calling Apply if they want to ensure that it is valid to + /// apply a schema. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static bool CanApply(const UsdPrim &prim, std::string *whyNot = nullptr); + + /// Applies this single-apply API schema to the given \p prim. + /// This information is stored by adding "MjcImageableAPI" to the + /// token-valued, listOp metadata \em apiSchemas on the prim. + /// + /// \return A valid MjcPhysicsImageableAPI object is returned upon success. + /// An invalid (or empty) MjcPhysicsImageableAPI object is returned upon + /// failure. See \ref UsdPrim::ApplyAPI() for conditions + /// resulting in failure. + /// + /// \sa UsdPrim::GetAppliedSchemas() + /// \sa UsdPrim::HasAPI() + /// \sa UsdPrim::CanApplyAPI() + /// \sa UsdPrim::ApplyAPI() + /// \sa UsdPrim::RemoveAPI() + /// + MJCPHYSICS_API + static MjcPhysicsImageableAPI Apply(const UsdPrim &prim); + + protected: + /// Returns the kind of schema this class belongs to. + /// + /// \sa UsdSchemaKind + MJCPHYSICS_API + UsdSchemaKind _GetSchemaKind() const override; + + private: + // needs to invoke _GetStaticTfType. + friend class UsdSchemaRegistry; + MJCPHYSICS_API + static const TfType &_GetStaticTfType(); + + static bool _IsTypedSchema(); + + // override SchemaBase virtuals. + MJCPHYSICS_API + const TfType &_GetTfType() const override; + + public: + // --------------------------------------------------------------------- // + // GROUP + // --------------------------------------------------------------------- // + /// Integer MuJoCo group to which the imageable belongs. + /// + /// | || + /// | -- | -- | + /// | Declaration | `uniform int mjc:group = 0` | + /// | C++ Type | int | + /// | \ref Usd_Datatypes "Usd Type" | SdfValueTypeNames->Int | + /// | \ref SdfVariability "Variability" | SdfVariabilityUniform | + MJCPHYSICS_API + UsdAttribute GetGroupAttr() const; + + /// See GetGroupAttr(), 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 CreateGroupAttr(VtValue const &defaultValue = VtValue(), + bool writeSparsely = false) const; + + public: + // ===================================================================== // + // Feel free to add custom code below this line, it will be preserved by + // the code generator. + // + // Just remember to: + // - Close the class declaration with }; + // - Close the namespace with PXR_NAMESPACE_CLOSE_SCOPE + // - Close the include guard with #endif + // ===================================================================== // + // --(BEGIN CUSTOM CODE)-- +}; + +PXR_NAMESPACE_CLOSE_SCOPE + +#endif diff --git a/include/mujoco/experimental/usd/mjcPhysics/tokens.h b/include/mujoco/experimental/usd/mjcPhysics/tokens.h index 1de2afb3..107608c1 100644 --- a/include/mujoco/experimental/usd/mjcPhysics/tokens.h +++ b/include/mujoco/experimental/usd/mjcPhysics/tokens.h @@ -408,8 +408,8 @@ struct MjcPhysicsTokensType { const TfToken mjcGear; /// \brief "mjc:group" /// - /// MjcPhysicsSiteAPI, MjcPhysicsCollisionAPI, MjcPhysicsActuator, - /// MjcPhysicsJointAPI + /// MjcPhysicsSiteAPI, MjcPhysicsImageableAPI, MjcPhysicsCollisionAPI, + /// MjcPhysicsActuator, MjcPhysicsJointAPI const TfToken mjcGroup; /// \brief "mjc:inertia" /// @@ -697,6 +697,10 @@ struct MjcPhysicsTokensType { /// /// Schema identifier and family for MjcPhysicsCollisionAPI const TfToken MjcCollisionAPI; + /// \brief "MjcImageableAPI" + /// + /// Schema identifier and family for MjcPhysicsImageableAPI + const TfToken MjcImageableAPI; /// \brief "MjcJointAPI" /// /// Schema identifier and family for MjcPhysicsJointAPI diff --git a/src/experimental/usd/CMakeLists.txt b/src/experimental/usd/CMakeLists.txt index 59f8e845..100a1c77 100644 --- a/src/experimental/usd/CMakeLists.txt +++ b/src/experimental/usd/CMakeLists.txt @@ -174,6 +174,7 @@ add_library(${MJC_PHYSICS_PLUGIN_TARGET_NAME} SHARED) target_sources(${MJC_PHYSICS_PLUGIN_TARGET_NAME} PRIVATE mjcPhysics/actuator.cpp mjcPhysics/collisionAPI.cpp + mjcPhysics/imageableAPI.cpp mjcPhysics/jointAPI.cpp mjcPhysics/keyframe.cpp mjcPhysics/materialAPI.cpp diff --git a/src/experimental/usd/mjcPhysics/generatedSchema.usda b/src/experimental/usd/mjcPhysics/generatedSchema.usda index 4d3848ce..5a7621aa 100644 --- a/src/experimental/usd/mjcPhysics/generatedSchema.usda +++ b/src/experimental/usd/mjcPhysics/generatedSchema.usda @@ -293,6 +293,16 @@ class "MjcSiteAPI" ( ) } +class "MjcImageableAPI" ( + doc = "API describing attributes for visual entities in MuJoCo." +) +{ + uniform int mjc:group = 0 ( + displayName = "Group" + doc = "Integer MuJoCo group to which the imageable belongs." + ) +} + class "MjcCollisionAPI" ( doc = "API describing a MuJoCo collider." ) diff --git a/src/experimental/usd/mjcPhysics/imageableAPI.cpp b/src/experimental/usd/mjcPhysics/imageableAPI.cpp new file mode 100644 index 00000000..4a3d36f3 --- /dev/null +++ b/src/experimental/usd/mjcPhysics/imageableAPI.cpp @@ -0,0 +1,124 @@ +// Copyright 2025 DeepMind Technologies Limited +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include +#include +#include + +PXR_NAMESPACE_OPEN_SCOPE + +// Register the schema with the TfType system. +TF_REGISTRY_FUNCTION(TfType) { + TfType::Define >(); +} + +/* virtual */ +MjcPhysicsImageableAPI::~MjcPhysicsImageableAPI() {} + +/* static */ +MjcPhysicsImageableAPI MjcPhysicsImageableAPI::Get(const UsdStagePtr &stage, + const SdfPath &path) { + if (!stage) { + TF_CODING_ERROR("Invalid stage"); + return MjcPhysicsImageableAPI(); + } + return MjcPhysicsImageableAPI(stage->GetPrimAtPath(path)); +} + +/* virtual */ +UsdSchemaKind MjcPhysicsImageableAPI::_GetSchemaKind() const { + return MjcPhysicsImageableAPI::schemaKind; +} + +/* static */ +bool MjcPhysicsImageableAPI::CanApply(const UsdPrim &prim, + std::string *whyNot) { + return prim.CanApplyAPI(whyNot); +} + +/* static */ +MjcPhysicsImageableAPI MjcPhysicsImageableAPI::Apply(const UsdPrim &prim) { + if (prim.ApplyAPI()) { + return MjcPhysicsImageableAPI(prim); + } + return MjcPhysicsImageableAPI(); +} + +/* static */ +const TfType &MjcPhysicsImageableAPI::_GetStaticTfType() { + static TfType tfType = TfType::Find(); + return tfType; +} + +/* static */ +bool MjcPhysicsImageableAPI::_IsTypedSchema() { + static bool isTyped = _GetStaticTfType().IsA(); + return isTyped; +} + +/* virtual */ +const TfType &MjcPhysicsImageableAPI::_GetTfType() const { + return _GetStaticTfType(); +} + +UsdAttribute MjcPhysicsImageableAPI::GetGroupAttr() const { + return GetPrim().GetAttribute(MjcPhysicsTokens->mjcGroup); +} + +UsdAttribute MjcPhysicsImageableAPI::CreateGroupAttr( + VtValue const &defaultValue, bool writeSparsely) const { + return UsdSchemaBase::_CreateAttr( + MjcPhysicsTokens->mjcGroup, SdfValueTypeNames->Int, + /* custom = */ false, SdfVariabilityUniform, defaultValue, writeSparsely); +} + +namespace { +static inline TfTokenVector _ConcatenateAttributeNames( + const TfTokenVector &left, const TfTokenVector &right) { + TfTokenVector result; + result.reserve(left.size() + right.size()); + result.insert(result.end(), left.begin(), left.end()); + result.insert(result.end(), right.begin(), right.end()); + return result; +} +} // namespace + +/*static*/ +const TfTokenVector &MjcPhysicsImageableAPI::GetSchemaAttributeNames( + bool includeInherited) { + static TfTokenVector localNames = { + MjcPhysicsTokens->mjcGroup, + }; + static TfTokenVector allNames = _ConcatenateAttributeNames( + UsdAPISchemaBase::GetSchemaAttributeNames(true), localNames); + + if (includeInherited) + return allNames; + else + return localNames; +} + +PXR_NAMESPACE_CLOSE_SCOPE + +// ===================================================================== // +// Feel free to add custom code below this line. It will be preserved by +// the code generator. +// +// Just remember to wrap code in the appropriate delimiters: +// 'PXR_NAMESPACE_OPEN_SCOPE', 'PXR_NAMESPACE_CLOSE_SCOPE'. +// ===================================================================== // +// --(BEGIN CUSTOM CODE)-- diff --git a/src/experimental/usd/mjcPhysics/plugInfo.json b/src/experimental/usd/mjcPhysics/plugInfo.json index fc1e68e9..8c37064f 100644 --- a/src/experimental/usd/mjcPhysics/plugInfo.json +++ b/src/experimental/usd/mjcPhysics/plugInfo.json @@ -23,6 +23,16 @@ ], "schemaKind": "singleApplyAPI" }, + "MjcPhysicsImageableAPI": { + "alias": { + "UsdSchemaBase": "MjcImageableAPI" + }, + "autoGenerated": true, + "bases": [ + "UsdAPISchemaBase" + ], + "schemaKind": "singleApplyAPI" + }, "MjcPhysicsJointAPI": { "alias": { "UsdSchemaBase": "MjcJointAPI" diff --git a/src/experimental/usd/mjcPhysics/schema.usda b/src/experimental/usd/mjcPhysics/schema.usda index 7e424f1f..3b4fd849 100644 --- a/src/experimental/usd/mjcPhysics/schema.usda +++ b/src/experimental/usd/mjcPhysics/schema.usda @@ -649,6 +649,25 @@ class "MjcSiteAPI" ) } +class "MjcImageableAPI" +( + customData = { + string className = "ImageableAPI" + } + doc = """API describing attributes for visual entities in MuJoCo.""" + + inherits = +) +{ + uniform int mjc:group = 0 ( + customData = { + string apiName = "Group" + } + displayName = "Group" + doc = """Integer MuJoCo group to which the imageable belongs.""" + ) +} + class "MjcCollisionAPI" ( customData = { diff --git a/src/experimental/usd/mjcPhysics/tokens.cpp b/src/experimental/usd/mjcPhysics/tokens.cpp index 26c22669..8a5c3e4a 100644 --- a/src/experimental/usd/mjcPhysics/tokens.cpp +++ b/src/experimental/usd/mjcPhysics/tokens.cpp @@ -177,6 +177,7 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() user("user", TfToken::Immortal), MjcActuator("MjcActuator", TfToken::Immortal), MjcCollisionAPI("MjcCollisionAPI", TfToken::Immortal), + MjcImageableAPI("MjcImageableAPI", TfToken::Immortal), MjcJointAPI("MjcJointAPI", TfToken::Immortal), MjcKeyframe("MjcKeyframe", TfToken::Immortal), MjcMaterialAPI("MjcMaterialAPI", TfToken::Immortal), @@ -336,6 +337,7 @@ MjcPhysicsTokensType::MjcPhysicsTokensType() user, MjcActuator, MjcCollisionAPI, + MjcImageableAPI, MjcJointAPI, MjcKeyframe, MjcMaterialAPI, diff --git a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc index 37af6929..d2cecc4b 100644 --- a/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc +++ b/src/experimental/usd/plugins/mjcf/mujoco_to_usd.cc @@ -656,7 +656,6 @@ class ModelWriter { create_flag_attr(token, flag, false); } - // Compiler attributes WriteUniformAttribute(physics_scene_path, pxr::SdfValueTypeNames->Bool, MjcPhysicsTokens->mjcCompilerAutoLimits, @@ -684,7 +683,8 @@ class ModelWriter { WriteUniformAttribute(physics_scene_path, pxr::SdfValueTypeNames->Token, MjcPhysicsTokens->mjcCompilerAngle, - spec_->compiler.degree ? MjcPhysicsTokens->degree:MjcPhysicsTokens->radian); + spec_->compiler.degree ? MjcPhysicsTokens->degree + : MjcPhysicsTokens->radian); WriteUniformAttribute(physics_scene_path, pxr::SdfValueTypeNames->Bool, MjcPhysicsTokens->mjcCompilerFitAABB, @@ -1621,6 +1621,13 @@ class ModelWriter { WriteUniformAttribute(geom_path, pxr::SdfValueTypeNames->Int, MjcPhysicsTokens->mjcGroup, geom->group); + if (model_->geom_contype[geom_id] == 0 && + model_->geom_conaffinity[geom_id] == 0) { + // If the geom is purely visual, apply the imageable API. + ApplyApiSchema(data_, geom_path, + MjcPhysicsTokens->MjcImageableAPI); + } + // Apply the physics schemas if we are writing physics and the // geom participates in collisions. if (write_physics_ && (model_->geom_contype[geom_id] != 0 || @@ -1715,6 +1722,11 @@ class ModelWriter { SetAttributeDefault(data_, approximation_attr, pxr::UsdPhysicsTokens->convexHull); } + } else { + // Currently imageable only has a group API. But since it's the same + // naming in MjcCollisionsAPI we've already set it earlier in this + // function. + ApplyApiSchema(data_, geom_path, MjcPhysicsTokens->MjcImageableAPI); } mjsDefault *spec_default = mjs_getDefault(geom->element); diff --git a/src/experimental/usd/usd_to_mjspec.cc b/src/experimental/usd/usd_to_mjspec.cc index c2e667ea..f701f63a 100644 --- a/src/experimental/usd/usd_to_mjspec.cc +++ b/src/experimental/usd/usd_to_mjspec.cc @@ -21,6 +21,7 @@ #include #include +#include #include #include #include @@ -1335,6 +1336,7 @@ void ParseUsdGeomGprim(mjSpec* spec, const pxr::UsdPrim& gprim, if (!MaybeParseGeomPrimitive(gprim, geom, caches.xform_cache)) { ParseUsdMesh(spec, gprim, geom, caches.xform_cache); } + pxr::UsdShadeMaterial bound_material = pxr::UsdShadeMaterialBindingAPI(gprim).ComputeBoundMaterial( &caches.bindings_cache, &caches.collection_query_cache); @@ -1350,6 +1352,14 @@ void ParseUsdGeomGprim(mjSpec* spec, const pxr::UsdPrim& gprim, } mjs_setString(geom->material, mjs_getName(material->element)->c_str()); } + + if (gprim.HasAPI()) { + auto imageable_api = pxr::MjcPhysicsImageableAPI(gprim); + auto group_attr = imageable_api.GetGroupAttr(); + if (group_attr.HasAuthoredValue()) { + group_attr.Get(&geom->group); + } + } } void ParseUsdPhysicsCollider(mjSpec* spec, @@ -1389,9 +1399,10 @@ void ParseUsdPhysicsCollider(mjSpec* spec, } } - // Parse the Mass API after the physics material APIs since the density attribute - // from the Mass API is supposed to override the Material API density attribute. - // See https://openusd.org/dev/api/usd_physics_page_front.html + // Parse the Mass API after the physics material APIs since the density + // attribute from the Mass API is supposed to override the Material API + // density attribute. See + // https://openusd.org/dev/api/usd_physics_page_front.html if (prim.HasAPI()) { ParseUsdPhysicsMassAPIForGeom(geom, pxr::UsdPhysicsMassAPI(prim)); } @@ -1672,8 +1683,9 @@ void PopulateSpecFromTree(pxr::UsdStageRefPtr stage, mjSpec* spec, : stage->GetPrimAtPath(current_node->body_path); for (const auto& gprim_path : current_node->visual_gprims) { - ParseUsdGeomGprim(spec, stage->GetPrimAtPath(gprim_path), - body_prim_for_xform, current_mj_body, caches); + auto gprim = stage->GetPrimAtPath(gprim_path); + ParseUsdGeomGprim(spec, gprim, body_prim_for_xform, current_mj_body, + caches); } for (const auto& collider_path : current_node->colliders) { diff --git a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc index 1863f444..8cebf956 100644 --- a/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc +++ b/test/experimental/usd/plugins/mjcf/mjcf_file_format_test.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -1453,6 +1454,32 @@ TEST_F(MjcfSdfFileFormatPluginTest, TestPhysicsColliders) { pxr::UsdPhysicsTokens->convexHull); } +TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsImageableAPI) { + static constexpr char xml[] = R"( + + + + + + + + + + + )"; + auto stage = OpenStageWithPhysics(xml); + + EXPECT_PRIM_API_APPLIED(stage, "/test/body/mesh/Mesh", + pxr::MjcPhysicsImageableAPI); + ExpectAttributeEqual(stage, "/test/body/mesh/Mesh.mjc:group", 4); +} + TEST_F(MjcfSdfFileFormatPluginTest, TestMjcPhysicsCollisionAPI) { static constexpr char xml[] = R"(