From 14972d249042ba6b7421bd5d75ea5bc8a47ec911 Mon Sep 17 00:00:00 2001 From: Sam Haves Date: Tue, 8 Jul 2025 10:03:02 -0700 Subject: [PATCH] Add mj_loadUSD function. Change simulate to use that instead of manual spec compilation. PiperOrigin-RevId: 780608874 Change-Id: I333238c756fe76b371518a7cdf9452b93da3540a --- CMakeLists.txt | 1 + include/mujoco/experimental/usd/usd.h | 6 +++ simulate/CMakeLists.txt | 3 +- simulate/main.cc | 35 ++----------- src/experimental/usd/CMakeLists.txt | 17 ++----- src/experimental/usd/layer_sink.cc | 3 +- src/xml/xml_api.cc | 33 +++++++++++++ test/xml/testdata/simple_hinge.usda | 71 +++++++++++++++++++++++++++ 8 files changed, 121 insertions(+), 48 deletions(-) create mode 100644 test/xml/testdata/simple_hinge.usda diff --git a/CMakeLists.txt b/CMakeLists.txt index f01d420d..a4794196 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -202,6 +202,7 @@ if(MUJOCO_BUILD_EXAMPLES) endif() if(MUJOCO_WITH_USD) + target_compile_definitions(mujoco PUBLIC mjUSEUSD) add_subdirectory(src/experimental/usd) endif() diff --git a/include/mujoco/experimental/usd/usd.h b/include/mujoco/experimental/usd/usd.h index f75b70ad..66023708 100644 --- a/include/mujoco/experimental/usd/usd.h +++ b/include/mujoco/experimental/usd/usd.h @@ -18,6 +18,12 @@ #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 stage, this function will do a best effort conversion to // mjSpec. // diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index 808de669..bd94acc3 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -177,7 +177,6 @@ if(SIMULATE_BUILD_EXECUTABLE) if (MUJOCO_WITH_USD) target_link_libraries( simulate - mujoco::usd mujoco::usd::mjcf ) endif() @@ -249,7 +248,7 @@ if(SIMULATE_BUILD_EXECUTABLE) ) if (MUJOCO_WITH_USD) - target_compile_definitions(simulate PUBLIC SIMULATE_WITH_USD) + target_compile_definitions(simulate PUBLIC mjUSEUSD) # Add support to RPATH for the samples. target_add_rpath( diff --git a/simulate/main.cc b/simulate/main.cc index e3d42a03..afe8b720 100644 --- a/simulate/main.cc +++ b/simulate/main.cc @@ -25,11 +25,8 @@ #include #include -#if defined(SIMULATE_WITH_USD) +#if defined(mjUSEUSD) #include -#include -#include -#include #endif #include #include "glfw_adapter.h" @@ -244,34 +241,10 @@ mjModel* LoadModel(const char* file, mj::Simulate& sim) { if (!mnew) { mju::strcpy_arr(loadError, "could not load binary model"); } -#if defined(SIMULATE_WITH_USD) +#if defined(mjUSEUSD) } else if (extension == ".usda" || extension == ".usd" || extension == ".usdc" || extension == ".usdz" ) { - auto stage = pxr::UsdStage::Open(filename); - if (!stage) { - mju::strcpy_arr(loadError, "could not open USD stage"); - } else { - mjSpec* spec = mj_parseUSDStage(stage); - if (!spec) { - mju::strcpy_arr(loadError, "could not parse USD stage to mjSpec"); - } else { - mjModel* model = mj_compile(spec, nullptr); - if (!model) { - mju::strcpy_arr(loadError, - "could not compile USD parsed mjSpec to mjModel:\n"); - mju::strcat_arr(loadError, mjs_getError(spec)); - } else { - // handle compile warning - if (mjs_isWarning(spec)) { - mju::strcpy_arr( - loadError, - "warning while compiling USD parsed mjSpec to mjModel:\n"); - mju::strcat_arr(loadError, mjs_getError(spec)); - } - } - mnew = model; - } - } + mnew = mj_loadUSD(filename, nullptr, loadError, kErrorLength); #endif } else { mnew = mj_loadXML(filename, nullptr, loadError, kErrorLength); @@ -537,7 +510,7 @@ int main(int argc, char** argv) { // scan for libraries in the plugin directory to load additional plugins scanPluginLibraries(); -#if defined(SIMULATE_WITH_USD) +#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 diff --git a/src/experimental/usd/CMakeLists.txt b/src/experimental/usd/CMakeLists.txt index 2a9f7fce..c52131da 100644 --- a/src/experimental/usd/CMakeLists.txt +++ b/src/experimental/usd/CMakeLists.txt @@ -16,7 +16,6 @@ # Plugin target names (used for library and plugInfo.json) set(MJCF_PLUGIN_TARGET_NAME usdMjcf) set(MJC_PHYSICS_PLUGIN_TARGET_NAME mjcPhysics) -set(MJ_USD_TARGET_NAME mj_usd) # Installation directory for USD plugins set(MJ_USD_INSTALL_DIR_LIB ${CMAKE_INSTALL_LIBDIR}/mujocoUsd) @@ -187,14 +186,12 @@ set_target_properties(${MJC_PHYSICS_PLUGIN_TARGET_NAME} PROPERTIES ${DEFAULT_CXX_VISIBILITY_PROPS} ) target_include_directories(${MJC_PHYSICS_PLUGIN_TARGET_NAME} PUBLIC - "${CMAKE_CURRENT_SOURCE_DIR}/mjcPhysics" + $ ) ## mj_usd -add_library(${MJ_USD_TARGET_NAME} SHARED) -add_library(mujoco::usd ALIAS ${MJ_USD_TARGET_NAME}) -target_sources(${MJ_USD_TARGET_NAME} PRIVATE +target_sources(mujoco PRIVATE kinematic_tree.cc kinematic_tree.h layer_sink.cc @@ -212,8 +209,6 @@ if (USD_DIR) target_link_libraries(${MJC_PHYSICS_PLUGIN_TARGET_NAME} PRIVATE ${OPENUSD_CORE_LIBS} - mujoco - tinyxml2 ) target_link_libraries(${MJCF_PLUGIN_TARGET_NAME} PRIVATE @@ -223,10 +218,8 @@ if (USD_DIR) ${MJC_PHYSICS_PLUGIN_TARGET_NAME} ) - target_link_libraries(${MJ_USD_TARGET_NAME} PUBLIC + target_link_libraries(mujoco PUBLIC ${OPENUSD_CORE_LIBS} - mujoco - tinyxml2 ${MJC_PHYSICS_PLUGIN_TARGET_NAME} ) elseif (HOUDINI_HFS_DIR) @@ -237,11 +230,9 @@ elseif (HOUDINI_HFS_DIR) # so we need to manually link all the required libraries. configure_houdini_target(${MJC_PHYSICS_PLUGIN_TARGET_NAME} PRIVATE PRIVATE) configure_houdini_target(${MJCF_PLUGIN_TARGET_NAME} PRIVATE PRIVATE) - configure_houdini_target(${MJ_USD_TARGET_NAME} PUBLIC PUBLIC) # Re-add inter-plugin dependencies after Houdini-specific linking target_link_libraries(${MJCF_PLUGIN_TARGET_NAME} PRIVATE ${MJC_PHYSICS_PLUGIN_TARGET_NAME}) - target_link_libraries(${MJ_USD_TARGET_NAME} PUBLIC ${MJC_PHYSICS_PLUGIN_TARGET_NAME}) endif() @@ -272,6 +263,6 @@ install(FILES install(TARGETS ${MJCF_PLUGIN_TARGET_NAME} ${MJC_PHYSICS_PLUGIN_TARGET_NAME} - ${MJ_USD_TARGET_NAME} + EXPORT ${PROJECT_NAME} LIBRARY DESTINATION ${MJ_USD_INSTALL_DIR_LIB} ) diff --git a/src/experimental/usd/layer_sink.cc b/src/experimental/usd/layer_sink.cc index 2ae8d55a..5a7dfb24 100644 --- a/src/experimental/usd/layer_sink.cc +++ b/src/experimental/usd/layer_sink.cc @@ -41,8 +41,7 @@ PXR_NAMESPACE_OPEN_SCOPE TF_DEFINE_PRIVATE_TOKENS(_layer_sink_tokens, - ((xformOpTransform, - "xformOp:transform:mujoco"))(Xform)); + ((xformOpTransform, "xformOp:transform:mujoco"))); PXR_NAMESPACE_CLOSE_SCOPE namespace { diff --git a/src/xml/xml_api.cc b/src/xml/xml_api.cc index b289adb3..b9678eae 100644 --- a/src/xml/xml_api.cc +++ b/src/xml/xml_api.cc @@ -34,6 +34,10 @@ #include "xml/xml.h" #include "xml/xml_native_reader.h" #include "xml/xml_util.h" +#if defined(mjUSEUSD) +#include +#include +#endif //---------------------------------- Globals ------------------------------------------------------- @@ -126,7 +130,36 @@ 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; + } + + GetGlobalModel().Set(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/test/xml/testdata/simple_hinge.usda b/test/xml/testdata/simple_hinge.usda new file mode 100644 index 00000000..9027c91e --- /dev/null +++ b/test/xml/testdata/simple_hinge.usda @@ -0,0 +1,71 @@ +#usda 1.0 +( + endTimeCode = 1 + framesPerSecond = 24 + metersPerUnit = 1 + startTimeCode = 1 + timeCodesPerSecond = 24 + upAxis = "Y" +) + +def Xform "World" +{ + def Xform "Axle" ( + prepend apiSchemas = ["PhysicsRigidBodyAPI", "PhysicsArticulationRootAPI"] + ) + { + matrix4d xformOp:transform:set_initial_pos = ( (1, 0, 0, 0), (0, 0.9999197811020019, -0.012666150201413267, 0), (0, 0.012666150201413267, 0.9999197811020019, 0), (0, 0.012296309694647789, 0.970723032951355, 1) ) + uniform token[] xformOpOrder = ["xformOp:transform:set_initial_pos"] + + def Xform "Rod" ( + prepend apiSchemas = ["PhysicsRigidBodyAPI"] + ) + { + matrix4d xformOp:transform:set_initial_rod_pos = ( (1, 0, 0, 0), (0, 0.3255001489278845, 0.9455419890453967, 0), (0, -0.9455419890453967, 0.3255001489278845, 0), (0, 0.47277099452269833, -0.1627500744639423, 1) ) + uniform token[] xformOpOrder = ["xformOp:transform:set_initial_rod_pos"] + + def Cylinder "Geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + token axis = "Z" + float3[] extent = [(-0.01, -0.01, -0.5), (0.01, 0.01, 0.5)] + double height = 1 + double radius = 0.01 + matrix4d xformOp:transform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, -0.03546595561524429, 1) ) + uniform token[] xformOpOrder = ["xformOp:transform"] + } + + def PhysicsRevoluteJoint "HingeJoint" ( + prepend apiSchemas = ["PhysicsActuatorAPI"] + ) + { + rel physics:body0 = + rel physics:body1 = + point3f physics:localPos0 = (0, 0, 0) + point3f physics:localPos1 = (0, 0, 0.5) + quatf physics:localRot0 = (0, 1, 0, 0) + quatf physics:localRot1 = (0, 1, 0, 0) + } + } + + def Cylinder "Geometry" ( + prepend apiSchemas = ["PhysicsCollisionAPI"] + ) + { + token axis = "Z" + float3[] extent = [(-0.01, -0.01, -0.1), (0.01, 0.01, 0.1)] + double height = 0.2 + double radius = 0.01 + matrix4d xformOp:transform = ( (0, 0, -1, 0), (0, 1, -0, 0), (1, 0, -0, 0), (0, 0, -0, 1) ) + uniform token[] xformOpOrder = ["xformOp:transform"] + } + + def PhysicsFixedJoint "FixedJoint" + { + rel physics:body0 = None + rel physics:body1 = + } + } +} +