From a5dc57c0c36a3a7d389275020eeb40888990e0bd Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Fri, 9 Jan 2026 14:47:37 -0800 Subject: [PATCH] Move OpenUSD parsing to usd_decoder Add usd_decoder to mujoco/plugin. - Previously was src/experimental/usd/usd_to_mjspec.cc, now that same spec is returned by the plugin. PiperOrigin-RevId: 854350962 Change-Id: Ia980190a557c50ce8197980922a2594b613859b4 --- CMakeLists.txt | 1 + cmake/third_party_deps/openusd/CMakeLists.txt | 67 ++++---- doc/OpenUSD/building.rst | 64 +++---- doc/changelog.rst | 7 + include/mujoco/experimental/usd/usd.h | 44 ----- plugin/usd_decoder/CMakeLists.txt | 53 ++++++ .../usd_decoder}/kinematic_tree.cc | 8 +- .../usd_decoder}/kinematic_tree.h | 12 +- .../usd_decoder}/material_parsing.cc | 59 +------ .../usd_decoder}/material_parsing.h | 10 +- .../usd_decoder/usd_decoder.cc | 160 ++++++++++++------ simulate/CMakeLists.txt | 1 + simulate/main.cc | 38 ++--- src/experimental/usd/CMakeLists.txt | 6 +- src/xml/xml_api.cc | 36 ---- src/xml/xml_native_reader.cc | 15 +- .../usd/mjcPhysics/mjc_site_api_test.cc | 6 +- 17 files changed, 270 insertions(+), 317 deletions(-) delete mode 100644 include/mujoco/experimental/usd/usd.h create mode 100644 plugin/usd_decoder/CMakeLists.txt rename {src/experimental/usd => plugin/usd_decoder}/kinematic_tree.cc (98%) rename {src/experimental/usd => plugin/usd_decoder}/kinematic_tree.h (88%) rename {src/experimental/usd => plugin/usd_decoder}/material_parsing.cc (86%) rename {src/experimental/usd => plugin/usd_decoder}/material_parsing.h (75%) rename src/experimental/usd/usd_to_mjspec.cc => plugin/usd_decoder/usd_decoder.cc (94%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8f0310af..31c82414 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -243,6 +243,7 @@ if(MUJOCO_WITH_USD) endif() add_subdirectory(src/experimental/usd) + add_subdirectory(plugin/usd_decoder) endif() if(BUILD_TESTING AND MUJOCO_BUILD_TESTS) diff --git a/cmake/third_party_deps/openusd/CMakeLists.txt b/cmake/third_party_deps/openusd/CMakeLists.txt index 37676017..4eb02281 100644 --- a/cmake/third_party_deps/openusd/CMakeLists.txt +++ b/cmake/third_party_deps/openusd/CMakeLists.txt @@ -13,42 +13,39 @@ # limitations under the License. cmake_minimum_required(VERSION "3.16") -option(BUILD_USD OFF "Build OpenUSD") project(openusd-dependency) -if (BUILD_USD) - include(ExternalProject) +include(ExternalProject) - set(DEPS_DIR ${CMAKE_BINARY_DIR}/../../../../build/_deps) +set(DEPS_DIR ${CMAKE_BINARY_DIR}/../../../../build/_deps) - ExternalProject_Add(openusd - GIT_REPOSITORY https://github.com/PixarAnimationStudios/USD.git - GIT_TAG v25.11 - PREFIX openusd - BUILD_IN_SOURCE 0 - UPDATE_COMMAND "" - CONFIGURE_COMMAND "" - INSTALL_COMMAND "" - SOURCE_DIR ${DEPS_DIR}/openusd-src - BINARY_DIR ${DEPS_DIR}/openusd-build - INSTALL_DIR ${DEPS_DIR}/openusd-build - BUILD_COMMAND python3 ${DEPS_DIR}/openusd-src/build_scripts/build_usd.py - --build-shared - --no-examples - --no-tools - --no-ptex - --no-prman - --no-openimageio - --no-opencolorio - --no-alembic - --no-draco - --no-materialx - --no-tutorials - --no-tests - --no-docs - --no-imaging - --no-python - --no-usdValidation - - ) -endif() +ExternalProject_Add(openusd + GIT_REPOSITORY https://github.com/PixarAnimationStudios/USD.git + GIT_TAG v25.11 + PREFIX openusd + BUILD_IN_SOURCE 0 + UPDATE_COMMAND "" + CONFIGURE_COMMAND "" + INSTALL_COMMAND "" + SOURCE_DIR ${DEPS_DIR}/openusd-src + BINARY_DIR ${DEPS_DIR}/openusd-build + INSTALL_DIR ${DEPS_DIR}/openusd-build + BUILD_COMMAND python3 ${DEPS_DIR}/openusd-src/build_scripts/build_usd.py + --build-shared + --no-examples + --no-tools + --no-ptex + --no-prman + --no-openimageio + --no-opencolorio + --no-alembic + --no-draco + --no-materialx + --no-tutorials + --no-tests + --no-docs + --no-imaging + --no-python + --no-usdValidation + +) diff --git a/doc/OpenUSD/building.rst b/doc/OpenUSD/building.rst index 880213d1..21b50aee 100644 --- a/doc/OpenUSD/building.rst +++ b/doc/OpenUSD/building.rst @@ -3,55 +3,61 @@ Building .. WARNING:: OpenUSD support is currently experimental and subject to frequent change. -Advanced users can start testing out USD support by building against their own USD libraries or USD built from source. +MuJoCo must be built against a pre-built USD library, we provide a utility to do so but you may also bring your own USD +libraries. -This assumes that you have built MuJoCo in ``~/mujoco`` and have a build directory at ``~/mujoco/build`` +The following instructions assume that you have cloned MuJoCo into ``~/mujoco`` and have a build directory at +``~/mujoco/build``. + +.. _usdBuildingUSD: Building USD ------------ -USD has a pretty streamlined installation via their ``build_usd.py`` script. It's recommended to use a separate -installation directory that exists outside of the cloned repository directory. +If you have a pre-built USD library, you can skip this section. + +MuJoCo provides a CMake project that simplifies the process of building USD. It will download and build USD with only +the necessary features enabled. + +.. code-block:: bash + + cd ~/mujoco + cmake -Bcmake/third_party_deps/openusd/build cmake/third_party_deps/openusd + cmake --build cmake/third_party_deps/openusd/build + +If you want to customize the build process, you can use USD's ``build_usd.py`` script. It's recommended to use a +separate installation directory that exists outside of the cloned repository directory. .. code-block:: bash git clone https://github.com/PixarAnimationStudios/OpenUSD python OpenUSD/build_scripts/build_usd.py /path/to/my_usd_install_dir +.. _usdEnablingUSD: + Enabling USD ------------ -USD is comprised of many plugins. When USD enabled application starts up it looks for an environment variable called -``PXR_PLUGINPATH_NAME``. Below is an example where we build MuJoCo with USD enabled and set this variable. +If USD was built with the third_party_deps/openusd CMake project, you can enable USD support with the MUJOCO_WITH_USD +flag. .. code-block:: bash - cd ~/mujoco/build - cmake .. -DCMAKE_BUILD_TYPE=Release -DUSD_DIR=/path/to/my_usd_install_dir - cmake --build . -j 30; sudo cmake --install . - export PXR_PLUGINPATH_NAME=/usr/local/lib/mujocoUsd/resources/*/plugInfo.json + cd ~/mujoco + cmake -Bbuild -S. -DMUJOCO_WITH_USD=True + cmake --build build -j 64 + +Otherwise, if you have a pre-built USD library, you must also pass the pxr_DIR flag. + +.. code-block:: bash + + cd ~/mujoco + cmake -Bbuild -S. -DMUJOCO_WITH_USD=True -Dpxr_DIR=/path/to/my_usd_install_dir + cmake --build build -j 64 + If we now run :ref:`simulate.cc `, we will be able to drag and drop USD files. .. code-block:: bash simulate - -Enabling plugins in Houdini ---------------------------- - -Houdini is a procedural content authoring tool with extensive support for USD workflows via their Solaris context. It's -highly popular in the VFX industry, and it's easy to imagine procedural generation tools for simulation ready assets and -scenes. - -To allow support for loading MJCF files in Solaris, and usage of the mjcPhysics schemas you can build against Houdini's -USD libraries. To do so, simply run `source ./houdini_setup` as descrived in the `SideFX documentation -`__. - -.. code-block:: bash - - cd ~/mujoco/build - cmake .. -DCMAKE_BUILD_TYPE=Release -DHOUDINI_HFS_DIR=$HFS - cmake --build . -j 30; sudo cmake --install . - export PXR_PLUGINPATH_NAME=/usr/local/lib/mujocoUsd/resources/*/plugInfo.json - houdini diff --git a/doc/changelog.rst b/doc/changelog.rst index 37d7592d..60d51dc1 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -39,6 +39,13 @@ General a silent implicit cast, now negativity will trigger an error. - Added a :ref:`depth` rendering flag. +- :doc:`OpenUSD `: + + - Parsing has been moved out of experimental into a mjpDecoder plugin. (documentation pending) + - OpenUSD can now be built with the `third_party_deps/openusd` CMake utility project. + - ``USD_DIR`` is no longer used by the MuJoCo CMake project, instead use ``pxr_DIR`` if you have a pre-built USD library. + - Users no longer have to set ``PXR_PLUGINPATH_NAME`` environment variable, MuJoCo should load USD plugins automatically. + MJX ^^^ - Added ``actuator_length``, ``cdof`` and ``cdof_dof`` fields to ``mjx.Data``. diff --git a/include/mujoco/experimental/usd/usd.h b/include/mujoco/experimental/usd/usd.h deleted file mode 100644 index cfeac6fb..00000000 --- a/include/mujoco/experimental/usd/usd.h +++ /dev/null @@ -1,44 +0,0 @@ -// 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 MUJOCO_EXPERIMENTAL_SRC_USD_USD_TO_MJSPEC_H_ -#define MUJOCO_EXPERIMENTAL_SRC_USD_USD_TO_MJSPEC_H_ - -#include -#include - -// Given a USD file parse to a spec, then compile and return the low-level -// model. -// -// Particular care is taken for physics data to be lossless but visual -// data such as materials may be lossy. -MJAPI mjModel* mj_loadUSD(const char* filename, const mjVFS* vfs, char* error, - int error_sz); - -// Given a USD layer identifier, this function will do a best effort conversion -// from the composed stage to mjSpec. -// -// Particular care is taken for physics data to be lossless but visual -// data such as materials may be lossy. -MJAPI mjSpec* mj_parseUSD(const char* identifier, const mjVFS* vfs, char* error, - int error_sz); - -// Given a USD stage, this function will do a best effort conversion to -// mjSpec. -// -// Particular care is taken for physics data to be lossless but visual -// data such as materials may be lossy. -MJAPI mjSpec* mj_parseUSDStage(pxr::UsdStageRefPtr stage); - -#endif // MUJOCO_EXPERIMENTAL_SRC_USD_USD_TO_MJSPEC_H_ diff --git a/plugin/usd_decoder/CMakeLists.txt b/plugin/usd_decoder/CMakeLists.txt new file mode 100644 index 00000000..2c113311 --- /dev/null +++ b/plugin/usd_decoder/CMakeLists.txt @@ -0,0 +1,53 @@ +# Copyright 2026 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 +# +# https://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. +find_package(pxr REQUIRED) + +set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE) +set(USD_DECODER_SRCS + usd_decoder.cc + kinematic_tree.cc + kinematic_tree.h + material_parsing.cc + material_parsing.h + utils.h +) + +add_library(usd_decoder_plugin SHARED ${USD_DECODER_SRCS}) +target_include_directories(usd_decoder_plugin PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../.. +) +target_link_libraries(usd_decoder_plugin PRIVATE + mujoco + mjcPhysics +) +target_compile_options(usd_decoder_plugin PRIVATE + ${AVX_COMPILE_OPTIONS} + ${MUJOCO_MACOS_COMPILE_OPTIONS} + ${EXTRA_COMPILE_OPTIONS} + ${MUJOCO_CXX_FLAGS} + -Wno-deprecated # pxr Tf lib uses deprecated header +) +target_link_options(usd_decoder_plugin PRIVATE + ${MUJOCO_MACOS_LINK_OPTIONS} + ${EXTRA_LINK_OPTIONS} +) + +# Install to mujoco_plugin directory in bin location so that it is picked up by simulate +# on startup. +install( + TARGETS usd_decoder_plugin + LIBRARY DESTINATION "${CMAKE_INSTALL_BINDIR}/mujoco_plugin" +) + +target_link_libraries(usd_decoder_plugin PRIVATE usd usdGeom usdPhysics usdShade gf tf ar vt kind) diff --git a/src/experimental/usd/kinematic_tree.cc b/plugin/usd_decoder/kinematic_tree.cc similarity index 98% rename from src/experimental/usd/kinematic_tree.cc rename to plugin/usd_decoder/kinematic_tree.cc index cf4a782a..c579f412 100644 --- a/src/experimental/usd/kinematic_tree.cc +++ b/plugin/usd_decoder/kinematic_tree.cc @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "experimental/usd/kinematic_tree.h" +#include "kinematic_tree.h" #include #include @@ -34,9 +34,6 @@ #include #include -namespace mujoco { -namespace usd { - bool GetJointBodies(const pxr::UsdPhysicsJoint& joint, pxr::SdfPath* from, pxr::SdfPath* to) { // Grab the default prim path @@ -274,6 +271,3 @@ std::unique_ptr BuildKinematicTree(const pxr::UsdStageRefPtr stage) { } return world_root; } - -} // namespace usd -} // namespace mujoco diff --git a/src/experimental/usd/kinematic_tree.h b/plugin/usd_decoder/kinematic_tree.h similarity index 88% rename from src/experimental/usd/kinematic_tree.h rename to plugin/usd_decoder/kinematic_tree.h index 3fe964d5..b80acc02 100644 --- a/src/experimental/usd/kinematic_tree.h +++ b/plugin/usd_decoder/kinematic_tree.h @@ -12,8 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef MUJOCO_SRC_EXPERIMENTAL_USD_KINEMATIC_TREE_H_ -#define MUJOCO_SRC_EXPERIMENTAL_USD_KINEMATIC_TREE_H_ +#ifndef MUJOCO_PLUGIN_USD_DECODER_KINEMATIC_TREE_H_ +#define MUJOCO_PLUGIN_USD_DECODER_KINEMATIC_TREE_H_ #include #include @@ -21,9 +21,6 @@ #include #include -namespace mujoco { -namespace usd { - // A struct to represent a node in the kinematic tree. // Using a struct with a vector of children preserves the order of bodies, // which is important for things like keyframes and policy compatibility. @@ -51,7 +48,4 @@ using JointVec = std::vector; // Returns the root of the kinematic tree, or `nullptr` for invalid structures. std::unique_ptr BuildKinematicTree(const pxr::UsdStageRefPtr stage); -} // namespace usd -} // namespace mujoco - -#endif // MUJOCO_SRC_EXPERIMENTAL_USD_KINEMATIC_TREE_H_ +#endif // MUJOCO_PLUGIN_USD_DECODER_KINEMATIC_TREE_H_ diff --git a/src/experimental/usd/material_parsing.cc b/plugin/usd_decoder/material_parsing.cc similarity index 86% rename from src/experimental/usd/material_parsing.cc rename to plugin/usd_decoder/material_parsing.cc index 9b26dcdd..259d0345 100644 --- a/src/experimental/usd/material_parsing.cc +++ b/plugin/usd_decoder/material_parsing.cc @@ -12,17 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#include "experimental/usd/material_parsing.h" +#include "material_parsing.h" -#include -#include #include -#include #include #include -#include -#include "lodepng.h" #include #include #include @@ -39,9 +34,6 @@ #include #include -namespace mujoco { -namespace usd { - // Using to satisfy TF_DEFINE_PRIVATE_TOKENS macro below and avoid operating in // PXR_NS. using pxr::TfToken; @@ -146,53 +138,16 @@ ResolvedShaderInput ReadUsdUVTexture(mjSpec* spec, } auto extension = resolver.GetExtension(resolved_texture_path); - if (extension != "png") { - mju_error("MuJoCo USD Parsing only supports PNG textures: %s", - resolved_texture_path.c_str()); - return out; - } FILE* fp = fopen(resolved_texture_path.c_str(), "r"); - if (fp) { - mjs_setString(texture->content_type, "image/png"); - mjs_setString(texture->file, resolved_texture_path.c_str()); - out.sampler = texture; + if (fp == nullptr) { + mju_error( + "USD decoder only supports assets that are available on the file " + "system"); return out; } - - std::shared_ptr texture_asset = - resolver.OpenAsset(pxr::ArResolvedPath(resolved_texture_path)); - - size_t texture_size = texture_asset->GetSize(); - - uint32_t width, height; - std::vector image; - lodepng::State state; - if (nchannels == 3) { - state.info_raw.colortype = LCT_RGB; - } else if (nchannels == 1) { - state.info_raw.colortype = LCT_GREY; - } else { - mju_error("MuJoCo USD Parsing only supports 1 or 3 channel textures."); - return out; - } - - unsigned error = lodepng::decode( - image, width, height, state, - reinterpret_cast(texture_asset->GetBuffer().get()), - texture_size); - - // check for errors - if (error) { - mju_error("LodePNG error %u: %s", error, lodepng_error_text(error)); - return out; - } - - texture->width = width; - texture->height = height; texture->nchannel = nchannels; - mjs_setBuffer(texture->data, image.data(), image.size()); - + mjs_setString(texture->file, resolved_texture_path.c_str()); out.sampler = texture; return out; } @@ -311,5 +266,3 @@ mjsMaterial* ParseMaterial(mjSpec* spec, return mj_mat; } -} // namespace usd -} // namespace mujoco diff --git a/src/experimental/usd/material_parsing.h b/plugin/usd_decoder/material_parsing.h similarity index 75% rename from src/experimental/usd/material_parsing.h rename to plugin/usd_decoder/material_parsing.h index 48103676..77a24321 100644 --- a/src/experimental/usd/material_parsing.h +++ b/plugin/usd_decoder/material_parsing.h @@ -12,16 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifndef MUJOCO_SRC_EXPERIMENTAL_USD_MATERIAL_PARSING_H_ -#define MUJOCO_SRC_EXPERIMENTAL_USD_MATERIAL_PARSING_H_ +#ifndef MUJOCO_PLUGIN_USD_DECODER_MATERIAL_PARSING_H_ +#define MUJOCO_PLUGIN_USD_DECODER_MATERIAL_PARSING_H_ #include #include -namespace mujoco { -namespace usd { mjsMaterial* ParseMaterial(mjSpec* spec, const pxr::UsdShadeMaterial &material); -} // namespace usd -} // namespace mujoco -#endif // MUJOCO_SRC_EXPERIMENTAL_USD_MATERIAL_PARSING_H_ +#endif // MUJOCO_PLUGIN_USD_DECODER_MATERIAL_PARSING_H_ diff --git a/src/experimental/usd/usd_to_mjspec.cc b/plugin/usd_decoder/usd_decoder.cc similarity index 94% rename from src/experimental/usd/usd_to_mjspec.cc rename to plugin/usd_decoder/usd_decoder.cc index 48bacd57..231031fc 100644 --- a/src/experimental/usd/usd_to_mjspec.cc +++ b/plugin/usd_decoder/usd_decoder.cc @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -33,11 +34,9 @@ #include #include #include -#include -#include #include -#include "experimental/usd/kinematic_tree.h" -#include "experimental/usd/material_parsing.h" +#include "kinematic_tree.h" +#include "material_parsing.h" #include #include #include @@ -74,9 +73,6 @@ #include #include -namespace mujoco { -namespace usd { - using pxr::MjcPhysicsTokens; using pxr::TfToken; @@ -87,6 +83,26 @@ struct UsdCaches { std::map parsed_materials; }; +constexpr const char* kUsdPrimPathKey = "usd_primpath"; + +void SetUsdPrimPathUserValue(mjsElement* element, + const pxr::SdfPath& prim_path) { + // The value is a pointer to a newly allocated SdfPath, which will be deleted + // when the mjsElement is deleted. + const pxr::SdfPath* usd_primpath = new pxr::SdfPath(prim_path); + mjs_setUserValueWithCleanup( + element, kUsdPrimPathKey, usd_primpath, + [](const void* data) { delete static_cast(data); }); +} + +pxr::SdfPath GetUsdPrimPathUserValue(mjsElement* element) { + const void* user_data = mjs_getUserValue(element, kUsdPrimPathKey); + if (user_data) { + return *static_cast(user_data); + } + return pxr::SdfPath(); +} + void SetDoubleArrFromGfVec3d(double* to, const pxr::GfVec3d& from) { to[0] = from[0]; to[1] = from[1]; @@ -279,7 +295,7 @@ mjsMesh* ParseUsdMesh(mjSpec* spec, const pxr::UsdPrim& prim, mjsGeom* geom, } mjsMesh* mesh = mjs_addMesh(spec, nullptr); - mujoco::usd::SetUsdPrimPathUserValue(mesh->element, prim.GetPath()); + SetUsdPrimPathUserValue(mesh->element, prim.GetPath()); geom->type = mjGEOM_MESH; pxr::UsdGeomMesh usd_mesh(prim); @@ -924,14 +940,13 @@ void ParseMjcPhysicsMeshCollisionAPI( } } -void ParseMjcPhysicsTendon(mjSpec* spec, - const pxr::MjcPhysicsTendon& tendon) { +void ParseMjcPhysicsTendon(mjSpec* spec, const pxr::MjcPhysicsTendon& tendon) { pxr::UsdPrim prim = tendon.GetPrim(); pxr::UsdStageRefPtr stage = prim.GetStage(); mjsTendon* mj_tendon = mjs_addTendon(spec, nullptr); mjs_setName(mj_tendon->element, prim.GetPath().GetAsString().c_str()); - mujoco::usd::SetUsdPrimPathUserValue(mj_tendon->element, prim.GetPath()); + SetUsdPrimPathUserValue(mj_tendon->element, prim.GetPath()); pxr::TfToken type; tendon.GetTypeAttr().Get(&type); @@ -963,25 +978,31 @@ void ParseMjcPhysicsTendon(mjSpec* spec, wrap_targets.size()); return; } - // Check that if we have >1 segments that the user has specified how much each segment - // contributes to the total segment length. + // Check that if we have >1 segments that the user has specified how much + // each segment contributes to the total segment length. if (!segments.empty() && divisors.empty()) { mju_warning( - "Spatial tendon %s has >1 segments (%d) but does not specify divisors, skipping.", - prim.GetPath().GetAsString().c_str(), *std::max_element(segments.begin(), segments.end()) + 1); + "Spatial tendon %s has >1 segments (%d) but does not specify " + "divisors, skipping.", + prim.GetPath().GetAsString().c_str(), + *std::max_element(segments.begin(), segments.end()) + 1); return; } // 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()) { + if (!side_site_indices.empty() && + side_site_indices.size() != wrap_targets.size()) { 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()); + "Spatial tendon %s has %lu sideSite indices but %lu wrap targets, " + "skipping.", + prim.GetPath().GetAsString().c_str(), side_site_indices.size(), + wrap_targets.size()); return; } if (!side_site_indices.empty() && side_site_paths.empty()) { mju_warning( - "Spatial tendon %s has %lu sideSite indices but no side sites, skipping.", + "Spatial tendon %s has %lu sideSite indices but no side sites, " + "skipping.", prim.GetPath().GetAsString().c_str(), side_site_indices.size()); return; } @@ -990,7 +1011,8 @@ void ParseMjcPhysicsTendon(mjSpec* spec, if (!coefs.empty() && coefs.size() != wrap_targets.size()) { mju_warning( "Spatial tendon %s has %lu coefs but %lu wrap targets, skipping.", - prim.GetPath().GetAsString().c_str(), coefs.size(), wrap_targets.size()); + prim.GetPath().GetAsString().c_str(), coefs.size(), + wrap_targets.size()); } } @@ -998,21 +1020,25 @@ void ParseMjcPhysicsTendon(mjSpec* spec, for (int i = 0; i < wrap_targets.size(); ++i) { auto wrap_target = wrap_targets[i]; auto wrap_prim = stage->GetPrimAtPath(wrap_target); - // Important to check site before Imageable here because some Imageable prims are sites. + // Important to check site before Imageable here because some Imageable + // prims are sites. if (!segments.empty()) { int segment = segments[i]; if (segment >= divisors.size()) { - mju_warning("Tendon %s has at least %d segments but only %lu divisors, skipping.", - prim.GetPath().GetAsString().c_str(), segment + 1, divisors.size()); + mju_warning( + "Tendon %s has at least %d segments but only %lu divisors, " + "skipping.", + prim.GetPath().GetAsString().c_str(), segment + 1, divisors.size()); return; } if (segment > last_segment) { mjsWrap* pulley_wrap = mjs_wrapPulley(mj_tendon, divisors[segment]); mjs_setString(pulley_wrap->info, ("Pulley between segments: " + - std::to_string(last_segment) + " and " + - std::to_string(segment)).c_str()); + std::to_string(last_segment) + + " and " + std::to_string(segment)) + .c_str()); } last_segment = segment; } @@ -1031,13 +1057,17 @@ void ParseMjcPhysicsTendon(mjSpec* spec, if (!side_site_indices.empty()) { int side_site_index = side_site_indices[i]; if (side_site_index >= side_site_paths.size()) { - mju_warning("Tendon %s has side site index %d but only %lu side sites, skipping.", - prim.GetPath().GetAsString().c_str(), side_site_index, side_site_paths.size()); + mju_warning( + "Tendon %s has side site index %d but only %lu side sites, " + "skipping.", + prim.GetPath().GetAsString().c_str(), side_site_index, + side_site_paths.size()); return; } side_site_name = side_site_paths[side_site_index].GetAsString(); } - wrap = mjs_wrapGeom(mj_tendon, wrap_target.GetAsString().c_str(), side_site_name.c_str()); + wrap = mjs_wrapGeom(mj_tendon, wrap_target.GetAsString().c_str(), + side_site_name.c_str()); } else { mju_warning("Tendon %s has an invalid wrap target type, skipping.", prim.GetPath().GetAsString().c_str()); @@ -1226,7 +1256,7 @@ void ParseMjcPhysicsActuator(mjSpec* spec, mjsActuator* mj_act = mjs_addActuator(spec, nullptr); mjs_setName(mj_act->element, prim.GetPath().GetAsString().c_str()); - mujoco::usd::SetUsdPrimPathUserValue(mj_act->element, prim.GetPath()); + SetUsdPrimPathUserValue(mj_act->element, prim.GetPath()); auto group_attr = tran.GetGroupAttr(); if (group_attr.HasAuthoredValue()) { @@ -1671,7 +1701,7 @@ void ParseUsdGeomGprim(mjSpec* spec, const pxr::UsdPrim& gprim, geom->contype = 0; geom->conaffinity = 0; - mujoco::usd::SetUsdPrimPathUserValue(geom->element, gprim.GetPath()); + SetUsdPrimPathUserValue(geom->element, gprim.GetPath()); ParseDisplayColorAndOpacity(gprim, geom); SetLocalPoseFromPrim(gprim, body_prim, geom, caches.xform_cache); @@ -1728,7 +1758,7 @@ void ParseUsdPhysicsCollider(mjSpec* spec, geom->contype = 1; geom->conaffinity = 1; - mujoco::usd::SetUsdPrimPathUserValue(geom->element, prim.GetPath()); + SetUsdPrimPathUserValue(geom->element, prim.GetPath()); if (prim.HasAPI()) { ParseMjcPhysicsCollisionAPI(geom, pxr::MjcPhysicsCollisionAPI(prim)); @@ -1810,7 +1840,7 @@ void ParseUsdPhysicsJoint(mjSpec* spec, const pxr::UsdPrim& prim, mjsBody* body, mj_joint->type = type; mjs_setName(mj_joint->element, prim.GetPath().GetAsString().c_str()); - mujoco::usd::SetUsdPrimPathUserValue(mj_joint->element, prim.GetPath()); + SetUsdPrimPathUserValue(mj_joint->element, prim.GetPath()); if (prim.IsA()) { pxr::UsdPhysicsRevoluteJoint revolute(prim); @@ -1901,7 +1931,7 @@ void ParseMjcPhysicsSite(mjSpec* spec, const pxr::MjcPhysicsSiteAPI& site_api, site_api.GetPrim().GetPath().GetAsString().c_str()); SetLocalPoseFromPrim(site_api.GetPrim(), parent_prim, site, xform_cache); - mujoco::usd::SetUsdPrimPathUserValue(site->element, prim.GetPath()); + SetUsdPrimPathUserValue(site->element, prim.GetPath()); auto group_attr = site_api.GetGroupAttr(); if (group_attr.HasAuthoredValue()) { @@ -1949,7 +1979,7 @@ void ParseMjcPhysicsKeyframe(mjSpec* spec, // If no time samples, we create a single keyframe. mjsKey* key = mjs_addKey(spec); - mujoco::usd::SetUsdPrimPathUserValue(key->element, prim.GetPath()); + SetUsdPrimPathUserValue(key->element, prim.GetPath()); mjs_setName(key->element, prim.GetName().GetString().c_str()); setKeyframeData(key, qpos_attr, &key->qpos); @@ -1966,7 +1996,7 @@ void ParseMjcPhysicsKeyframe(mjSpec* spec, for (double time : times) { mjsKey* key = mjs_addKey(spec); - mujoco::usd::SetUsdPrimPathUserValue(key->element, prim.GetPath()); + SetUsdPrimPathUserValue(key->element, prim.GetPath()); std::string key_name = prim.GetName().GetString() + "_" + std::to_string(keyframe_id++); @@ -1995,7 +2025,12 @@ mjsBody* ParseUsdPhysicsRigidbody( ParseUsdPhysicsMassAPIForBody(body, pxr::UsdPhysicsMassAPI(prim)); } - mujoco::usd::SetUsdPrimPathUserValue(body->element, prim.GetPath()); + // The value is a pointer to a newly allocated SdfPath, which will be deleted + // when the mjsElement is deleted. + const pxr::SdfPath* usd_primpath = new pxr::SdfPath(prim.GetPath()); + mjs_setUserValueWithCleanup( + body->element, kUsdPrimPathKey, usd_primpath, + [](const void* data) { delete static_cast(data); }); return body; } @@ -2080,49 +2115,45 @@ void PopulateSpecFromTree(pxr::UsdStageRefPtr stage, mjSpec* spec, child_node.get(), caches); } } -} // namespace usd -} // namespace mujoco -mjSpec* mj_parseUSDStage(const pxr::UsdStageRefPtr stage) { +mjSpec* ParseStage(const pxr::UsdStageRefPtr stage) { mjSpec* spec = mj_makeSpec(); - std::unique_ptr root = - mujoco::usd::BuildKinematicTree(stage); + std::unique_ptr root = BuildKinematicTree(stage); // First parse the physics scene and other root elements such as keyframes // and actuators. if (!root->physics_scene.IsEmpty()) { - mujoco::usd::ParseUsdPhysicsScene( - spec, pxr::UsdPhysicsScene::Get(stage, root->physics_scene)); + ParseUsdPhysicsScene(spec, + pxr::UsdPhysicsScene::Get(stage, root->physics_scene)); } else { // If there is no physics scene we still need to infer the gravity vector // from the stage up axis and units per meter metadata. - mujoco::usd::SetGravityAttributes(spec, stage); + SetGravityAttributes(spec, stage); } if (!root->keyframes.empty()) { for (const auto& keyframe : root->keyframes) { - mujoco::usd::ParseMjcPhysicsKeyframe( - spec, pxr::MjcPhysicsKeyframe::Get(stage, keyframe)); + ParseMjcPhysicsKeyframe(spec, + pxr::MjcPhysicsKeyframe::Get(stage, keyframe)); } } if (!root->actuators.empty()) { for (const auto& actuator : root->actuators) { - mujoco::usd::ParseMjcPhysicsActuator( - spec, pxr::MjcPhysicsActuator::Get(stage, actuator)); + ParseMjcPhysicsActuator(spec, + pxr::MjcPhysicsActuator::Get(stage, actuator)); } } if (!root->tendons.empty()) { for (const auto& tendon : root->tendons) { - mujoco::usd::ParseMjcPhysicsTendon( - spec, pxr::MjcPhysicsTendon::Get(stage, tendon)); + ParseMjcPhysicsTendon(spec, pxr::MjcPhysicsTendon::Get(stage, tendon)); } } // Set of caches to use for all queries when parsing. - mujoco::usd::UsdCaches caches; + UsdCaches caches; // Then populate the kinematic tree. PopulateSpecFromTree(stage, spec, /*parent_mj_body=*/nullptr, /*parent_node=*/nullptr, root.get(), caches); @@ -2130,8 +2161,27 @@ mjSpec* mj_parseUSDStage(const pxr::UsdStageRefPtr stage) { return spec; } -mjSpec* mj_parseUSD(const char* identifier, const mjVFS* vfs, char* error, - int error_sz) { - auto stage = pxr::UsdStage::Open(identifier); - return mj_parseUSDStage(stage); +namespace { +// load 2D +mjSpec* Decode(mjResource* resource, const mjVFS* vfs) { + auto stage = pxr::UsdStage::Open(resource->name); + return ParseStage(stage); +} + +int CanDecode(const mjResource* resource) { + std::string_view name(resource->name); + return name.ends_with(".usd") || name.ends_with(".usda") || + name.ends_with(".usdc") || name.ends_with(".usdz"); +} +} // namespace + +// clang-format off +mjPLUGIN_LIB_INIT { + mjpDecoder decoder; + mjp_defaultDecoder(&decoder); + decoder.content_type = "model/usd"; + decoder.extension = ".usd|.usda|.usdc|.usdz"; + decoder.decode = Decode; + decoder.can_decode = CanDecode; + mjp_registerDecoder(&decoder); } diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index 7dcf7fdc..4d360e0f 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -196,6 +196,7 @@ if(SIMULATE_BUILD_EXECUTABLE) target_link_libraries( simulate mujoco::usd::mjcf + usd_decoder_plugin ) endif() diff --git a/simulate/main.cc b/simulate/main.cc index c2d72d95..2eff3ffc 100644 --- a/simulate/main.cc +++ b/simulate/main.cc @@ -25,9 +25,6 @@ #include #include -#if defined(mjUSEUSD) -#include -#endif #include #include "glfw_adapter.h" #include "simulate.h" @@ -241,22 +238,26 @@ mjModel* LoadModel(const char* file, mj::Simulate& sim) { if (!mnew) { mju::strcpy_arr(loadError, "could not load binary model"); } -#if defined(mjUSEUSD) - } else if (extension == ".usda" || extension == ".usd" || - extension == ".usdc" || extension == ".usdz" ) { - mnew = mj_loadUSD(filename, nullptr, loadError, kErrorLength); -#endif - } else { + } else if (extension == ".xml") { mnew = mj_loadXML(filename, nullptr, loadError, kErrorLength); - - // remove trailing newline character from loadError - if (loadError[0]) { - int error_length = mju::strlen_arr(loadError); - if (loadError[error_length-1] == '\n') { - loadError[error_length-1] = '\0'; - } + } else { + mjSpec* spec = mj_parse(filename, nullptr, nullptr, loadError, kErrorLength); + if (!spec) { + mju::strcpy_arr(loadError, "could not parse model"); + } else { + mnew = mj_compile(spec, nullptr); + mj_deleteSpec(spec); } } + + // remove trailing newline character from loadError + if (loadError[0]) { + int error_length = mju::strlen_arr(loadError); + if (loadError[error_length-1] == '\n') { + loadError[error_length-1] = '\0'; + } + } + auto load_interval = mj::Simulate::Clock::now() - load_start; double load_seconds = Seconds(load_interval).count(); @@ -516,11 +517,6 @@ int main(int argc, char** argv) { // scan for libraries in the plugin directory to load additional plugins scanPluginLibraries(); -#if defined(mjUSEUSD) - // If USD is used, print the version. - std::printf("OpenUSD version v%d.%02d\n", PXR_MINOR_VERSION, PXR_PATCH_VERSION); -#endif - mjvCamera cam; mjv_defaultCamera(&cam); diff --git a/src/experimental/usd/CMakeLists.txt b/src/experimental/usd/CMakeLists.txt index fc571ff9..f2d01f0f 100644 --- a/src/experimental/usd/CMakeLists.txt +++ b/src/experimental/usd/CMakeLists.txt @@ -13,6 +13,7 @@ # limitations under the License. # --- Global Configuration --- + # Plugin target names (used for library and plugInfo.json) set(MJCF_PLUGIN_TARGET_NAME usdMjcf) set(MJC_PHYSICS_PLUGIN_TARGET_NAME mjcPhysics) @@ -147,12 +148,7 @@ target_include_directories(${MJC_PHYSICS_PLUGIN_TARGET_NAME} PUBLIC ## ----- mujoco USD utils ----- target_sources(mujoco PRIVATE - kinematic_tree.cc - kinematic_tree.h layer_sink.cc - material_parsing.cc - material_parsing.h - usd_to_mjspec.cc utils.cc writer.cc ) diff --git a/src/xml/xml_api.cc b/src/xml/xml_api.cc index c27d6755..5fab0475 100644 --- a/src/xml/xml_api.cc +++ b/src/xml/xml_api.cc @@ -31,12 +31,6 @@ #include "xml/xml_global.h" #include "xml/xml_native_reader.h" #include "xml/xml_util.h" -#if defined(mjUSEUSD) -#include -#include -#endif - - //---------------------------------- Functions ----------------------------------------------------- @@ -75,36 +69,6 @@ mjModel* mj_loadXML(const char* filename, const mjVFS* vfs, return m; } -#if defined(mjUSEUSD) -// parse USD file, compile it, and return low-level model. -// if vfs is not NULL, look up files in vfs before reading from disk -// error can be NULL; otherwise assumed to have size error_sz -mjModel* mj_loadUSD(const char* filename, const mjVFS* vfs, char* error, int error_sz) { - auto stage = pxr::UsdStage::Open(filename); - - if (stage == nullptr) { - mjCopyError(error, "Failed to load USD stage from file.", error_sz); - return nullptr; - } - - // Parse USD into mjSpec. - std::unique_ptr > spec( - mj_parseUSDStage(stage), - [](mjSpec* s) { - mj_deleteSpec(s); - }); - - // Compile new model. - mjModel* m = mj_compile(spec.get(), vfs); - if (!m) { - mjCopyError(error, mjs_getError(spec.get()), error_sz); - return nullptr; - } - - SetGlobalXmlSpec(spec.release()); - return m; -} -#endif // update XML data structures with info from low-level model, save as MJCF // returns 1 if successful, 0 otherwise diff --git a/src/xml/xml_native_reader.cc b/src/xml/xml_native_reader.cc index eb164dd3..e1485b88 100644 --- a/src/xml/xml_native_reader.cc +++ b/src/xml/xml_native_reader.cc @@ -45,9 +45,6 @@ #include "xml/xml_base.h" #include "xml/xml_util.h" #include "tinyxml2.h" -#ifdef mjUSEUSD -#include -#endif // mjUSEUSD namespace { using std::string; @@ -3446,21 +3443,11 @@ void mjXReader::Asset(XMLElement* section, const mjVFS* vfs) { ReadAttrTxt(elem, "content_type", content_type); // parse the child - mjSpec* child = nullptr; std::array error; auto filename = modelfiledir_ + ReadAttrFile(elem, "file", vfs).value(); -#ifdef mjUSEUSD - if (content_type == "text/usd") { - child = mj_parseUSD(filename.c_str(), vfs, error.data(), error.size()); - } else { -#endif // mjUSEUSD - child = mj_parse(filename.c_str(), content_type.c_str(), vfs, + mjSpec* child = mj_parse(filename.c_str(), content_type.c_str(), vfs, error.data(), error.size()); -#ifdef mjUSEUSD - } -#endif // mjUSEUSD - if (!child) { throw mjXError(elem, "could not parse model file with error: %s", error.data()); } diff --git a/test/experimental/usd/mjcPhysics/mjc_site_api_test.cc b/test/experimental/usd/mjcPhysics/mjc_site_api_test.cc index 360989f9..3eaa3b36 100644 --- a/test/experimental/usd/mjcPhysics/mjc_site_api_test.cc +++ b/test/experimental/usd/mjcPhysics/mjc_site_api_test.cc @@ -12,10 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include #include #include #include -#include #include #include "test/fixture.h" #include @@ -79,7 +79,9 @@ TEST_F(MjcSiteApiTest, TestApply) { auto box = pxr::UsdGeomCube::Define(stage, test_box_site_path); MjcPhysicsSiteAPI::Apply(box.GetPrim()); - mjSpec* spec = mj_parseUSDStage(stage); + std::array error; + mjSpec* spec = mj_parse(stage->GetRootLayer()->GetIdentifier().c_str(), + "model/usd", nullptr, error.data(), error.size()); mjModel* default_model = mj_compile(spec, nullptr); EXPECT_THAT(default_model, NotNull()) << mjs_getError(spec);