diff --git a/.github/workflows/build_steps.sh b/.github/workflows/build_steps.sh index 066d6949..9a9164be 100755 --- a/.github/workflows/build_steps.sh +++ b/.github/workflows/build_steps.sh @@ -222,7 +222,7 @@ package_wasm() { cp wasm/package.npm.json wasm/dist/package.json cp wasm/README.md wasm/dist/README.md - VERSION=${GITHUB_REF#refs/tags/} + VERSION="${VERSION:-${GITHUB_REF#refs/tags/}}" npm --prefix wasm/dist version "${VERSION}" --no-git-tag-version echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ~/.npmrc diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml index 33c5b7c1..297c0491 100644 --- a/.github/workflows/publish-wasm.yml +++ b/.github/workflows/publish-wasm.yml @@ -5,10 +5,25 @@ on: tags: - '[0-9]*.[0-9]*.[0-9]*' + workflow_dispatch: + inputs: + tag_version: + description: 'Release tag version to deploy' + required: true + default: '0.0.0' + +permissions: + id-token: write + contents: read + jobs: publish: name: Publish MuJoCo WASM package runs-on: ubuntu-24.04 + + env: + VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_version || github.ref_name }} + steps: - uses: actions/checkout@v3 @@ -32,5 +47,5 @@ jobs: - name: Package WASM bindings env: NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} - GITHUB_REF: ${{ github.ref }} + VERSION: ${{ env.VERSION }} run: bash ./.github/workflows/build_steps.sh package_wasm diff --git a/CMakeLists.txt b/CMakeLists.txt index 6c1747f3..f79153c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco - VERSION 3.6.0 + VERSION 3.7.0 DESCRIPTION "MuJoCo Physics Simulator" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/dist/mujoco.rc b/dist/mujoco.rc index 2e73e1d9..25f2e83e 100644 --- a/dist/mujoco.rc +++ b/dist/mujoco.rc @@ -1,6 +1,6 @@ 1 VERSIONINFO -FILEVERSION 3,6,0,0 -PRODUCTVERSION 3,6,0,0 +FILEVERSION 3,7,0,0 +PRODUCTVERSION 3,7,0,0 FILEOS 0x4 FILETYPE 0x1 { @@ -9,9 +9,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.6.0" + VALUE "ProductVersion", "3.7.0" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.6.0" + VALUE "FileVersion", "3.7.0" VALUE "InternalName", "mujoco.dll" VALUE "OriginalFilename", "mujoco.dll" VALUE "CompanyName", "Google DeepMind" diff --git a/dist/simulate.rc b/dist/simulate.rc index 011ae7e9..0626bc62 100644 --- a/dist/simulate.rc +++ b/dist/simulate.rc @@ -1,8 +1,8 @@ MUJOCO ICON "mujoco.ico" 1 VERSIONINFO -FILEVERSION 3,6,0,0 -PRODUCTVERSION 3,6,0,0 +FILEVERSION 3,7,0,0 +PRODUCTVERSION 3,7,0,0 FILEOS 0x4 FILETYPE 0x1 { @@ -11,9 +11,9 @@ FILETYPE 0x1 BLOCK "040904b0" { VALUE "ProductName", "MuJoCo" - VALUE "ProductVersion", "3.6.0" + VALUE "ProductVersion", "3.7.0" VALUE "FileDescription", "MuJoCo" - VALUE "FileVersion", "3.6.0" + VALUE "FileVersion", "3.7.0" VALUE "InternalName", "simulate.exe" VALUE "OriginalFilename", "simulate.exe" VALUE "CompanyName", "Google DeepMind" diff --git a/doc/APIreference/APIglobals.rst b/doc/APIreference/APIglobals.rst index 516edbed..926868fd 100644 --- a/doc/APIreference/APIglobals.rst +++ b/doc/APIreference/APIglobals.rst @@ -388,7 +388,7 @@ Defined in `mujoco.h `__. Basic Usage ----------- +----------- Rendering or raycasting requires a :class:`mjw.RenderContext ` which contains BVH structures, rendering specific fields, and output buffers. diff --git a/doc/mjx.rst b/doc/mjx.rst index d3d77d16..7d81c6bc 100644 --- a/doc/mjx.rst +++ b/doc/mjx.rst @@ -119,7 +119,7 @@ Notice that we pass two extra arguments to ``mjx.make_data``: * ``naconmax`` defines the maximum number of contacts for all worlds combined. * ``njmax`` defines the maximum number of constraints per world. If you are developing a new scene, these parameters - should be tuned by loading them in the :ref:`viewer ` and increasing the values accordingly as overflows + should be tuned by loading them in the :ref:`viewer ` and increasing the values accordingly as overflows occur. Scale ``naconmax`` by the number of environments you'll eventually need in a ``jax.vmap``! Contacts diff --git a/doc/unity.rst b/doc/unity.rst index 3afede84..49d8177a 100644 --- a/doc/unity.rst +++ b/doc/unity.rst @@ -37,14 +37,14 @@ _____ The MuJoCo app needs to be run at least once before the native library can be used, in order to register the library as a trusted binary. Then, copy the dynamic library file from -``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.6.0.dylib`` (it can be +``/Applications/MuJoCo.app/Contents/Frameworks/mujoco.framework/Versions/Current/libmujoco.3.7.0.dylib`` (it can be found by browsing the contents of ``MuJoCo.app``) and rename it as ``mujoco.dylib``. Linux _____ Expand the ``tar.gz`` archive to ``~/.mujoco``. Then copy the dynamic library from -``~/.mujoco/mujoco-3.6.0/lib/libmujoco.so.3.6.0`` and rename it as ``libmujoco.so``. +``~/.mujoco/mujoco-3.7.0/lib/libmujoco.so.3.7.0`` and rename it as ``libmujoco.so``. Windows _______ diff --git a/include/mujoco/mujoco.h b/include/mujoco/mujoco.h index 4578bc85..f3cfef95 100644 --- a/include/mujoco/mujoco.h +++ b/include/mujoco/mujoco.h @@ -16,7 +16,7 @@ #define MUJOCO_MUJOCO_H_ // header version; should match the library version as returned by mj_version() -#define mjVERSION_HEADER 3006000 +#define mjVERSION_HEADER 3007000 // needed to define size_t, fabs and log10 #include @@ -609,8 +609,8 @@ MJAPI void mj_objectAcceleration(const mjModel* m, const mjData* d, // Return smallest signed distance between two geoms and optionally segment from geom1 to geom2. // Nullable: fromto -MJAPI mjtNum mj_geomDistance(const mjModel* m, const mjData* d, int geom1, int geom2, - mjtNum distmax, mjtNum fromto[6]); +MJAPI mjtNum mj_geomDistance(const mjModel* m, mjData* d, int geom1, int geom2, mjtNum distmax, + mjtNum fromto[6]); // Extract 6D force:torque given contact id, in the contact frame. MJAPI void mj_contactForce(const mjModel* m, const mjData* d, int id, mjtNum result[6]); diff --git a/mjx/pyproject.toml b/mjx/pyproject.toml index 6d6fe4a9..ff7133cd 100644 --- a/mjx/pyproject.toml +++ b/mjx/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name="mujoco-mjx" -version = "3.6.0" +version = "3.7.0" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -29,7 +29,7 @@ dependencies = [ "etils[epath]", "jax", "jaxlib", - "mujoco>=3.6.0.dev0", + "mujoco>=3.7.0.dev0", "scipy", "trimesh", ] @@ -45,9 +45,9 @@ mjx-viewer = "mujoco.mjx.viewer:main" [project.urls] Homepage = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Documentation = "https://mujoco.readthedocs.io/en/3.6.0" +Documentation = "https://mujoco.readthedocs.io/en/3.7.0" Repository = "https://github.com/google-deepmind/mujoco/tree/main/mjx" -Changelog = "https://mujoco.readthedocs.io/en/3.6.0/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.7.0/changelog.html" [tool.isort] force_single_line = true diff --git a/python/mujoco/CMakeLists.txt b/python/mujoco/CMakeLists.txt index ca2fd5a0..9db950bb 100644 --- a/python/mujoco/CMakeLists.txt +++ b/python/mujoco/CMakeLists.txt @@ -86,7 +86,7 @@ if(NOT TARGET mujoco) if(MUJOCO_FRAMEWORK) message("MuJoCo framework is at ${MUJOCO_FRAMEWORK}/mujoco.framework") set(MUJOCO_LIBRARY - ${MUJOCO_FRAMEWORK}/mujoco.framework/Versions/A/libmujoco.3.6.0.dylib + ${MUJOCO_FRAMEWORK}/mujoco.framework/Versions/A/libmujoco.3.7.0.dylib ) target_compile_options(mujoco INTERFACE -F${MUJOCO_FRAMEWORK}) endif() @@ -94,7 +94,7 @@ if(NOT TARGET mujoco) if(NOT MUJOCO_FRAMEWORK) find_library( - MUJOCO_LIBRARY mujoco mujoco.3.6.0 HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED + MUJOCO_LIBRARY mujoco mujoco.3.7.0 HINTS ${MUJOCO_LIBRARY_DIR} REQUIRED ) find_path(MUJOCO_INCLUDE mujoco/mujoco.h HINTS ${MUJOCO_INCLUDE_DIR} REQUIRED) message("MuJoCo is at ${MUJOCO_LIBRARY}") diff --git a/python/mujoco/functions.cc b/python/mujoco/functions.cc index 776e8ae4..d6e0c724 100644 --- a/python/mujoco/functions.cc +++ b/python/mujoco/functions.cc @@ -663,7 +663,7 @@ PYBIND11_MODULE(_functions, pymodule) { Def(pymodule); Def( pymodule, - [](const raw::MjModel* m, const raw::MjData* d, + [](const raw::MjModel* m, raw::MjData* d, int geom1, int geom2, mjtNum distmax, std::optional> fromto) { if (fromto.has_value() && fromto->size() != 6) { diff --git a/python/mujoco/introspect/functions.py b/python/mujoco/introspect/functions.py index a1e2d806..bf608c26 100644 --- a/python/mujoco/introspect/functions.py +++ b/python/mujoco/introspect/functions.py @@ -3626,7 +3626,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([ FunctionParameterDecl( name='d', type=PointerType( - inner_type=ValueType(name='mjData', is_const=True), + inner_type=ValueType(name='mjData'), ), ), FunctionParameterDecl( diff --git a/python/mujoco/mjpython/Info.plist b/python/mujoco/mjpython/Info.plist index 184bdd4c..2cf7f5ed 100644 --- a/python/mujoco/mjpython/Info.plist +++ b/python/mujoco/mjpython/Info.plist @@ -7,13 +7,13 @@ CFBundleIdentifier org.mujoco.mjpython CFBundleVersion - 3.6.0 + 3.7.0 CFBundleGetInfoString - 3.6.0 + 3.7.0 CFBundleLongVersionString - 3.6.0 + 3.7.0 CFBundleShortVersionString - 3.6.0 + 3.7.0 CFBundleExecutable mjpython CFBundleIconFile diff --git a/python/pyproject.toml b/python/pyproject.toml index 32f9a089..157d9898 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "mujoco" -version = "3.6.0" +version = "3.7.0" authors = [ {name = "Google DeepMind", email = "mujoco@deepmind.com"}, ] @@ -34,9 +34,9 @@ dynamic = ["readme", "scripts"] [project.urls] Homepage = "https://github.com/google-deepmind/mujoco" -Documentation = "https://mujoco.readthedocs.io/en/3.6.0" +Documentation = "https://mujoco.readthedocs.io/en/3.7.0" Repository = "https://github.com/google-deepmind/mujoco" -Changelog = "https://mujoco.readthedocs.io/en/3.6.0/changelog.html" +Changelog = "https://mujoco.readthedocs.io/en/3.7.0/changelog.html" [tool.setuptools] include-package-data = false diff --git a/sample/CMakeLists.txt b/sample/CMakeLists.txt index 7280cfdb..09c20cc0 100644 --- a/sample/CMakeLists.txt +++ b/sample/CMakeLists.txt @@ -24,7 +24,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON) project( mujoco_samples - VERSION 3.6.0 + VERSION 3.7.0 DESCRIPTION "MuJoCo samples binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/simulate/CMakeLists.txt b/simulate/CMakeLists.txt index 5d804c6c..640ac5ad 100644 --- a/simulate/CMakeLists.txt +++ b/simulate/CMakeLists.txt @@ -29,7 +29,7 @@ set(MUJOCO_DEP_VERSION_lodepng project( mujoco_simulate - VERSION 3.6.0 + VERSION 3.7.0 DESCRIPTION "MuJoCo simulate binaries" HOMEPAGE_URL "https://mujoco.org" ) diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c index fc0dab5e..0e2dcd35 100644 --- a/src/engine/engine_support.c +++ b/src/engine/engine_support.c @@ -43,8 +43,8 @@ //-------------------------- Constants ------------------------------------------------------------- - #define mjVERSION 3006000 -#define mjVERSIONSTRING "3.6.0" + #define mjVERSION 3007000 +#define mjVERSIONSTRING "3.7.0" // names of disable flags const char* mjDISABLESTRING[mjNDISABLE] = { @@ -524,7 +524,7 @@ void mj_xfrcAccumulate(const mjModel* m, mjData* d, mjtNum* qfrc) { //-------------------------- miscellaneous --------------------------------------------------------- // returns the smallest distance between two geoms (using nativeccd) -static mjtNum mj_geomDistanceCCD(const mjModel* m, const mjData* d, int g1, int g2, +static mjtNum mj_geomDistanceCCD(const mjModel* m, mjData* d, int g1, int g2, mjtNum distmax, mjtNum fromto[6]) { mjCCDConfig config; mjCCDStatus status; @@ -553,7 +553,7 @@ static mjtNum mj_geomDistanceCCD(const mjModel* m, const mjData* d, int g1, int // returns the smallest distance between two geoms -mjtNum mj_geomDistance(const mjModel* m, const mjData* d, int geom1, int geom2, mjtNum distmax, +mjtNum mj_geomDistance(const mjModel* m, mjData* d, int geom1, int geom2, mjtNum distmax, mjtNum fromto[6]) { mjContact con[mjMAXCONPAIR]; mjtNum dist = distmax; diff --git a/src/engine/engine_support.h b/src/engine/engine_support.h index 04125ed3..7e1fd911 100644 --- a/src/engine/engine_support.h +++ b/src/engine/engine_support.h @@ -86,8 +86,8 @@ void mj_xfrcAccumulate(const mjModel* m, mjData* d, mjtNum* qfrc); //-------------------------- miscellaneous --------------------------------------------------------- // returns the smallest distance between two geoms -MJAPI mjtNum mj_geomDistance(const mjModel* m, const mjData* d, int geom1, int geom2, - mjtNum distmax, mjtNum fromto[6]); +MJAPI mjtNum mj_geomDistance(const mjModel* m, mjData* d, int geom1, int geom2, mjtNum distmax, + mjtNum fromto[6]); // compute velocity by finite-differencing two positions MJAPI void mj_differentiatePos(const mjModel* m, mjtNum* qvel, mjtNum dt, diff --git a/src/experimental/filament/filament/filament_context.cc b/src/experimental/filament/filament/filament_context.cc index 844d3cf1..e514e1d7 100644 --- a/src/experimental/filament/filament/filament_context.cc +++ b/src/experimental/filament/filament/filament_context.cc @@ -140,6 +140,19 @@ void FilamentContext::Render(const mjrRect& viewport, const mjvScene* scene, mju_error("Unexpected context."); } + // If we're rendering to the window, and the window size has changed, we need + // to reacquire the swap chain. + if (scene_swap_chain_target_ == kWindowSwapChain && + (viewport.width != window_width_ || viewport.height != window_height_)) { + if (window_width_ != 0 && window_height_ != 0) { + engine_->flushAndWait(); + engine_->destroy(window_swap_chain_); + window_swap_chain_ = engine_->createSwapChain(config_.native_window); + } + window_width_ = viewport.width; + window_height_ = viewport.height; + } + scene_view_->SetViewport(viewport); scene_view_->UpdateScene(con, scene); // Update the UX renderable entity after processing the scene in case there diff --git a/src/experimental/filament/filament/filament_context.h b/src/experimental/filament/filament/filament_context.h index dc460d9f..d0e7a75e 100644 --- a/src/experimental/filament/filament/filament_context.h +++ b/src/experimental/filament/filament/filament_context.h @@ -89,6 +89,8 @@ class FilamentContext { std::unique_ptr object_manager_; std::unique_ptr scene_view_; std::unique_ptr gui_view_; + int window_width_ = 0; + int window_height_ = 0; }; } // namespace mujoco diff --git a/src/experimental/mjz/mjz_decoder.cc b/src/experimental/mjz/mjz_decoder.cc index 59ec7656..409607e7 100644 --- a/src/experimental/mjz/mjz_decoder.cc +++ b/src/experimental/mjz/mjz_decoder.cc @@ -74,11 +74,11 @@ class ZipArchiveProvider : public mjpResourceProvider { // Look for the root XML model in the archive. First look for an XML file // with the same name as the archive itself. Failing that, look for an XML - // file with the same name including the parent directory. + // file within a subdirectory with the same name as the archive. const std::filesystem::path path(name_); root_model_ = (path / path.stem()).string() + ".xml"; if (!Contains(root_model_)) { - root_model_ = (path / path.parent_path() / path.stem()).string() + ".xml"; + root_model_ = (path / path.stem() / path.stem()).string() + ".xml"; if (!Contains(root_model_)) { SetError(error, error_sz, "Zip error: no root XML file found."); return; diff --git a/src/experimental/platform/gui.cc b/src/experimental/platform/gui.cc index a9ae3699..f9a4c5b8 100644 --- a/src/experimental/platform/gui.cc +++ b/src/experimental/platform/gui.cc @@ -26,6 +26,7 @@ #include #include #include "experimental/platform/helpers.h" +#include "experimental/platform/interaction.h" #include "experimental/platform/imgui_widgets.h" namespace mujoco::platform { @@ -41,70 +42,70 @@ void SetupTheme(GuiTheme theme) { ImVec4* c = s.Colors; if (theme == GuiTheme::kDark) { ImGui::StyleColorsDark(&s); - c[ImGuiCol_Text] = ImVec4(1.00, 1.00, 1.00, 1.00); - c[ImGuiCol_TextDisabled] = ImVec4(0.40, 0.40, 0.40, 1.00); - c[ImGuiCol_ChildBg] = ImVec4(0.25, 0.25, 0.25, 1.00); - c[ImGuiCol_WindowBg] = ImVec4(0.25, 0.25, 0.25, 1.00); - c[ImGuiCol_PopupBg] = ImVec4(0.25, 0.25, 0.25, 1.00); - c[ImGuiCol_Border] = ImVec4(0.12, 0.12, 0.12, 0.71); - c[ImGuiCol_BorderShadow] = ImVec4(1.00, 1.00, 1.00, 0.06); - c[ImGuiCol_FrameBg] = ImVec4(0.42, 0.42, 0.42, 0.54); - c[ImGuiCol_FrameBgHovered] = ImVec4(0.42, 0.42, 0.42, 0.40); - c[ImGuiCol_FrameBgActive] = ImVec4(0.56, 0.56, 0.56, 0.67); - c[ImGuiCol_TitleBg] = ImVec4(0.19, 0.19, 0.19, 1.00); - c[ImGuiCol_TitleBgActive] = ImVec4(0.22, 0.22, 0.22, 1.00); - c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.17, 0.17, 0.17, 0.90); - c[ImGuiCol_MenuBarBg] = ImVec4(0.34, 0.34, 0.34, 1.00); - c[ImGuiCol_ScrollbarBg] = ImVec4(0.24, 0.24, 0.24, 0.53); - c[ImGuiCol_ScrollbarGrab] = ImVec4(0.41, 0.41, 0.41, 1.00); - c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.52, 0.52, 0.52, 1.00); - c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.76, 0.76, 0.76, 1.00); - c[ImGuiCol_CheckMark] = ImVec4(0.65, 0.65, 0.65, 1.00); - c[ImGuiCol_SliderGrab] = ImVec4(0.52, 0.52, 0.52, 1.00); - c[ImGuiCol_SliderGrabActive] = ImVec4(0.64, 0.64, 0.64, 1.00); - c[ImGuiCol_Button] = ImVec4(0.54, 0.54, 0.54, 0.35); - c[ImGuiCol_ButtonHovered] = ImVec4(0.52, 0.52, 0.52, 0.59); - c[ImGuiCol_ButtonActive] = ImVec4(0.76, 0.76, 0.76, 1.00); - c[ImGuiCol_Header] = ImVec4(0.38, 0.38, 0.38, 1.00); - c[ImGuiCol_HeaderHovered] = ImVec4(0.47, 0.47, 0.47, 1.00); - c[ImGuiCol_HeaderActive] = ImVec4(0.76, 0.76, 0.76, 0.77); - c[ImGuiCol_Separator] = ImVec4(0.00, 0.00, 0.00, 0.18); - c[ImGuiCol_SeparatorHovered] = ImVec4(0.70, 0.67, 0.60, 0.29); - c[ImGuiCol_SeparatorActive] = ImVec4(0.70, 0.67, 0.60, 0.67); - c[ImGuiCol_ResizeGrip] = ImVec4(0.26, 0.59, 0.98, 0.25); - c[ImGuiCol_ResizeGripHovered] = ImVec4(0.26, 0.59, 0.98, 0.67); - c[ImGuiCol_ResizeGripActive] = ImVec4(0.26, 0.59, 0.98, 0.95); - c[ImGuiCol_PlotLines] = ImVec4(0.61, 0.61, 0.61, 1.00); - c[ImGuiCol_PlotLinesHovered] = ImVec4(1.00, 0.43, 0.35, 1.00); - c[ImGuiCol_PlotHistogram] = ImVec4(0.90, 0.70, 0.00, 1.00); - c[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00, 0.60, 0.00, 1.00); - c[ImGuiCol_TextSelectedBg] = ImVec4(0.73, 0.73, 0.73, 0.35); - c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80, 0.80, 0.80, 0.35); - c[ImGuiCol_DragDropTarget] = ImVec4(1.00, 1.00, 0.00, 0.90); - c[ImGuiCol_NavHighlight] = ImVec4(0.26, 0.59, 0.98, 1.00); - c[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00, 1.00, 1.00, 0.70); - c[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80, 0.80, 0.80, 0.20); - c[ImGuiCol_DockingEmptyBg] = ImVec4(0.38, 0.38, 0.38, 1.00); - c[ImGuiCol_Tab] = ImVec4(0.25, 0.25, 0.25, 1.00); - c[ImGuiCol_TabHovered] = ImVec4(0.40, 0.40, 0.40, 1.00); - c[ImGuiCol_TabActive] = ImVec4(0.33, 0.33, 0.33, 1.00); - c[ImGuiCol_TabUnfocused] = ImVec4(0.25, 0.25, 0.25, 1.00); - c[ImGuiCol_TabUnfocusedActive] = ImVec4(0.33, 0.33, 0.33, 1.00); - c[ImGuiCol_DockingPreview] = ImVec4(0.85, 0.85, 0.85, 0.28); + c[ImGuiCol_Text] = ImVec4(1.00, 1.00, 1.00, 1.00); + c[ImGuiCol_TextDisabled] = ImVec4(0.40, 0.40, 0.40, 1.00); + c[ImGuiCol_ChildBg] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_WindowBg] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_PopupBg] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_Border] = ImVec4(0.12, 0.12, 0.12, 0.71); + c[ImGuiCol_BorderShadow] = ImVec4(1.00, 1.00, 1.00, 0.06); + c[ImGuiCol_FrameBg] = ImVec4(0.42, 0.42, 0.42, 0.54); + c[ImGuiCol_FrameBgHovered] = ImVec4(0.42, 0.42, 0.42, 0.40); + c[ImGuiCol_FrameBgActive] = ImVec4(0.56, 0.56, 0.56, 0.67); + c[ImGuiCol_TitleBg] = ImVec4(0.19, 0.19, 0.19, 1.00); + c[ImGuiCol_TitleBgActive] = ImVec4(0.22, 0.22, 0.22, 1.00); + c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.17, 0.17, 0.17, 0.90); + c[ImGuiCol_MenuBarBg] = ImVec4(0.34, 0.34, 0.34, 1.00); + c[ImGuiCol_ScrollbarBg] = ImVec4(0.24, 0.24, 0.24, 0.53); + c[ImGuiCol_ScrollbarGrab] = ImVec4(0.41, 0.41, 0.41, 1.00); + c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.52, 0.52, 0.52, 1.00); + c[ImGuiCol_ScrollbarGrabActive] = ImVec4(0.76, 0.76, 0.76, 1.00); + c[ImGuiCol_CheckMark] = ImVec4(0.65, 0.65, 0.65, 1.00); + c[ImGuiCol_SliderGrab] = ImVec4(0.52, 0.52, 0.52, 1.00); + c[ImGuiCol_SliderGrabActive] = ImVec4(0.64, 0.64, 0.64, 1.00); + c[ImGuiCol_Button] = ImVec4(0.54, 0.54, 0.54, 0.35); + c[ImGuiCol_ButtonHovered] = ImVec4(0.52, 0.52, 0.52, 0.59); + c[ImGuiCol_ButtonActive] = ImVec4(0.76, 0.76, 0.76, 1.00); + c[ImGuiCol_Header] = ImVec4(0.38, 0.38, 0.38, 1.00); + c[ImGuiCol_HeaderHovered] = ImVec4(0.47, 0.47, 0.47, 1.00); + c[ImGuiCol_HeaderActive] = ImVec4(0.76, 0.76, 0.76, 0.77); + c[ImGuiCol_Separator] = ImVec4(0.00, 0.00, 0.00, 0.18); + c[ImGuiCol_SeparatorHovered] = ImVec4(0.70, 0.67, 0.60, 0.29); + c[ImGuiCol_SeparatorActive] = ImVec4(0.70, 0.67, 0.60, 0.67); + c[ImGuiCol_ResizeGrip] = ImVec4(0.26, 0.59, 0.98, 0.25); + c[ImGuiCol_ResizeGripHovered] = ImVec4(0.26, 0.59, 0.98, 0.67); + c[ImGuiCol_ResizeGripActive] = ImVec4(0.26, 0.59, 0.98, 0.95); + c[ImGuiCol_PlotLines] = ImVec4(0.61, 0.61, 0.61, 1.00); + c[ImGuiCol_PlotLinesHovered] = ImVec4(1.00, 0.43, 0.35, 1.00); + c[ImGuiCol_PlotHistogram] = ImVec4(0.90, 0.70, 0.00, 1.00); + c[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00, 0.60, 0.00, 1.00); + c[ImGuiCol_TextSelectedBg] = ImVec4(0.73, 0.73, 0.73, 0.35); + c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80, 0.80, 0.80, 0.35); + c[ImGuiCol_DragDropTarget] = ImVec4(1.00, 1.00, 0.00, 0.90); + c[ImGuiCol_NavHighlight] = ImVec4(0.26, 0.59, 0.98, 1.00); + c[ImGuiCol_NavWindowingHighlight] = ImVec4(1.00, 1.00, 1.00, 0.70); + c[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80, 0.80, 0.80, 0.20); + c[ImGuiCol_DockingEmptyBg] = ImVec4(0.38, 0.38, 0.38, 1.00); + c[ImGuiCol_Tab] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_TabHovered] = ImVec4(0.40, 0.40, 0.40, 1.00); + c[ImGuiCol_TabActive] = ImVec4(0.33, 0.33, 0.33, 1.00); + c[ImGuiCol_TabUnfocused] = ImVec4(0.25, 0.25, 0.25, 1.00); + c[ImGuiCol_TabUnfocusedActive] = ImVec4(0.33, 0.33, 0.33, 1.00); + c[ImGuiCol_DockingPreview] = ImVec4(0.85, 0.85, 0.85, 0.28); c[ImGuiCol_WindowBg].w = 1.0f; } else if (theme == GuiTheme::kLight) { ImGui::StyleColorsLight(&s); - ImVec4 white = ImVec4(1.00, 1.00, 1.00, 1.00); + ImVec4 white = ImVec4(1.00, 1.00, 1.00, 1.00); ImVec4 transparent = ImVec4(0.00, 0.00, 0.00, 0.00); - ImVec4 dark = ImVec4(0.00, 0.00, 0.00, 0.20); - ImVec4 darker = ImVec4(0.00, 0.00, 0.00, 0.50); - ImVec4 background = ImVec4(0.95, 0.95, 0.95, 1.00); - ImVec4 text = ImVec4(0.10, 0.10, 0.10, 1.00); - ImVec4 border = ImVec4(0.60, 0.60, 0.60, 1.00); - ImVec4 grab = ImVec4(0.69, 0.69, 0.69, 1.00); - ImVec4 header = ImVec4(0.86, 0.86, 0.86, 1.00); - ImVec4 active = ImVec4(0.00, 0.47, 0.84, 1.00); - ImVec4 hover = ImVec4(0.00, 0.47, 0.84, 0.20); + ImVec4 dark = ImVec4(0.00, 0.00, 0.00, 0.20); + ImVec4 darker = ImVec4(0.00, 0.00, 0.00, 0.50); + ImVec4 background = ImVec4(0.95, 0.95, 0.95, 1.00); + ImVec4 text = ImVec4(0.10, 0.10, 0.10, 1.00); + ImVec4 border = ImVec4(0.60, 0.60, 0.60, 1.00); + ImVec4 grab = ImVec4(0.69, 0.69, 0.69, 1.00); + ImVec4 header = ImVec4(0.86, 0.86, 0.86, 1.00); + ImVec4 active = ImVec4(0.00, 0.47, 0.84, 1.00); + ImVec4 hover = ImVec4(0.00, 0.47, 0.84, 0.20); c[ImGuiCol_Text] = text; c[ImGuiCol_WindowBg] = background; @@ -129,46 +130,46 @@ void SetupTheme(GuiTheme theme) { c[ImGuiCol_ScrollbarGrab] = grab; c[ImGuiCol_ScrollbarGrabHovered] = dark; c[ImGuiCol_ScrollbarGrabActive] = darker; - } else { + } else { ImGui::StyleColorsDark(&s); - ImVec4 black = ImVec4(0.00, 0.00, 0.00, 1.0); - ImVec4 window = ImVec4(0.25, 0.25, 0.25, 1.0); - ImVec4 font_active = ImVec4(1.00, 1.00, 1.00, 1.0); + ImVec4 black = ImVec4(0.00, 0.00, 0.00, 1.0); + ImVec4 window = ImVec4(0.25, 0.25, 0.25, 1.0); + ImVec4 font_active = ImVec4(1.00, 1.00, 1.00, 1.0); ImVec4 font_inactive = ImVec4(0.50, 0.50, 0.50, 1.0); - ImVec4 thumb = ImVec4(0.12, 0.12, 0.12, 1.0); - ImVec4 section = ImVec4(0.40, 0.15, 0.15, 1.0); - ImVec4 button = ImVec4(0.60, 0.40, 0.40, 1.0); - ImVec4 check = ImVec4(0.40, 0.40, 0.70, 1.0); - ImVec4 frame = ImVec4(0.40, 0.30, 0.40, 1.0); - ImVec4 slider = ImVec4(0.60, 0.40, 0.60, 1.0); + ImVec4 thumb = ImVec4(0.12, 0.12, 0.12, 1.0); + ImVec4 section = ImVec4(0.40, 0.15, 0.15, 1.0); + ImVec4 button = ImVec4(0.60, 0.40, 0.40, 1.0); + ImVec4 check = ImVec4(0.40, 0.40, 0.70, 1.0); + ImVec4 frame = ImVec4(0.40, 0.30, 0.40, 1.0); + ImVec4 slider = ImVec4(0.60, 0.40, 0.60, 1.0); - c[ImGuiCol_WindowBg] = window; - c[ImGuiCol_ChildBg] = black; - c[ImGuiCol_PopupBg] = window; - c[ImGuiCol_Text] = font_active; - c[ImGuiCol_TextDisabled] = font_inactive; - c[ImGuiCol_CheckMark] = font_active; - c[ImGuiCol_Header] = section; - c[ImGuiCol_HeaderHovered] = section; - c[ImGuiCol_HeaderActive] = section; - c[ImGuiCol_TitleBgActive] = window; - c[ImGuiCol_ScrollbarBg] = window; - c[ImGuiCol_ScrollbarGrab] = thumb; - c[ImGuiCol_ScrollbarGrabHovered] = thumb; - c[ImGuiCol_ScrollbarGrabActive] = thumb; - c[ImGuiCol_FrameBg] = frame; - c[ImGuiCol_FrameBgHovered] = frame; - c[ImGuiCol_FrameBgActive] = frame; - c[ImGuiCol_SliderGrab] = slider; - c[ImGuiCol_SliderGrabActive] = slider; - c[ImGuiCol_Button] = window; - c[ImGuiCol_ButtonHovered] = button; - c[ImGuiCol_ButtonActive] = button; - c[ImGuiCol_Tab] = window; - c[ImGuiCol_TabHovered] = check; - c[ImGuiCol_TabSelected] = check; - c[ImGuiCol_TabDimmed] = window; - c[ImGuiCol_TabDimmedSelected] = check; + c[ImGuiCol_WindowBg] = window; + c[ImGuiCol_ChildBg] = black; + c[ImGuiCol_PopupBg] = window; + c[ImGuiCol_Text] = font_active; + c[ImGuiCol_TextDisabled] = font_inactive; + c[ImGuiCol_CheckMark] = font_active; + c[ImGuiCol_Header] = section; + c[ImGuiCol_HeaderHovered] = section; + c[ImGuiCol_HeaderActive] = section; + c[ImGuiCol_TitleBgActive] = window; + c[ImGuiCol_ScrollbarBg] = window; + c[ImGuiCol_ScrollbarGrab] = thumb; + c[ImGuiCol_ScrollbarGrabHovered] = thumb; + c[ImGuiCol_ScrollbarGrabActive] = thumb; + c[ImGuiCol_FrameBg] = frame; + c[ImGuiCol_FrameBgHovered] = frame; + c[ImGuiCol_FrameBgActive] = frame; + c[ImGuiCol_SliderGrab] = slider; + c[ImGuiCol_SliderGrabActive] = slider; + c[ImGuiCol_Button] = window; + c[ImGuiCol_ButtonHovered] = button; + c[ImGuiCol_ButtonActive] = button; + c[ImGuiCol_Tab] = window; + c[ImGuiCol_TabHovered] = check; + c[ImGuiCol_TabSelected] = check; + c[ImGuiCol_TabDimmed] = window; + c[ImGuiCol_TabDimmedSelected] = check; } int hspacing = 4; @@ -207,14 +208,11 @@ ImVec4 ConfigureDockingLayout() { const float kToolsBarHeight = 48.f * scale; const float kStatusBarHeight = 32.f * scale; - const ImVec2 dockspace_pos{ - viewport->WorkPos.x, - viewport->WorkPos.y + kToolsBarHeight - }; + const ImVec2 dockspace_pos{viewport->WorkPos.x, + viewport->WorkPos.y + kToolsBarHeight}; const ImVec2 dockspace_size{ viewport->WorkSize.x, - viewport->WorkSize.y - kToolsBarHeight - kStatusBarHeight - }; + viewport->WorkSize.y - kToolsBarHeight - kStatusBarHeight}; ImGuiID root = ImGui::GetID("Root"); const bool first_time = (ImGui::DockBuilderGetNode(root) == nullptr); @@ -228,16 +226,16 @@ ImVec4 ConfigureDockingLayout() { ImGuiID main = root; ImGuiID options = 0; - ImGui::DockBuilderSplitNode(main, ImGuiDir_Left, kOptionsRelWidth, - &options, &main); + ImGui::DockBuilderSplitNode(main, ImGuiDir_Left, kOptionsRelWidth, &options, + &main); ImGuiID inspector = 0; ImGui::DockBuilderSplitNode(main, ImGuiDir_Right, kInspectorRelWidth, &inspector, &main); ImGuiID stats = 0; - ImGui::DockBuilderSplitNode(options, ImGuiDir_Down, kStatsRelHeight, - &stats, &options); + ImGui::DockBuilderSplitNode(options, ImGuiDir_Down, kStatsRelHeight, &stats, + &options); ImGuiID properties = 0; ImGui::DockBuilderSplitNode(inspector, ImGuiDir_Down, kStatsRelHeight, @@ -265,11 +263,10 @@ ImVec4 ConfigureDockingLayout() { ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground; - const ImGuiWindowFlags kFixedFlags = ImGuiWindowFlags_NoTitleBar | - ImGuiWindowFlags_NoMove | - ImGuiWindowFlags_NoResize | - ImGuiWindowFlags_NoScrollbar | - ImGuiWindowFlags_NoDocking; + const ImGuiWindowFlags kFixedFlags = + ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove | + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar | + ImGuiWindowFlags_NoDocking; // Main workspace area in which we can dock other windows. { @@ -292,7 +289,8 @@ ImVec4 ConfigureDockingLayout() { style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f); style.Var(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kToolsBarHeight), ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kToolsBarHeight), + ImGuiCond_Always); ImGui::Begin("ToolBar", nullptr, kFixedFlags); ImGui::End(); } @@ -302,8 +300,10 @@ ImVec4 ConfigureDockingLayout() { platform::ScopedStyle style; style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f); style.Var(ImGuiStyleVar_WindowRounding, 0.0f); - ImGui::SetNextWindowPos(ImVec2(0, viewport->Size.y - kStatusBarHeight), ImGuiCond_Always); - ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kStatusBarHeight), ImGuiCond_Always); + ImGui::SetNextWindowPos(ImVec2(0, viewport->Size.y - kStatusBarHeight), + ImGuiCond_Always); + ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kStatusBarHeight), + ImGuiCond_Always); ImGui::Begin("StatusBar", nullptr, kFixedFlags); ImGui::End(); } @@ -317,6 +317,148 @@ ImVec4 ConfigureDockingLayout() { return ImVec4(workspace_x, workspace_y, workspace_w, workspace_h); } +bool ThemeSelectGui(GuiTheme* theme) { + static constexpr const char* ICON_DARKMODE = ICON_FA_CIRCLE; + static constexpr const char* ICON_LIGHTMODE = ICON_FA_CIRCLE_O; + static constexpr const char* ICON_CLASSICMODE = ICON_FA_ADJUST; + const char* theme_icons[] = {ICON_LIGHTMODE, ICON_DARKMODE, ICON_CLASSICMODE}; + const char* theme_tooltips[] = {"Light Mode", "Dark Mode", "Classic Mode"}; + const GuiTheme theme_values[] = { + GuiTheme::kLight, + GuiTheme::kDark, + GuiTheme::kClassic, + }; + + bool changed = false; + + int theme_idx = static_cast(*theme); + ImGui::SetNextItemWidth(ImGui::CalcTextSize(theme_icons[0]).x + + ImGui::GetStyle().FramePadding.x * 2); + if (ImGui::BeginCombo("##Theme", theme_icons[theme_idx], + ImGuiComboFlags_NoArrowButton)) { + for (int n = 0; n < IM_ARRAYSIZE(theme_icons); n++) { + if (ImGui::Selectable(theme_icons[n], (theme_idx == n))) { + *theme = theme_values[n]; + changed = true; + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("%s", theme_tooltips[n]); + } + } + ImGui::EndCombo(); + } + ImGui::SetItemTooltip("%s", "Theme"); + + return changed; +} + +bool LabelSelectionGui(mjvOption* opts) { + static constexpr const char* ICON_LABEL = ICON_FA_COMMENT; + static constexpr const char* kLabelNames[] = { + "None", "Body", "Joint", "Geom", "Site", "Camera", + "Light", "Tendon", "Actuator", "Constraint", "Flex", "Skin", + "Selection", "Sel Pnt", "Contact", "Force", "Island"}; + + bool changed = false; + const std::string label_preview = + std::string(ICON_LABEL) + " " + kLabelNames[opts->label]; + if (ImGui::BeginCombo("##Label", label_preview.c_str(), + ImGuiComboFlags_NoArrowButton)) { + for (int n = 0; n < IM_ARRAYSIZE(kLabelNames); n++) { + if (ImGui::Selectable(kLabelNames[n], (opts->label == n))) { + changed = true; + opts->label = n; + } + } + ImGui::EndCombo(); + } + ImGui::SetItemTooltip("%s", "Label"); + return changed; +} + +bool FrameSelectionGui(mjvOption* opts) { + static constexpr const char* ICON_FRAME = ICON_FA_ARROWS; + static constexpr const char* kFrameNames[] = { + "None", "Body", "Geom", "Site", "Camera", "Light", "Contact", "World"}; + + bool changed = false; + const std::string frame_preview = + std::string(ICON_FRAME) + " " + kFrameNames[opts->frame]; + if (ImGui::BeginCombo("##Frame", frame_preview.c_str(), + ImGuiComboFlags_NoArrowButton)) { + for (int n = 0; n < IM_ARRAYSIZE(kFrameNames); n++) { + if (ImGui::Selectable(kFrameNames[n], (opts->frame == n))) { + opts->frame = n; + changed = true; + } + } + ImGui::EndCombo(); + } + ImGui::SetItemTooltip("%s", "Frame"); + return changed; +} + +static std::string GetCameraName(const mjModel* model, const mjvCamera& camera, + int index) { + static constexpr char kCameraTumbleName[] = "Free: tumble"; + static constexpr char kCameraWasdName[] = "Free: wasd"; + static constexpr char kCameraUnnamedName[] = "Unnamed"; + + if (index == kTumbleCameraIdx) { + return kCameraTumbleName; + } else if (index == kFreeCameraIdx) { + return kCameraWasdName; + } else if (index == kTrackingCameraIdx) { + return "Tracking (" + std::to_string(camera.trackbodyid) + ")"; + } else if (model->names[model->name_camadr[index]]) { + return std::string(model->names + model->name_camadr[index]); + } else { + return kCameraUnnamedName; + } +} + +bool CameraSelectionGui(const mjModel* model, mjData* data, mjvCamera& camera, + int& index) { + static constexpr const char* ICON_CAMERA = ICON_FA_CAMERA; + + auto select = [&](int type, int idx) { + if (ImGui::Selectable(GetCameraName(model, camera, type).c_str(), + (type == idx))) { + return true; + } + return false; + }; + + bool changed = false; + + const std::string preview = + std::string(ICON_CAMERA) + " " + GetCameraName(model, camera, index); + if (ImGui::BeginCombo("##Camera", preview.c_str(), + ImGuiComboFlags_NoArrowButton)) { + if (select(kTumbleCameraIdx, index)) { + index = SetCamera(model, &camera, kTumbleCameraIdx); + changed = true; + } + if (select(kFreeCameraIdx, index)) { + index = SetCamera(model, &camera, kFreeCameraIdx); + changed = true; + } + if (select(kTrackingCameraIdx, index)) { + index = SetCamera(model, &camera, kTrackingCameraIdx); + changed = true; + } + for (int cam = 0; cam < model->ncam; cam++) { + if (select(cam, index)) { + index = SetCamera(model, &camera, cam); + changed = true; + } + } + ImGui::EndCombo(); + } + ImGui::SetItemTooltip("%s", "Camera"); + return changed; +} + void SensorGui(const mjModel* model, const mjData* data) { if (model->nsensor == 0) { return; @@ -392,11 +534,12 @@ void StateGui(const mjModel* model, mjData* data, std::vector& state, ImGui::Unindent(0.5f * ImGui::GetTreeNodeToLabelSpacing()); // State component names and tooltips. - static constexpr const char* name_and_tooltip[][2] = { + static constexpr const char* name_and_tooltip[mjNSTATE][2] = { {"TIME", "Time"}, {"QPOS", "Position"}, {"QVEL", "Velocity"}, {"ACT", "Actuator activation"}, + {"HISTORY", "History buffers (control, sensor)"}, {"WARMSTART", "Acceleration used for warmstart"}, {"CTRL", "Control"}, {"QFRC_APPLIED", "Applied generalized force"}, @@ -935,36 +1078,49 @@ void ConvergenceGui(const mjModel* model, mjData* data) { ImPlot::SetupLegend(ImPlotLocation_NorthEast); ImPlot::SetupFinish(); - const int nisland = data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0; + const int nisland = + data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0; for (int k = 0; k < nisland; k++) { mjSolverStat* stats = data->solver + k * mjNSOLVER; const int npoints = mjMIN(mjMIN(data->solver_niter[k], mjNSOLVER), mjMAXLINEPNT); - ImPlot::PlotLineG("improvement", +[](int i, void* user_data) { - const mjSolverStat* stats = static_cast(user_data); - const float x = static_cast(i); - const float y = mju_log10(mju_max(mjMINVAL, stats[i].improvement)); - return ImPlotPoint{x, y}; - }, stats, npoints); + ImPlot::PlotLineG( + "improvement", + +[](int i, void* user_data) { + const mjSolverStat* stats = + static_cast(user_data); + const float x = static_cast(i); + const float y = mju_log10(mju_max(mjMINVAL, stats[i].improvement)); + return ImPlotPoint{x, y}; + }, + stats, npoints); if (model->opt.solver == mjSOL_PGS) { continue; } - ImPlot::PlotLineG("gradient", +[](int i, void* user_data) { - const mjSolverStat* stats = static_cast(user_data); - const float x = static_cast(i); - const float y = mju_log10(mju_max(mjMINVAL, stats[i].gradient)); - return ImPlotPoint{x, y}; - }, stats, npoints); + ImPlot::PlotLineG( + "gradient", + +[](int i, void* user_data) { + const mjSolverStat* stats = + static_cast(user_data); + const float x = static_cast(i); + const float y = mju_log10(mju_max(mjMINVAL, stats[i].gradient)); + return ImPlotPoint{x, y}; + }, + stats, npoints); - ImPlot::PlotLineG("lineslope", +[](int i, void* user_data) { - const mjSolverStat* stats = static_cast(user_data); - const float x = static_cast(i); - const float y = mju_log10(mju_max(mjMINVAL, stats[i].lineslope)); - return ImPlotPoint{x, y}; - }, stats, npoints); + ImPlot::PlotLineG( + "lineslope", + +[](int i, void* user_data) { + const mjSolverStat* stats = + static_cast(user_data); + const float x = static_cast(i); + const float y = mju_log10(mju_max(mjMINVAL, stats[i].lineslope)); + return ImPlotPoint{x, y}; + }, + stats, npoints); } ImPlot::PopStyleVar(); @@ -982,56 +1138,76 @@ void CountsGui(const mjModel* model, mjData* data) { ImPlot::SetupLegend(ImPlotLocation_NorthEast); ImPlot::SetupFinish(); - const int nisland = data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0; + const int nisland = + data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0; for (int k = 0; k < nisland; k++) { const int npoints = mjMIN(mjMIN(data->solver_niter[k], mjNSOLVER), mjMAXLINEPNT); - mjSolverStat* stats = data->solver + k*mjNSOLVER; + mjSolverStat* stats = data->solver + k * mjNSOLVER; int nefc = nisland == 1 ? data->nefc : data->island_nefc[k]; - ImPlot::PlotLineG("total", +[](int i, void* user_data) { - const float x = static_cast(i); - const float y = *(static_cast(user_data)); - return ImPlotPoint{x, y}; - }, &nefc, npoints); + ImPlot::PlotLineG( + "total", + +[](int i, void* user_data) { + const float x = static_cast(i); + const float y = *(static_cast(user_data)); + return ImPlotPoint{x, y}; + }, + &nefc, npoints); - ImPlot::PlotLineG("active", +[](int i, void* user_data) { - const mjSolverStat* stats = static_cast(user_data); - const float x = static_cast(i); - const float y = stats[i].nactive; - return ImPlotPoint{x, y}; - }, stats, npoints); + ImPlot::PlotLineG( + "active", + +[](int i, void* user_data) { + const mjSolverStat* stats = + static_cast(user_data); + const float x = static_cast(i); + const float y = stats[i].nactive; + return ImPlotPoint{x, y}; + }, + stats, npoints); - ImPlot::PlotLineG("changed", +[](int i, void* user_data) { - const mjSolverStat* stats = static_cast(user_data); - const float x = static_cast(i); - const float y = stats[i].nchange; - return ImPlotPoint{x, y}; - }, stats, npoints); + ImPlot::PlotLineG( + "changed", + +[](int i, void* user_data) { + const mjSolverStat* stats = + static_cast(user_data); + const float x = static_cast(i); + const float y = stats[i].nchange; + return ImPlotPoint{x, y}; + }, + stats, npoints); if (model->opt.solver == mjSOL_PGS) { continue; } - ImPlot::PlotLineG("evals", +[](int i, void* user_data) { - const mjSolverStat* stats = static_cast(user_data); - const float x = static_cast(i); - const float y = stats[i].neval; - return ImPlotPoint{x, y}; - }, stats, npoints); + ImPlot::PlotLineG( + "evals", + +[](int i, void* user_data) { + const mjSolverStat* stats = + static_cast(user_data); + const float x = static_cast(i); + const float y = stats[i].neval; + return ImPlotPoint{x, y}; + }, + stats, npoints); if (model->opt.solver == mjSOL_CG) { continue; } - ImPlot::PlotLineG("updates", +[](int i, void* user_data) { - const mjSolverStat* stats = static_cast(user_data); - const float x = static_cast(i); - const float y = stats[i].nupdate; - return ImPlotPoint{x, y}; - }, stats, npoints); + ImPlot::PlotLineG( + "updates", + +[](int i, void* user_data) { + const mjSolverStat* stats = + static_cast(user_data); + const float x = static_cast(i); + const float y = stats[i].nupdate; + return ImPlotPoint{x, y}; + }, + stats, npoints); } ImPlot::PopStyleVar(); diff --git a/src/experimental/platform/gui.h b/src/experimental/platform/gui.h index 7dbdae09..d0b1ff79 100644 --- a/src/experimental/platform/gui.h +++ b/src/experimental/platform/gui.h @@ -62,6 +62,19 @@ void SetupTheme(GuiTheme theme); // be used to place additional elements (e.g. floating charts). ImVec4 ConfigureDockingLayout(); +// UX for selecting the GUI theme. +bool ThemeSelectGui(GuiTheme* theme); + +// UX for selecting the visualization label option. +bool LabelSelectionGui(mjvOption* opts); + +// UX for selecting the visualization frame option. +bool FrameSelectionGui(mjvOption* opts); + +// UX for selecting the camera. +bool CameraSelectionGui(const mjModel* model, mjData* data, mjvCamera& camera, + int& index); + // UX for controlling the physics simulation parameters (e.g. integrator, // solver, etc.) in mjModel. void PhysicsGui(mjModel* model, float min_width); diff --git a/src/experimental/platform/gui_spec.cc b/src/experimental/platform/gui_spec.cc index 27dbbba3..7d30d31d 100644 --- a/src/experimental/platform/gui_spec.cc +++ b/src/experimental/platform/gui_spec.cc @@ -93,28 +93,29 @@ static void AddEditorButtons(mjsElement* element, mjsElement** selected_element, if (ImGui::SmallButton(ICON_FA_PLUS)) { ImGui::OpenPopupOnItemClick("BodyAddChild", 0); } + + if (ImGui::BeginPopupContextItem("BodyAddChild")) { + mjsBody* body = mjs_asBody(element); + auto option = [&](const char* label, mjtObj type) { + if (ImGui::Selectable(label)) { + *selected_element = editor.AddBodyElement(body, type); + } + }; + option("Camera", mjOBJ_CAMERA); + option("Frame", mjOBJ_FRAME); + option("Geom", mjOBJ_GEOM); + option("Joint", mjOBJ_JOINT); + option("Light", mjOBJ_LIGHT); + option("Site", mjOBJ_SITE); + ImGui::EndPopup(); + } + ImGui::SameLine(); } if (ImGui::SmallButton(ICON_FA_TRASH_CAN)) { editor.DeleteActiveElement(); *selected_element = nullptr; } - - if (ImGui::BeginPopupContextItem("BodyAddChild")) { - mjsBody* body = mjs_asBody(element); - auto option = [&](const char* label, mjtObj type) { - if (ImGui::Selectable(label)) { - *selected_element = editor.AddBodyElement(body, type); - } - }; - option("Camera", mjOBJ_CAMERA); - option("Frame", mjOBJ_FRAME); - option("Geom", mjOBJ_GEOM); - option("Joint", mjOBJ_JOINT); - option("Light", mjOBJ_LIGHT); - option("Site", mjOBJ_SITE); - ImGui::EndPopup(); - } } static void SelectableElement(mjsElement* element, diff --git a/src/experimental/platform/model_holder.cc b/src/experimental/platform/model_holder.cc index a59105be..78bbc4cc 100644 --- a/src/experimental/platform/model_holder.cc +++ b/src/experimental/platform/model_holder.cc @@ -61,6 +61,10 @@ std::unique_ptr ModelHolder::FromBuffer( return mh; } +ModelHolder::ModelHolder() { + mj_defaultVFS(&vfs_); +} + ModelHolder::~ModelHolder() { if (data_) { mj_deleteData(data_); @@ -94,7 +98,6 @@ void ModelHolder::InitFromSpec(mjSpec* spec) { } void ModelHolder::InitFromFile(std::string_view filepath) { - mj_defaultVFS(&vfs_); if (filepath.ends_with(".mjb")) { model_ = mj_loadModel(filepath.data(), &vfs_); } else { @@ -108,8 +111,6 @@ void ModelHolder::InitFromFile(std::string_view filepath) { void ModelHolder::InitFromBuffer(std::span buffer, std::string_view content_type, std::string_view filename) { - mj_defaultVFS(&vfs_); - if (content_type == "text/xml") { const char* ptr = reinterpret_cast(buffer.data()); spec_ = mj_parseXMLString(ptr, nullptr, error_, sizeof(error_)); diff --git a/src/experimental/platform/model_holder.h b/src/experimental/platform/model_holder.h index e710b25b..e14d2c6a 100644 --- a/src/experimental/platform/model_holder.h +++ b/src/experimental/platform/model_holder.h @@ -64,7 +64,7 @@ class ModelHolder { mjModel* ReleaseModel(); private: - ModelHolder() = default; + ModelHolder(); void InitFromSpec(mjSpec* spec); void InitFromFile(std::string_view filepath); void InitFromBuffer(std::span buffer, diff --git a/src/experimental/studio/app.cc b/src/experimental/studio/app.cc index 36e0786a..0e843912 100644 --- a/src/experimental/studio/app.cc +++ b/src/experimental/studio/app.cc @@ -78,13 +78,7 @@ static constexpr const char* ICON_VISCOUS_PAUSE = platform::ICON_FA_MAGIC; static constexpr const char* ICON_COPY_CAMERA = platform::ICON_FA_COPY; static constexpr const char* ICON_UNLOAD_MODEL = platform::ICON_FA_EJECT; static constexpr const char* ICON_RELOAD_MODEL = platform::ICON_FA_REFRESH; -static constexpr const char* ICON_LABEL = platform::ICON_FA_COMMENT; static constexpr const char* ICON_RESET_MODEL = platform::ICON_FA_UNDO; -static constexpr const char* ICON_FRAME = platform::ICON_FA_ARROWS; -static constexpr const char* ICON_CAMERA = platform::ICON_FA_CAMERA; -static constexpr const char* ICON_DARKMODE = platform::ICON_FA_CIRCLE; -static constexpr const char* ICON_LIGHTMODE = platform::ICON_FA_CIRCLE_O; -static constexpr const char* ICON_CLASSICMODE = platform::ICON_FA_ADJUST; static constexpr const char* ICON_PREV_FRAME = platform::ICON_FA_CARET_LEFT; static constexpr const char* ICON_NEXT_FRAME = platform::ICON_FA_CARET_RIGHT; static constexpr const char* ICON_CURR_FRAME = platform::ICON_FA_FAST_FORWARD; @@ -93,16 +87,6 @@ static constexpr const char* ICON_RELOAD_SPEC = platform::ICON_FA_REFRESH; static constexpr const char* ICON_UNDO_SPEC = platform::ICON_FA_UNDO; static constexpr const char* ICON_REDO_SPEC = platform::ICON_FA_REPEAT; -// UI labels for mjtLabel. -static constexpr const char* kLabelNames[] = { - "None", "Body", "Joint", "Geom", "Site", "Camera", - "Light", "Tendon", "Actuator", "Constraint", "Flex", "Skin", - "Selection", "Sel Pnt", "Contact", "Force", "Island"}; - -// UI labels for mjtFrame. -static constexpr const char* kFrameNames[] = { - "None", "Body", "Geom", "Site", "Camera", "Light", "Contact", "World"}; - // logarithmically spaced real-time slow-down coefficients (percent) // clang-format off static constexpr std::array kPercentRealTime = { @@ -846,7 +830,10 @@ void App::BuildGui() { return; } - SetupTheme(ui_.theme); + if (!tmp_.style_editor) { + platform::SetupTheme(ui_.theme); + } + const ImVec4 workspace_rect = platform::ConfigureDockingLayout(); // Place charts in bottom right corner of the workspace. @@ -1024,14 +1011,6 @@ void App::BuildGui() { } } -void App::SetupTheme(platform::GuiTheme theme) { - if (!tmp_.style_editor) { - platform::SetupTheme(theme); - ui_.theme = theme; - ImGui::GetIO().WantSaveIniSettings = true; - } -} - void App::ModelOptionsGui() { const float min_width = GetExpectedLabelWidth(); const ImGuiChildFlags child_flags = @@ -1449,7 +1428,7 @@ void App::ToolBarGui() { const float label_width = GetExpectedLabelWidth(); const float copy_btn_width = ImGui::CalcTextSize(ICON_COPY_CAMERA).x + ImGui::GetStyle().FramePadding.x * 2; - const float theme_width = ImGui::CalcTextSize(ICON_LIGHTMODE).x + + const float theme_width = ImGui::CalcTextSize(platform::ICON_FA_CIRCLE_O).x + ImGui::GetStyle().FramePadding.x * 2; const float sp = ImGui::GetStyle().ItemSpacing.x; const float right_width = label_width + sp + label_width + sp + @@ -1576,84 +1555,30 @@ void App::ToolBarGui() { ImGui::SetCursorPosY(ImGui::GetCursorPosY() + (button_size.y - ImGui::GetFrameHeight()) * 0.5f); - // Camera selection. if (ImGui::Button(ICON_COPY_CAMERA)) { std::string camera_string = platform::CameraToString(data(), &camera_); platform::MaybeSaveToClipboard(camera_string); } ImGui::SetItemTooltip("%s", "Copy Camera"); + ImGui::SameLine(0, 0); ImGui::SetNextItemWidth(GetExpectedLabelWidth()); - int camera_idx = ui_.camera_idx - platform::kTumbleCameraIdx; - std::vector cameras = GetCameraNames(); - std::string camera_preview = - std::string(ICON_CAMERA) + " " + cameras[camera_idx]; - if (ImGui::BeginCombo("##Camera", camera_preview.c_str(), combo_flags)) { - for (int n = 0; n < cameras.size(); n++) { - if (ImGui::Selectable(cameras[n], (camera_idx == n))) { - ui_.camera_idx = platform::SetCamera(model(), &camera_, - n + platform::kTumbleCameraIdx); - } - } - ImGui::EndCombo(); - } - ImGui::SetItemTooltip("%s", "Camera"); + platform::CameraSelectionGui(model(), data(), camera_, ui_.camera_idx); - // Label selection. ImGui::SameLine(); ImGui::SetNextItemWidth(GetExpectedLabelWidth()); - std::string label_preview = - std::string(ICON_LABEL) + " " + kLabelNames[vis_options_.label]; - if (ImGui::BeginCombo("##Label", label_preview.c_str(), combo_flags)) { - for (int n = 0; n < IM_ARRAYSIZE(kLabelNames); n++) { - if (ImGui::Selectable(kLabelNames[n], (vis_options_.label == n))) { - vis_options_.label = n; - } - } - ImGui::EndCombo(); - } - ImGui::SetItemTooltip("%s", "Label"); + platform::LabelSelectionGui(&vis_options_); - // Frame selection. ImGui::SameLine(); ImGui::SetNextItemWidth(GetExpectedLabelWidth()); - std::string frame_preview = - std::string(ICON_FRAME) + " " + kFrameNames[vis_options_.frame]; - if (ImGui::BeginCombo("##Frame", frame_preview.c_str(), combo_flags)) { - for (int n = 0; n < IM_ARRAYSIZE(kFrameNames); n++) { - if (ImGui::Selectable(kFrameNames[n], (vis_options_.frame == n))) { - vis_options_.frame = n; - } - } - ImGui::EndCombo(); - } - ImGui::SetItemTooltip("%s", "Frame"); + platform::FrameSelectionGui(&vis_options_); - // Theme selection. ImGui::SameLine(); - const char* theme_icons[] = {ICON_LIGHTMODE, ICON_DARKMODE, - ICON_CLASSICMODE}; - const char* theme_tooltips[] = {"Light Mode", "Dark Mode", "Classic Mode"}; - const platform::GuiTheme theme_values[] = { - platform::GuiTheme::kLight, - platform::GuiTheme::kDark, - platform::GuiTheme::kClassic, - }; - int theme_idx = static_cast(ui_.theme); - ImGui::SetNextItemWidth(ImGui::CalcTextSize(theme_icons[0]).x + - ImGui::GetStyle().FramePadding.x * 2); - if (ImGui::BeginCombo("##Theme", theme_icons[theme_idx], combo_flags)) { - for (int n = 0; n < IM_ARRAYSIZE(theme_icons); n++) { - if (ImGui::Selectable(theme_icons[n], (theme_idx == n))) { - SetupTheme(theme_values[n]); - } - if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("%s", theme_tooltips[n]); - } - } - ImGui::EndCombo(); + ImGui::SetNextItemWidth(GetExpectedLabelWidth()); + if (platform::ThemeSelectGui(&ui_.theme)) { + platform::SetupTheme(ui_.theme); + ImGui::GetIO().WantSaveIniSettings = true; } - ImGui::SetItemTooltip("%s", "Theme"); ImGui::EndTable(); } @@ -2011,34 +1936,6 @@ float App::GetExpectedLabelWidth() { return tmp_.expected_label_width; } -std::vector App::GetCameraNames() { - if (tmp_.camera_names.empty()) { - tmp_.camera_names.reserve(model()->ncam + 3); - - tmp_.camera_names.push_back("Free: tumble"); - tmp_.camera_names.push_back("Free: wasd"); - tmp_.camera_names.push_back("Tracking (-1)"); - for (int i = 0; i < model()->ncam; i++) { - if (model()->names[model()->name_camadr[i]]) { - tmp_.camera_names.push_back(model()->names + model()->name_camadr[i]); - } else { - tmp_.camera_names.push_back("Unnamed"); - } - } - } - - // Update tracking camera name as this can change over time. - tmp_.camera_names[2] = - "Tracking (" + std::to_string(camera_.trackbodyid) + ")"; - - std::vector names; - names.reserve(tmp_.camera_names.size()); - for (const auto& name : tmp_.camera_names) { - names.push_back(name.c_str()); - } - return names; -} - App::UiState::Dict App::UiState::ToDict() const { return { {"theme", std::to_string(static_cast(theme))}, diff --git a/src/experimental/studio/app.h b/src/experimental/studio/app.h index 2fb1c767..7b438d68 100644 --- a/src/experimental/studio/app.h +++ b/src/experimental/studio/app.h @@ -223,7 +223,6 @@ class App { void SpecEditorGui(); float GetExpectedLabelWidth(); - std::vector GetCameraNames(); mjSpec* spec() { return model_holder_->spec(); } mjModel* model() { return model_holder_->model(); } diff --git a/src/user/user_flexcomp.cc b/src/user/user_flexcomp.cc index 867f0707..c349fb28 100644 --- a/src/user/user_flexcomp.cc +++ b/src/user/user_flexcomp.cc @@ -1185,7 +1185,7 @@ bool mjCFlexcomp::MakeMesh(mjCModel* model, mjsCompiler* compiler, char* error, } // copy vertices - point = mesh.Vert(); + point.assign(mesh.Vert().begin(), mesh.Vert().end()); if (mesh.HasTexcoord()) { texcoord = mesh.Texcoord(); diff --git a/src/user/user_mesh.cc b/src/user/user_mesh.cc index 20244116..db852729 100644 --- a/src/user/user_mesh.cc +++ b/src/user/user_mesh.cc @@ -492,7 +492,7 @@ void mjCMesh::CacheMesh(mjCCache* cache, const mjResource* resource) { // calculate estimated size of mesh std::size_t size = sizeof(mjCMesh) - + (sizeof(double) * vert_.size()) + + (sizeof(float) * vert_.size()) + (sizeof(float) * normal_.size()) + (sizeof(float) * texcoord_.size()) + (sizeof(int) * face_.size()) @@ -538,7 +538,7 @@ struct VertexKey { -// convert vertices to double precision and remove repeated vertices if requested +// process and remove repeated vertices if requested void mjCMesh::ProcessVertices(const std::vector& vert, bool remove_repeated) { vert_.clear(); int nvert = vert.size(); @@ -647,19 +647,6 @@ void mjCMesh::LoadFromDecoder(mjResource* resource, bool remove_repeated) { facenormal_.assign(src_mesh->userfacenormal->begin(), src_mesh->userfacenormal->end()); facetexcoord_.assign(src_mesh->userfacetexcoord->begin(), src_mesh->userfacetexcoord->end()); - // correct winding order for left-handed coordinate systems - bool righthand = scale[0] * scale[1] * scale[2] > 0; - if (!righthand) { - for (size_t i = 0; i < face_.size(); i += 3) { - std::swap(face_[i + 1], face_[i + 2]); - } - for (size_t i = 0; i < facenormal_.size(); i += 3) { - std::swap(facenormal_[i + 1], facenormal_[i + 2]); - } - for (size_t i = 0; i < facetexcoord_.size(); i += 3) { - std::swap(facetexcoord_[i + 1], facetexcoord_[i + 2]); - } - } std::vector vert(src_mesh->uservert->begin(), src_mesh->uservert->end()); mj_deleteSpec(mesh_spec); @@ -788,24 +775,24 @@ void mjCMesh::TryCompile(const mjVFS* vfs) { // compute mesh properties if (!fromCache) { Process(); - } - // make octree - if (!needsdf) { - octree_.Clear(); // this occurs when a non-SDF mesh is loaded from a cached SDF mesh - } else if (octree_.NumNodes() == 0) { - octree_.SetFace(vert_, face_); - octree_.CreateOctree(aamm_); - - // compute sdf coefficients - if (!plugin.active) { - octree_.ComputeSdfCoeffs(vert_.data(), nvert(), face_.data(), nface(), tree_); + if (!file_.empty()) { + CacheMesh(cache, resource_); + } + } else { + // When a mesh is loaded from the cache, has no octree but needs one, + // we need to compute it here. If inversely it has an octree but we *do not* + // need one, we clear it. + if (!needsdf) { + octree_.Clear(); + } else if (octree_.NumNodes() == 0) { + std::vector dvert(vert_.begin(), vert_.end()); + octree_.SetFace(dvert, face_); + octree_.CreateOctree(aamm_); + if (!plugin.active) { + octree_.ComputeSdfCoeffs(dvert.data(), nvert(), face_.data(), nface(), tree_); + } } - } - - // cache mesh - if (!fromCache && !file_.empty()) { - CacheMesh(cache, resource_); } // close resource @@ -818,18 +805,18 @@ void mjCMesh::TryCompile(const mjVFS* vfs) { // get bounding volume -void mjCMesh::SetBoundingVolume(int faceid) { +void mjCMesh::SetBoundingVolume(int faceid, const double* dvert) { constexpr double kMaxVal = std::numeric_limits::max(); double face_aamm[6] = {kMaxVal, kMaxVal, kMaxVal, -kMaxVal, -kMaxVal, -kMaxVal}; for (int j = 0; j < 3; j++) { int vertid = face_[3*faceid + j]; - face_aamm[0] = std::min(face_aamm[0], vert_[3*vertid + 0]); - face_aamm[1] = std::min(face_aamm[1], vert_[3*vertid + 1]); - face_aamm[2] = std::min(face_aamm[2], vert_[3*vertid + 2]); - face_aamm[3] = std::max(face_aamm[3], vert_[3*vertid + 0]); - face_aamm[4] = std::max(face_aamm[4], vert_[3*vertid + 1]); - face_aamm[5] = std::max(face_aamm[5], vert_[3*vertid + 2]); + face_aamm[0] = std::min(face_aamm[0], dvert[3*vertid + 0]); + face_aamm[1] = std::min(face_aamm[1], dvert[3*vertid + 1]); + face_aamm[2] = std::min(face_aamm[2], dvert[3*vertid + 2]); + face_aamm[3] = std::max(face_aamm[3], dvert[3*vertid + 0]); + face_aamm[4] = std::max(face_aamm[4], dvert[3*vertid + 1]); + face_aamm[5] = std::max(face_aamm[5], dvert[3*vertid + 2]); } face_aabb_.push_back(.5 * (face_aamm[0] + face_aamm[3])); @@ -864,9 +851,7 @@ bool mjCMesh::HasTexcoord() const { void mjCMesh::CopyVert(float* arr) const { - for (int i = 0; i < vert_.size(); ++i) { - arr[i] = (float)vert_[i]; - } + std::copy(vert_.begin(), vert_.end(), arr); } @@ -1200,16 +1185,15 @@ void mjCMesh::LoadMSH(mjResource* resource, bool remove_repeated) { // compute the volume and center-of-mass of the mesh given the face centroid -double mjCMesh::ComputeVolume(double CoM[3], const double facecen[3]) const { +double mjCMesh::ComputeVolume(double CoM[3], const double facecen[3], const double* dvert) const { double normal[3], center[3], total_volume = 0; CoM[0] = CoM[1] = CoM[2] = 0; int nf = (inertia == mjMESH_INERTIA_CONVEX) ? graph_[1] : nface(); const int* f = (inertia == mjMESH_INERTIA_CONVEX) ? GraphFaces() : face_.data(); for (int i = 0; i < nf; i++) { - // get area, normal and center - double area = triangle(normal, center, &vert_[3*f[3*i]], &vert_[3*f[3*i + 1]], - &vert_[3*f[3*i + 2]]); + double area = triangle(normal, center, &dvert[3*f[3*i]], &dvert[3*f[3*i + 1]], + &dvert[3*f[3*i + 2]]); // compute and add volume double vec[3] = {center[0] - facecen[0], center[1] - facecen[1], center[2] - facecen[2]}; @@ -1239,14 +1223,13 @@ double mjCMesh::ComputeVolume(double CoM[3], const double facecen[3]) const { // compute the surface area and center-of-mass of the mesh given the face centroid -double mjCMesh::ComputeSurfaceArea(double CoM[3], const double facecen[3]) const { +double mjCMesh::ComputeSurfaceArea(double CoM[3], const double facecen[3], const double* dvert) const { double surface = 0; CoM[0] = CoM[1] = CoM[2] = 0; for (int i = 0; i < nface(); i++) { - // get area and center double area, center[3]; - area = triangle(nullptr, center, &vert_[3*face_[3*i]], - &vert_[3*face_[3*i + 1]], &vert_[3*face_[3*i + 2]]); + area = triangle(nullptr, center, &dvert[3*face_[3*i]], + &dvert[3*face_[3*i + 1]], &dvert[3*face_[3*i + 2]]); // add pyramid com surface += area; @@ -1267,28 +1250,24 @@ double mjCMesh::ComputeSurfaceArea(double CoM[3], const double facecen[3]) const // apply transformations -void mjCMesh::ApplyTransformations() { - // translate +void mjCMesh::ApplyTransformations(double* dvert) { if (refpos[0] != 0 || refpos[1] != 0 || refpos[2] != 0) { int nv = nvert(); for (int i = 0; i < nv; i++) { - vert_[3*i + 0] -= refpos[0]; - vert_[3*i + 1] -= refpos[1]; - vert_[3*i + 2] -= refpos[2]; + dvert[3*i + 0] -= refpos[0]; + dvert[3*i + 1] -= refpos[1]; + dvert[3*i + 2] -= refpos[2]; } } - // rotate if (refquat[0] != 1 || refquat[1] != 0 || refquat[2] != 0 || refquat[3] != 0) { - // prepare rotation double quat[4] = {refquat[0], refquat[1], refquat[2], refquat[3]}; double mat[9]; mjuu_normvec(quat, 4); mjuu_quat2mat(mat, quat); - // process vertices for (int i = 0; i < nvert(); i++) { - mjuu_mulvecmatT(&vert_[3*i], &vert_[3*i], mat); + mjuu_mulvecmatT(&dvert[3*i], &dvert[3*i], mat); } // process normals @@ -1304,9 +1283,9 @@ void mjCMesh::ApplyTransformations() { // scale if (scale[0] != 1 || scale[1] != 1 || scale[2] != 1) { for (int i = 0; i < nvert(); i++) { - vert_[3*i + 0] *= scale[0]; - vert_[3*i + 1] *= scale[1]; - vert_[3*i + 2] *= scale[2]; + dvert[3*i + 0] *= scale[0]; + dvert[3*i + 1] *= scale[1]; + dvert[3*i + 2] *= scale[2]; } for (int i = 0; i < nnormal(); i++) { @@ -1338,14 +1317,13 @@ void mjCMesh::ApplyTransformations() { // find centroid of faces, return total area -double mjCMesh::ComputeFaceCentroid(double facecen[3]) const { +double mjCMesh::ComputeFaceCentroid(double facecen[3], const double* dvert) const { double total_area = 0; for (int i = 0; i < nface(); i++) { - // get area and center double area, center[3]; - area = triangle(nullptr, center, &vert_[3*face_[3*i]], - &vert_[3*face_[3*i + 1]], &vert_[3*face_[3*i + 2]]); + area = triangle(nullptr, center, &dvert[3*face_[3*i]], + &dvert[3*face_[3*i + 1]], &dvert[3*face_[3*i + 2]]); // accumulate facecen[0] += area * center[0]; @@ -1366,13 +1344,15 @@ double mjCMesh::ComputeFaceCentroid(double facecen[3]) const { void mjCMesh::Process() { + std::vector dvert(vert_.begin(), vert_.end()); + // create half-edge structure (if mesh was in XML) if (halfedge_.empty()) { for (int i = 0; i < nface(); i++) { int v0 = face_[3*i + 0]; int v1 = face_[3*i + 1]; int v2 = face_[3*i + 2]; - if (triangle(nullptr, nullptr, &vert_[3*v0], &vert_[3*v1], &vert_[3*v2]) > sqrt(mjMINVAL)) { + if (triangle(nullptr, nullptr, &dvert[3*v0], &dvert[3*v1], &dvert[3*v2]) > sqrt(mjMINVAL)) { halfedge_.push_back({v0, v1}); halfedge_.push_back({v1, v2}); halfedge_.push_back({v2, v0}); @@ -1396,7 +1376,7 @@ void mjCMesh::Process() { // make graph describing convex hull if (needhull_ || face_.empty()) { - MakeGraph(); + MakeGraph(dvert.data()); } // no faces: copy from convex hull @@ -1406,7 +1386,7 @@ void mjCMesh::Process() { // no normals: make if (normal_.empty()) { - MakeNormal(); + MakeNormal(dvert.data()); } // check facenormal size @@ -1429,17 +1409,31 @@ void mjCMesh::Process() { } if (szgraph_) { - MakePolygons(); + MakePolygons(dvert.data()); } else { polygon_map_.resize(nvert()); } + // correct winding order for left-handed coordinate systems + bool righthand = scale[0] * scale[1] * scale[2] > 0; + if (!righthand) { + for (size_t i = 0; i < face_.size(); i += 3) { + std::swap(face_[i + 1], face_[i + 2]); + } + for (size_t i = 0; i < facenormal_.size(); i += 3) { + std::swap(facenormal_[i + 1], facenormal_[i + 2]); + } + for (size_t i = 0; i < facetexcoord_.size(); i += 3) { + std::swap(facetexcoord_[i + 1], facetexcoord_[i + 2]); + } + } + // user offset, rotation, scaling - ApplyTransformations(); + ApplyTransformations(dvert.data()); // find centroid of faces double facecen[3] = {0, 0, 0}; - if (ComputeFaceCentroid(facecen) < mjMINVAL) { + if (ComputeFaceCentroid(facecen, dvert.data()) < mjMINVAL) { throw mjCError(this, "mesh surface area is too small: %s", name.c_str()); } @@ -1450,12 +1444,12 @@ void mjCMesh::Process() { // compute CoM and volume/area if (inertia == mjMESH_INERTIA_SHELL) { - surface_ = ComputeSurfaceArea(CoM, facecen); + surface_ = ComputeSurfaceArea(CoM, facecen, dvert.data()); if (surface_ < mjMINVAL) { throw mjCError(this, "mesh surface area is too small: %s", name.c_str()); } } else { - if ((volume_ = ComputeVolume(CoM, facecen)) < mjMINVAL) { + if ((volume_ = ComputeVolume(CoM, facecen, dvert.data())) < mjMINVAL) { if (volume_ < 0) { throw mjCError(this, "mesh volume is negative (misoriented triangles): %s", name.c_str()); } else { @@ -1466,7 +1460,7 @@ void mjCMesh::Process() { } // compute inertia - double total_volume = ComputeInertia(inert, CoM); + double total_volume = ComputeInertia(inert, CoM, dvert.data()); if (inertia == mjMESH_INERTIA_SHELL) { surface_ = total_volume; } else { @@ -1510,11 +1504,11 @@ void mjCMesh::Process() { // transform CoM to origin for (int i=0; i < nvert(); i++) { - vert_[3*i + 0] -= CoM[0]; - vert_[3*i + 1] -= CoM[1]; - vert_[3*i + 2] -= CoM[2]; + dvert[3*i + 0] -= CoM[0]; + dvert[3*i + 1] -= CoM[1]; + dvert[3*i + 2] -= CoM[2]; } - Rotate(quattmp); + Rotate(quattmp, dvert.data()); // save the pos and quat that was used to transform the mesh mjuu_copyvec(pos_, CoM, 3); @@ -1524,11 +1518,11 @@ void mjCMesh::Process() { // no radii: make if (!center_) { - MakeCenter(); + MakeCenter(dvert.data()); } // recompute polygon normals - MakePolygonNormals(); + MakePolygonNormals(dvert.data()); // make bounding volume hierarchy if (tree_.Bvh().empty()) { @@ -1536,16 +1530,31 @@ void mjCMesh::Process() { face_aabb_.reserve(3*face_.size()); tree_.AllocateBoundingVolumes(nface()); for (int i = 0; i < nface(); i++) { - SetBoundingVolume(i); + SetBoundingVolume(i, dvert.data()); } tree_.CreateBVH(); } + + // make octree + if (needsdf) { + octree_.SetFace(dvert, face_); + octree_.CreateOctree(aamm_); + + if (!plugin.active) { + octree_.ComputeSdfCoeffs(dvert.data(), nvert(), face_.data(), nface(), tree_); + } + } + + // narrow back to float + for (int i = 0; i < (int)dvert.size(); i++) { + vert_[i] = (float)dvert[i]; + } } // compute abstract (unitless) inertia, recompute area / volume -double mjCMesh::ComputeInertia(double inert[6], const double CoM[3]) const { +double mjCMesh::ComputeInertia(double inert[6], const double CoM[3], const double* dvert) const { double total_volume = 0; // copy vertices to avoid modifying the original mesh @@ -1554,9 +1563,9 @@ double mjCMesh::ComputeInertia(double inert[6], const double CoM[3]) const { // translate vertices to origin in order to compute inertia for (int i = 0; i < nvert(); i++) { - vert_centered.push_back(vert_[3*i + 0] - CoM[0]); - vert_centered.push_back(vert_[3*i + 1] - CoM[1]); - vert_centered.push_back(vert_[3*i + 2] - CoM[2]); + vert_centered.push_back(dvert[3*i + 0] - CoM[0]); + vert_centered.push_back(dvert[3*i + 1] - CoM[1]); + vert_centered.push_back(dvert[3*i + 2] - CoM[2]); } // accumulate products of inertia, recompute volume @@ -1611,22 +1620,20 @@ double mjCMesh::ComputeInertia(double inert[6], const double CoM[3]) const { -void mjCMesh::Rotate(double quat[4]) { - // rotate vertices and normals of mesh by quaternion +void mjCMesh::Rotate(double quat[4], double* dvert) { double neg[4] = {quat[0], -quat[1], -quat[2], -quat[3]}; double mat[9]; mjuu_quat2mat(mat, neg); int nv = nvert(); for (int i = 0; i < nv; i++) { - mjuu_mulvecmat(&vert_[3*i], &vert_[3*i], mat); + mjuu_mulvecmat(&dvert[3*i], &dvert[3*i], mat); - // axis-aligned bounding box - aamm_[0] = std::min(aamm_[0], vert_[3*i + 0]); - aamm_[3] = std::max(aamm_[3], vert_[3*i + 0]); - aamm_[1] = std::min(aamm_[1], vert_[3*i + 1]); - aamm_[4] = std::max(aamm_[4], vert_[3*i + 1]); - aamm_[2] = std::min(aamm_[2], vert_[3*i + 2]); - aamm_[5] = std::max(aamm_[5], vert_[3*i + 2]); + aamm_[0] = std::min(aamm_[0], dvert[3*i + 0]); + aamm_[3] = std::max(aamm_[3], dvert[3*i + 0]); + aamm_[1] = std::min(aamm_[1], dvert[3*i + 1]); + aamm_[4] = std::max(aamm_[4], dvert[3*i + 1]); + aamm_[2] = std::min(aamm_[2], dvert[3*i + 2]); + aamm_[5] = std::max(aamm_[5], dvert[3*i + 2]); } for (int i=0; i < nnormal(); i++) { @@ -1698,7 +1705,7 @@ double mjCMesh::GetVolumeRef() const { // make graph describing convex hull -void mjCMesh::MakeGraph() { +void mjCMesh::MakeGraph(const double* dvert) { int adr, ok, curlong, totlong, exitcode; facetT* facet, **facetp; vertexT* vertex, *vertex1, **vertex1p; @@ -1727,7 +1734,7 @@ void mjCMesh::MakeGraph() { if (!exitcode) { // actual init qh_initflags(qh, const_cast(qhopt.c_str())); - qh_init_B(qh, vert_.data(), nvert(), 3, qh_False); + qh_init_B(qh, const_cast(dvert), nvert(), 3, qh_False); // construct convex hull qh_qhull(qh); @@ -2402,7 +2409,7 @@ void mjCMesh::MakeCone(int nedge, double radius) { // compute vertex normals -void mjCMesh::MakeNormal() { +void mjCMesh::MakeNormal(const double* dvert) { // only if normal data is missing if (!normal_.empty()) { return; @@ -2426,8 +2433,8 @@ void mjCMesh::MakeNormal() { // get triangle edges double vec01[3], vec02[3]; for (int j=0; j < 3; j++) { - vec01[j] = vert_[3*vertid[1]+j] - vert_[3*vertid[0]+j]; - vec02[j] = vert_[3*vertid[2]+j] - vert_[3*vertid[0]+j]; + vec01[j] = dvert[3*vertid[1]+j] - dvert[3*vertid[0]+j]; + vec02[j] = dvert[3*vertid[2]+j] - dvert[3*vertid[0]+j]; } // compute face normal @@ -2461,8 +2468,8 @@ void mjCMesh::MakeNormal() { // get triangle edges double vec01[3], vec02[3]; for (int j=0; j < 3; j++) { - vec01[j] = vert_[3*vertid[1]+j] - vert_[3*vertid[0]+j]; - vec02[j] = vert_[3*vertid[2]+j] - vert_[3*vertid[0]+j]; + vec01[j] = dvert[3*vertid[1]+j] - dvert[3*vertid[0]+j]; + vec02[j] = dvert[3*vertid[2]+j] - dvert[3*vertid[0]+j]; } // compute face normal @@ -2514,7 +2521,7 @@ void mjCMesh::MakeNormal() { // compute face circumradii -void mjCMesh::MakeCenter() { +void mjCMesh::MakeCenter(const double* dvert) { if (center_) { return; } @@ -2530,8 +2537,8 @@ void mjCMesh::MakeCenter() { // get triangle edges double a[3], b[3]; for (int j=0; j < 3; j++) { - a[j] = vert_[3*vertid[0]+j] - vert_[3*vertid[2]+j]; - b[j] = vert_[3*vertid[1]+j] - vert_[3*vertid[2]+j]; + a[j] = dvert[3*vertid[0]+j] - dvert[3*vertid[2]+j]; + b[j] = dvert[3*vertid[1]+j] - dvert[3*vertid[2]+j]; } // compute face normal @@ -2550,20 +2557,20 @@ void mjCMesh::MakeCenter() { norm_a_2 * b[2] - norm_b_2 * a[2] }; mjuu_crossvec(res, vec, nrm); - center_[3*i+0] = res[0]/(2*area*area) + vert_[3*vertid[2]+0]; - center_[3*i+1] = res[1]/(2*area*area) + vert_[3*vertid[2]+1]; - center_[3*i+2] = res[2]/(2*area*area) + vert_[3*vertid[2]+2]; + center_[3*i+0] = res[0]/(2*area*area) + dvert[3*vertid[2]+0]; + center_[3*i+1] = res[1]/(2*area*area) + dvert[3*vertid[2]+1]; + center_[3*i+2] = res[2]/(2*area*area) + dvert[3*vertid[2]+2]; } } // compute the normals of the polygons -void mjCMesh::MakePolygonNormals() { +void mjCMesh::MakePolygonNormals(const double* dvert) { for (int i = 0; i < polygons_.size(); ++i) { double n[3]; - mjuu_makenormal(n, &vert_[3*polygons_[i][0]], &vert_[3*polygons_[i][1]], - &vert_[3*polygons_[i][2]]); + mjuu_makenormal(n, &dvert[3*polygons_[i][0]], &dvert[3*polygons_[i][1]], + &dvert[3*polygons_[i][2]]); polygon_normals_[3*i + 0] = n[0]; polygon_normals_[3*i + 1] = n[1]; polygon_normals_[3*i + 2] = n[2]; @@ -2802,7 +2809,7 @@ struct PairHash { // merge coplanar mesh triangular faces into polygonal sides to represent the geometry of the mesh -void mjCMesh::MakePolygons() { +void mjCMesh::MakePolygons(const double* dvert) { constexpr double kAngleTol = 0.01; std::unordered_map, MeshPolygon, PairHash> mesh_polygons; polygons_.clear(); @@ -2819,9 +2826,9 @@ void mjCMesh::MakePolygons() { int vi1 = faces[3*i + 0]; int vi2 = faces[3*i + 1]; int vi3 = faces[3*i + 2]; - double* v1 = &vert_[3*vi1]; - double* v2 = &vert_[3*vi2]; - double* v3 = &vert_[3*vi3]; + const double* v1 = &dvert[3*vi1]; + const double* v2 = &dvert[3*vi2]; + const double* v3 = &dvert[3*vi3]; std::pair key; if (!MeshPolygonKey(key, v1, v2, v3, kAngleTol)) { diff --git a/src/user/user_objects.cc b/src/user/user_objects.cc index 718c9f70..30c1c525 100644 --- a/src/user/user_objects.cc +++ b/src/user/user_objects.cc @@ -587,6 +587,7 @@ void mjCOctree::CopyCoeff(mjtNum* coeff) const { void mjCOctree::SetFace(const std::vector& vert, const std::vector& face) { + face_.reserve(face.size()/3); for (int i = 0; i < face.size(); i += 3) { std::array v0 = {vert[3*face[i+0]], vert[3*face[i+0]+1], vert[3*face[i+0]+2]}; std::array v1 = {vert[3*face[i+1]], vert[3*face[i+1]+1], vert[3*face[i+1]+2]}; @@ -613,6 +614,8 @@ void mjCOctree::Make(std::vector& elements) { void mjCOctree::CreateOctree(const double aamm[6]) { + Clear(); + double aabb[6] = {(aamm[0] + aamm[3]) / 2, (aamm[1] + aamm[4]) / 2, (aamm[2] + aamm[5]) / 2, (aamm[3] - aamm[0]) / 2, (aamm[4] - aamm[1]) / 2, (aamm[5] - aamm[2]) / 2}; double box[6] = {aabb[0] - 1.1 * aabb[3], aabb[1] - 1.1 * aabb[4], aabb[2] - 1.1 * aabb[5], diff --git a/src/user/user_objects.h b/src/user/user_objects.h index 88599740..232d0470 100644 --- a/src/user/user_objects.h +++ b/src/user/user_objects.h @@ -299,8 +299,11 @@ class mjCOctree : public mjCOctree_ { sizeof(Point) * vert_.size(); } void Clear() { + nnode_ = 0; + nvert_ = 0; node_.clear(); - face_.clear(); + vert_.clear(); + hang_.clear(); } void AddCoeff(int n, int v, double coeff) { node_[n].coeff[v] = coeff; } double Coeff(int n, int v) const { return node_[n].coeff[v]; } @@ -1071,7 +1074,7 @@ class mjCMesh_ : public mjCBase { std::string content_type_ = ""; // content type of file std::string file_; // mesh file mjResource* resource_ = nullptr; // resource for mesh file - std::vector vert_; // vertex data + std::vector vert_; // vertex data std::vector normal_; // normal data std::vector texcoord_; // texcoord data std::vector face_; // vertex indices @@ -1156,8 +1159,8 @@ class mjCMesh: public mjCMesh_, private mjsMesh { const double* Refquat() const { return refquat; } const double* Scale() const { return scale; } bool SmoothNormal() const { return smoothnormal; } - const std::vector& Vert() const { return vert_; } - double Vert(int i) const { return vert_[i]; } + const std::vector& Vert() const { return vert_; } + float Vert(int i) const { return vert_[i]; } const std::vector& UserVert() const { return spec_vert_; } const std::vector& UserNormal() const { return spec_normal_; } const std::vector& Texcoord() const { return texcoord_; } @@ -1233,7 +1236,7 @@ class mjCMesh: public mjCMesh_, private mjsMesh { void CopyPolygonNormals(mjtNum* arr); // sets properties of a bounding volume given a face id - void SetBoundingVolume(int faceid); + void SetBoundingVolume(int faceid, const double* dvert); // load from OBJ, STL, or MSH file; throws mjCError on failure void LoadFromResource(mjResource* resource, bool remove_repeated = false); @@ -1264,22 +1267,22 @@ class mjCMesh: public mjCMesh_, private mjsMesh { void LoadMSH(mjResource* resource, bool remove_repeated); // load mesh in MSH BIN format void LoadSDF(); // generate mesh using marching cubes - void MakeGraph(); // make graph of convex hull + void MakeGraph(const double* dvert); // make graph of convex hull void CopyGraph(); // copy graph into face data - void MakeNormal(); // compute vertex normals - void MakeCenter(); // compute face circumcircle data + void MakeNormal(const double* dvert); // compute vertex normals + void MakeCenter(const double* dvert); // compute face circumcircle data void Process(); // compute inertial properties - void ApplyTransformations(); // apply user transformations - double ComputeFaceCentroid(double[3]) const; // compute centroid of all faces + void ApplyTransformations(double* dvert); // apply user transformations + double ComputeFaceCentroid(double[3], const double* dvert) const; void CheckInitialMesh() const; // check if initial mesh is valid void CopyPlugin(); - void Rotate(double quat[4]); // rotate mesh by quaternion + void Rotate(double quat[4], double* dvert); // rotate mesh by quaternion void Transform(double pos[3], double quat[4]); // transform mesh by position and quaternion - void MakePolygons(); // compute the polygon sides of the mesh - void MakePolygonNormals(); // compute the normals of the polygons + void MakePolygons(const double* dvert); // compute the polygon sides of the mesh + void MakePolygonNormals(const double* dvert); // compute the normals of the polygons // computes the inertia matrix of the mesh given the type of inertia - double ComputeInertia(double inert[6], const double CoM[3]) const; + double ComputeInertia(double inert[6], const double CoM[3], const double* dvert) const; int* GraphFaces() const { return graph_ + 2 + 3*(graph_[0] + graph_[1]); @@ -1295,9 +1298,8 @@ class mjCMesh: public mjCMesh_, private mjsMesh { std::vector> polygon_map_; // map from vertex to polygon // compute the volume and center-of-mass of the mesh given the face centroid - double ComputeVolume(double CoM[3], const double facecen[3]) const; - // compute the surface area and center-of-mass of the mesh given the face centroid - double ComputeSurfaceArea(double CoM[3], const double facecen[3]) const; + double ComputeVolume(double CoM[3], const double facecen[3], const double* dvert) const; + double ComputeSurfaceArea(double CoM[3], const double facecen[3], const double* dvert) const; }; diff --git a/src/user/user_resource.cc b/src/user/user_resource.cc index 072df4ad..5f5ce4c6 100644 --- a/src/user/user_resource.cc +++ b/src/user/user_resource.cc @@ -44,9 +44,9 @@ mjResource* mju_openResource(const char* dir, const char* name, if (non_const_vfs == nullptr) { mjVFS* local_vfs = (mjVFS*)mju_malloc(sizeof(mjVFS)); mj_defaultVFS(local_vfs); - mujoco::user::VFS::Upcast(local_vfs)->SetToSelfDestruct([](mjVFS* ptr) { - mj_deleteVFS(ptr); - mju_free(ptr); + mujoco::user::VFS::Upcast(local_vfs)->SetToSelfDestruct([=]() { + mj_deleteVFS(local_vfs); + mju_free(local_vfs); }); non_const_vfs = local_vfs; diff --git a/src/user/user_vfs.cc b/src/user/user_vfs.cc index c9e15660..11d35c71 100644 --- a/src/user/user_vfs.cc +++ b/src/user/user_vfs.cc @@ -105,7 +105,8 @@ std::string StripPathAndLower(std::string path) { namespace mujoco::user { -VFS::VFS(mjVFS* vfs) : self_(vfs) { +VFS::VFS(mjVFS* vfs) { + wrapped_vfs_.impl_ = this; mjp_defaultResourceProvider(&default_provider_); default_provider_.open = [](mjResource* res) { return OpenFile(res->name, res); @@ -121,7 +122,7 @@ VFS::VFS(mjVFS* vfs) : self_(vfs) { }; default_provider_.prefix = nullptr; - default_mount_.vfs = self_; + default_mount_.vfs = &wrapped_vfs_; default_mount_.provider = &default_provider_; default_mount_.data = nullptr; default_mount_.name = nullptr; @@ -245,7 +246,7 @@ int VFS::Read(mjResource* resource, const void** buffer) { VFS::ResourcePtr VFS::CreateResource(std::string_view name, const mjpResourceProvider* provider) { mjResource* res = new mjResource(); - res->vfs = self_; + res->vfs = &wrapped_vfs_; res->provider = provider; res->data = nullptr; res->name = new char[name.size() + 1]; @@ -312,11 +313,14 @@ mjResource* VFS::FindMount(const std::string& fullpath) { void VFS::MaybeSelfDestruct() { if (destructor_) { - destructor_(self_); + // Copy the destructor to a local variable so that we can destroy `this` + // object within the destructor. + auto fn = std::move(destructor_); + fn(); } } -void VFS::SetToSelfDestruct(std::function destructor) { +void VFS::SetToSelfDestruct(std::function destructor) { destructor_ = std::move(destructor); } diff --git a/src/user/user_vfs.h b/src/user/user_vfs.h index b8d8013f..cfa2694c 100644 --- a/src/user/user_vfs.h +++ b/src/user/user_vfs.h @@ -96,7 +96,7 @@ class VFS { // This is useful for when you want to create a temporary VFS instance with // a lifetime tied to a single mjResource to be opened. The `destructor` // should be set to `delete this` and any other cleanup that needs to happen. - void SetToSelfDestruct(std::function destructor); + void SetToSelfDestruct(std::function destructor); // Converts the public C-API pointer to the internal C++ class. static VFS* Upcast(mjVFS* vfs); @@ -117,13 +117,13 @@ class VFS { // that `this` will be invalidated after this call. void MaybeSelfDestruct(); - mjVFS* self_; + mjVFS wrapped_vfs_; std::mutex mutex_; // Protects open_resources_ and mounts_. std::unordered_map open_resources_; std::unordered_map mounts_; mjResource default_mount_; mjpResourceProvider default_provider_; - std::function destructor_; + std::function destructor_; }; } // namespace mujoco::user diff --git a/test/user/user_mesh_test.cc b/test/user/user_mesh_test.cc index 80eb76ef..1618b789 100644 --- a/test/user/user_mesh_test.cc +++ b/test/user/user_mesh_test.cc @@ -1086,6 +1086,71 @@ TEST_F(MjCMeshTest, MeshScale) { mj_deleteModel(model); } +TEST_F(MjCMeshTest, NegativeScaleUserMeshCompiles) { + static constexpr char xml[] = R"( + + + + + + + + + )"; + char error[1024]; + mjModel* model = LoadModelFromString(xml, error, sizeof(error)); + ASSERT_THAT(model, NotNull()) << error; + mj_deleteModel(model); +} + +TEST_F(MjCMeshTest, NegativeScaleUserMeshMatchesPositiveScale) { + static constexpr char pos_xml[] = R"( + + + + + + + + + )"; + + static constexpr char neg_xml[] = R"( + + + + + + + + + )"; + + char error[1024]; + mjModel* pos_model = LoadModelFromString(pos_xml, error, sizeof(error)); + ASSERT_THAT(pos_model, NotNull()) << error; + + mjModel* neg_model = LoadModelFromString(neg_xml, error, sizeof(error)); + ASSERT_THAT(neg_model, NotNull()) << error; + + ASSERT_EQ(pos_model->nmeshface, neg_model->nmeshface); + + for (int i = 0; i < pos_model->nmeshface; i++) { + EXPECT_EQ(pos_model->mesh_face[3*i + 0], neg_model->mesh_face[3*i + 0]); + EXPECT_EQ(pos_model->mesh_face[3*i + 1], neg_model->mesh_face[3*i + 2]); + EXPECT_EQ(pos_model->mesh_face[3*i + 2], neg_model->mesh_face[3*i + 1]); + } + + mj_deleteModel(pos_model); + mj_deleteModel(neg_model); +} + TEST_F(MjCMeshTest, ShellInertiaTest) { static constexpr char xml[] = R"( diff --git a/test/user/user_vfs_test.cc b/test/user/user_vfs_test.cc index 84d750c3..db5a22b5 100644 --- a/test/user/user_vfs_test.cc +++ b/test/user/user_vfs_test.cc @@ -407,5 +407,29 @@ TEST_F(UserVfsTest, StackedMounts) { EXPECT_EQ(test2, expect2); EXPECT_EQ(test3, expect3); } + +TEST_F(UserVfsTest, MoveVfs) { + // Create and move a VFS to another address. + mjVFS* original = new mjVFS(); + mj_defaultVFS(original); + mjVFS vfs = *original; + delete original; + + std::string buffer = ""; + mj_addBufferVFS(&vfs, "model", static_cast(buffer.c_str()), + buffer.size()); + + mjResource* resource = mju_openResource("", "model", &vfs, nullptr, 0); + ASSERT_THAT(resource, NotNull()); + + const void* out = nullptr; + const int size = mju_readResource(resource, &out); + EXPECT_GT(size, 0); + EXPECT_THAT(out, NotNull()); + + mju_closeResource(resource); + mj_deleteVFS(&vfs); +} + } // namespace } // namespace mujoco diff --git a/unity/Editor/Bindings/MujocoBinaryRetriever.cs b/unity/Editor/Bindings/MujocoBinaryRetriever.cs index 78e32e15..b3840a7e 100644 --- a/unity/Editor/Bindings/MujocoBinaryRetriever.cs +++ b/unity/Editor/Bindings/MujocoBinaryRetriever.cs @@ -37,7 +37,7 @@ public class MujocoBinaryRetriever { if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/mujoco.dylib") == null) { File.Copy( "/Applications/MuJoCo.app/Contents/Frameworks" + - "/mujoco.framework/Versions/Current/libmujoco.3.6.0.dylib", + "/mujoco.framework/Versions/Current/libmujoco.3.7.0.dylib", mujocoPath + "/mujoco.dylib"); AssetDatabase.Refresh(); } @@ -45,7 +45,7 @@ public class MujocoBinaryRetriever { if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/libmujoco.so") == null) { File.Copy( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + - "/.mujoco/mujoco-3.6.0/lib/libmujoco.so.3.6.0", + "/.mujoco/mujoco-3.7.0/lib/libmujoco.so.3.7.0", mujocoPath + "/libmujoco.so"); AssetDatabase.Refresh(); } diff --git a/unity/Runtime/Bindings/MjBindings.cs b/unity/Runtime/Bindings/MjBindings.cs index 4e9b9f75..dbdbd276 100644 --- a/unity/Runtime/Bindings/MjBindings.cs +++ b/unity/Runtime/Bindings/MjBindings.cs @@ -112,7 +112,7 @@ public const int mjMAXLINEPNT = 1001; public const int mjMAXPLANEGRID = 200; public const bool THIRD_PARTY_MUJOCO_MJXMACRO_H_ = true; public const bool THIRD_PARTY_MUJOCO_MUJOCO_H_ = true; -public const int mjVERSION_HEADER = 3006000; +public const int mjVERSION_HEADER = 3007000; // ------------------------------------Enums------------------------------------ diff --git a/unity/package.json b/unity/package.json index 54954dc2..67cf4330 100644 --- a/unity/package.json +++ b/unity/package.json @@ -1,7 +1,7 @@ { "name": "org.mujoco", "displayName": "MuJoCo", - "version": "3.6.0", + "version": "3.7.0", "description": "MuJoCo importer and runtime plug-in", "dependencies": {}, "author": { diff --git a/wasm/README.md b/wasm/README.md index 2938ec5c..1cf0ac2e 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -307,7 +307,7 @@ The function `mjv_updateScene` populates an `mjvScene` object with information from `mjModel` and `mjData`. ```typescript // Create instances of the necessary structs. -const model = mujoco.MjModel.loadFromXML(xmlContent); +const model = mujoco.MjModel.from_xml_string(xmlContent); const data = new mujoco.MjData(model); const scene = new mujoco.MjvScene(model, 1000); const option = new mujoco.MjvOption(); @@ -348,23 +348,31 @@ mujoco.mjtDisableBit.mjDSBL_CLAMPCTRL.value ``` ### Constants -Scalar constants will be accessed the same way they are on python, simply: +Scalar constants can be accessed as properties: ```javascript mujoco.mjNEQDATA ``` -Due to Embind limitations, more complex constants that are not scalar, but are -represented in more dimensions are exposed as functions. E.g. to use -`mujoco.mjFRAMESTRING` you will need to call a function: +Non-scalar constants like `mjFRAMESTRING` are also accessed as properties, and +return JavaScript arrays: ```javascript -mujoco.get_mjFRAMESTRING() +mujoco.mjFRAMESTRING ``` This will return a javascript array representation of the values in MuJoCo `mjFRAMESTRING`. +> [!NOTE] +> You will notice constants like `mjFRAMESTRING` are typed as `any`. This is +> because they are bound using `emscripten::val::array()` in C++, and Embind +> maps `emscripten::val` to `any` in TypeScript definition files. While +> `EMSCRIPTEN_DECLARE_VAL_TYPE(StringArray)` could be used to define +> `StringArray` as an alias for `emscripten::val` and hint to Embind how to +> handle conversions in function signatures or when using `.as()` it does not +> change how `emscripten::constant` infers types for properties. + ## Development In order to change the bindings you will need to change the [`bindings.cc`](codegen/generated/bindings.cc) diff --git a/wasm/codegen/generated/bindings.cc b/wasm/codegen/generated/bindings.cc index 9c83a652..4cfa9bd8 100644 --- a/wasm/codegen/generated/bindings.cc +++ b/wasm/codegen/generated/bindings.cc @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -8361,6 +8362,39 @@ std::unique_ptr mj_loadModel_wrapper(std::string filename, const MjVFS& return std::unique_ptr(new MjModel(model)); } +std::unique_ptr from_xml_string_wrapper_1(const std::string& xml) { + mjVFS vfs; + mj_defaultVFS(&vfs); + const char* filename = "model.xml"; + int add_result = mj_addBufferVFS(&vfs, filename, xml.c_str(), xml.length()); + if (add_result != 0) { + mj_deleteVFS(&vfs); + mju_error("Could not add XML string to VFS: %d", add_result); + } + char error[1000]; + mjModel* model = mj_loadXML(filename, &vfs, error, sizeof(error)); + mj_deleteVFS(&vfs); + if (!model) { + mju_error("Loading error: %s\n", error); + } + return std::unique_ptr(new MjModel(model)); +} + +std::unique_ptr from_xml_string_wrapper_2(const std::string& xml, const MjVFS& vfs) { + std::string filename = "model.xml"; + int add_result = mj_addBufferVFS(vfs.get(), filename.c_str(), xml.c_str(), xml.length()); + if (add_result != 0) { + mju_error("Could not add XML string to VFS: %d", add_result); + } + char error[1000]; + mjModel* model = mj_loadXML(filename.c_str(), vfs.get(), error, sizeof(error)); + mj_deleteFileVFS(vfs.get(), filename.c_str()); + if (!model) { + mju_error("Loading error: %s\n", error); + } + return std::unique_ptr(new MjModel(model)); +} + std::unique_ptr parseXMLString_wrapper(const std::string &xml) { char error[1000]; mjSpec *ptr = mj_parseXMLString(xml.c_str(), nullptr, error, sizeof(error)); @@ -8599,7 +8633,7 @@ void mj_fwdVelocity_wrapper(const MjModel& m, MjData& d) { mj_fwdVelocity(m.get(), d.get()); } -mjtNum mj_geomDistance_wrapper(const MjModel& m, const MjData& d, int geom1, int geom2, mjtNum distmax, const val& fromto) { +mjtNum mj_geomDistance_wrapper(const MjModel& m, MjData& d, int geom1, int geom2, mjtNum distmax, const val& fromto) { UNPACK_NULLABLE_VALUE(mjtNum, fromto); CHECK_SIZE(fromto, 6); return mj_geomDistance(m.get(), d.get(), geom1, geom2, distmax, fromto_.data()); @@ -11596,9 +11630,16 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .property("useexisting", &MjLROpt::useexisting, &MjLROpt::set_useexisting, reference()) .property("uselimit", &MjLROpt::uselimit, &MjLROpt::set_uselimit, reference()); emscripten::class_("MjModel") + // mj_loadXML is deprecated and will be removed in a future release .class_function("mj_loadXML", emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1)) .class_function("mj_loadXML", emscripten::select_overload(std::string, const MjVFS&)>(&mj_loadXML_wrapper_2)) + // mj_loadModel is deprecated and will be removed in a future release .class_function("mj_loadModel", &mj_loadModel_wrapper) + .class_function("from_binary_path", &mj_loadModel_wrapper) + .class_function("from_xml_string", emscripten::select_overload(const std::string&)>(&from_xml_string_wrapper_1)) + .class_function("from_xml_string", emscripten::select_overload(const std::string&, const MjVFS&)>(&from_xml_string_wrapper_2)) + .class_function("from_xml_path", emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1)) + .class_function("from_xml_path", emscripten::select_overload(std::string, const MjVFS&)>(&mj_loadXML_wrapper_2)) .constructor() // Binds the functions on MjModel that return accessors. #define X_ACCESSOR(NAME, Name, OBJTYPE, field_name, nfield) \ @@ -13379,6 +13420,8 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { // as using std::optional caused memory errors due to missing copy/move constructors. function("mj_compile", emscripten::select_overload(const MjSpec&)>(&mj_compile_wrapper_1)); function("mj_compile", emscripten::select_overload(const MjSpec&, const MjVFS&)>(&mj_compile_wrapper_2)); + function("from_xml_string", emscripten::select_overload(const std::string&)>(&from_xml_string_wrapper_1)); + function("from_xml_string", emscripten::select_overload(const std::string&, const MjVFS&)>(&from_xml_string_wrapper_2)); emscripten::class_>("FloatBuffer") .constructor() @@ -13409,6 +13452,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .function("GetView", &WasmBuffer::GetView); emscripten::register_vector("mjStringVec"); + emscripten::register_vector>("mjStringVecVec"); emscripten::register_vector("mjIntVec"); emscripten::register_vector("mjIntVecVec"); emscripten::register_vector("mjFloatVec"); @@ -13453,7 +13497,6 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { emscripten::constant("mjPI", mjPI); emscripten::constant("mjVERSION_HEADER", mjVERSION_HEADER); - // These complex constants are bound using function() rather than constant() emscripten::function("get_mjDISABLESTRING", &get_mjDISABLESTRING); emscripten::function("get_mjENABLESTRING", &get_mjENABLESTRING); emscripten::function("get_mjFRAMESTRING", &get_mjFRAMESTRING); @@ -13461,6 +13504,31 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { emscripten::function("get_mjRNDSTRING", &get_mjRNDSTRING); emscripten::function("get_mjTIMERSTRING", &get_mjTIMERSTRING); emscripten::function("get_mjVISSTRING", &get_mjVISSTRING); + // Bind these complex constants as properties on the module object. + // We use emscripten::constant with emscripten::val::array() to type them + // as `any` in TypeScript. At runtime, the EM_ASM block below overrides + // these properties with getters that return native JavaScript arrays + // (string[] or string[][]) via the get_ functions above, which is more + // performant and idiomatic than vector wrappers. + emscripten::constant("mjDISABLESTRING", emscripten::val::array()); + emscripten::constant("mjENABLESTRING", emscripten::val::array()); + emscripten::constant("mjFRAMESTRING", emscripten::val::array()); + emscripten::constant("mjLABELSTRING", emscripten::val::array()); + emscripten::constant("mjRNDSTRING", emscripten::val::array()); + emscripten::constant("mjTIMERSTRING", emscripten::val::array()); + emscripten::constant("mjVISSTRING", emscripten::val::array()); + EM_ASM({ + if (typeof Module !== "undefined") { + "mjDISABLESTRING mjENABLESTRING mjFRAMESTRING mjLABELSTRING mjRNDSTRING mjTIMERSTRING mjVISSTRING".split(" ").forEach(function(name) { + Object.defineProperty(Module, name, { + get: function() { return Module["get_" + name](); }, + set: function(v) { }, + enumerable: true, + configurable: true + }); + }); + } + }); } } // namespace mujoco::wasm diff --git a/wasm/codegen/generators/structs.py b/wasm/codegen/generators/structs.py index 0517dba9..24dd291f 100644 --- a/wasm/codegen/generators/structs.py +++ b/wasm/codegen/generators/structs.py @@ -549,6 +549,9 @@ def _build_struct_bindings( MJDATA_ACCESSORS #undef X_ACCESSOR""".lstrip()) elif w == "MjModel": + builder.line( + "// mj_loadXML is deprecated and will be removed in a future release" + ) builder.line( '.class_function("mj_loadXML",' " emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1))" @@ -561,7 +564,31 @@ def _build_struct_bindings( f2 = common.wrapped_function_name( introspect_functions.FUNCTIONS["mj_loadModel"] ) + builder.line( + "// mj_loadModel is deprecated and will be removed in a future" + " release" + ) builder.line(f'.class_function("mj_loadModel", &{f2})') + builder.line(f'.class_function("from_binary_path", &{f2})') + builder.line( + '.class_function("from_xml_string",' + " emscripten::select_overload(const" + " std::string&)>(&from_xml_string_wrapper_1))" + ) + builder.line( + '.class_function("from_xml_string",' + " emscripten::select_overload(const" + " std::string&, const MjVFS&)>(&from_xml_string_wrapper_2))" + ) + builder.line( + '.class_function("from_xml_path",' + " emscripten::select_overload(std::string)>(&mj_loadXML_wrapper_1))" + ) + builder.line( + '.class_function("from_xml_path",' + " emscripten::select_overload(std::string," + " const MjVFS&)>(&mj_loadXML_wrapper_2))" + ) builder.line(".constructor()") builder.line(""" // Binds the functions on MjModel that return accessors. diff --git a/wasm/codegen/templates/bindings.cc b/wasm/codegen/templates/bindings.cc index 8f2a1ecf..05694fb6 100644 --- a/wasm/codegen/templates/bindings.cc +++ b/wasm/codegen/templates/bindings.cc @@ -17,6 +17,7 @@ #include #include +#include #include #include @@ -746,6 +747,39 @@ std::unique_ptr mj_loadModel_wrapper(std::string filename, const MjVFS& return std::unique_ptr(new MjModel(model)); } +std::unique_ptr from_xml_string_wrapper_1(const std::string& xml) { + mjVFS vfs; + mj_defaultVFS(&vfs); + const char* filename = "model.xml"; + int add_result = mj_addBufferVFS(&vfs, filename, xml.c_str(), xml.length()); + if (add_result != 0) { + mj_deleteVFS(&vfs); + mju_error("Could not add XML string to VFS: %d", add_result); + } + char error[1000]; + mjModel* model = mj_loadXML(filename, &vfs, error, sizeof(error)); + mj_deleteVFS(&vfs); + if (!model) { + mju_error("Loading error: %s\n", error); + } + return std::unique_ptr(new MjModel(model)); +} + +std::unique_ptr from_xml_string_wrapper_2(const std::string& xml, const MjVFS& vfs) { + std::string filename = "model.xml"; + int add_result = mj_addBufferVFS(vfs.get(), filename.c_str(), xml.c_str(), xml.length()); + if (add_result != 0) { + mju_error("Could not add XML string to VFS: %d", add_result); + } + char error[1000]; + mjModel* model = mj_loadXML(filename.c_str(), vfs.get(), error, sizeof(error)); + mj_deleteFileVFS(vfs.get(), filename.c_str()); + if (!model) { + mju_error("Loading error: %s\n", error); + } + return std::unique_ptr(new MjModel(model)); +} + std::unique_ptr parseXMLString_wrapper(const std::string &xml) { char error[1000]; mjSpec *ptr = mj_parseXMLString(xml.c_str(), nullptr, error, sizeof(error)); @@ -845,6 +879,8 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { // as using std::optional caused memory errors due to missing copy/move constructors. function("mj_compile", emscripten::select_overload(const MjSpec&)>(&mj_compile_wrapper_1)); function("mj_compile", emscripten::select_overload(const MjSpec&, const MjVFS&)>(&mj_compile_wrapper_2)); + function("from_xml_string", emscripten::select_overload(const std::string&)>(&from_xml_string_wrapper_1)); + function("from_xml_string", emscripten::select_overload(const std::string&, const MjVFS&)>(&from_xml_string_wrapper_2)); emscripten::class_>("FloatBuffer") .constructor() @@ -875,6 +911,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { .function("GetView", &WasmBuffer::GetView); emscripten::register_vector("mjStringVec"); + emscripten::register_vector>("mjStringVecVec"); emscripten::register_vector("mjIntVec"); emscripten::register_vector("mjIntVecVec"); emscripten::register_vector("mjFloatVec"); @@ -919,7 +956,6 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { emscripten::constant("mjPI", mjPI); emscripten::constant("mjVERSION_HEADER", mjVERSION_HEADER); - // These complex constants are bound using function() rather than constant() emscripten::function("get_mjDISABLESTRING", &get_mjDISABLESTRING); emscripten::function("get_mjENABLESTRING", &get_mjENABLESTRING); emscripten::function("get_mjFRAMESTRING", &get_mjFRAMESTRING); @@ -927,6 +963,31 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) { emscripten::function("get_mjRNDSTRING", &get_mjRNDSTRING); emscripten::function("get_mjTIMERSTRING", &get_mjTIMERSTRING); emscripten::function("get_mjVISSTRING", &get_mjVISSTRING); + // Bind these complex constants as properties on the module object. + // We use emscripten::constant with emscripten::val::array() to type them + // as `any` in TypeScript. At runtime, the EM_ASM block below overrides + // these properties with getters that return native JavaScript arrays + // (string[] or string[][]) via the get_ functions above, which is more + // performant and idiomatic than vector wrappers. + emscripten::constant("mjDISABLESTRING", emscripten::val::array()); + emscripten::constant("mjENABLESTRING", emscripten::val::array()); + emscripten::constant("mjFRAMESTRING", emscripten::val::array()); + emscripten::constant("mjLABELSTRING", emscripten::val::array()); + emscripten::constant("mjRNDSTRING", emscripten::val::array()); + emscripten::constant("mjTIMERSTRING", emscripten::val::array()); + emscripten::constant("mjVISSTRING", emscripten::val::array()); + EM_ASM({ + if (typeof Module !== "undefined") { + "mjDISABLESTRING mjENABLESTRING mjFRAMESTRING mjLABELSTRING mjRNDSTRING mjTIMERSTRING mjVISSTRING".split(" ").forEach(function(name) { + Object.defineProperty(Module, name, { + get: function() { return Module["get_" + name](); }, + set: function(v) { }, + enumerable: true, + configurable: true + }); + }); + } + }); } } // namespace mujoco::wasm diff --git a/wasm/demo_app/app.ts b/wasm/demo_app/app.ts index 9e5a1a9f..35712c73 100644 --- a/wasm/demo_app/app.ts +++ b/wasm/demo_app/app.ts @@ -205,10 +205,7 @@ class App { } loadModel(xmlContent: string) { - // Write xml as a file so that mujoco can find it - (mujoco as any).FS.writeFile('/working/model.xml', xmlContent); - - this.mjModel = mujoco.MjModel.mj_loadXML('/working/model.xml'); + this.mjModel = mujoco.MjModel.from_xml_string(xmlContent); if (!app.mjModel) { throw new Error('Failed to load model'); } @@ -437,8 +434,6 @@ function setupWindowEvents() { // Tip: put "window.dispatchEvent(new Event('unload'))" in the console to test window.addEventListener('unload', () => { app.dispose(); - - (mujoco as any).FS.unmount('/working'); }); window.addEventListener('keydown', (event) => { @@ -464,10 +459,6 @@ async function main() { try { mujoco = await loadMujoco(); - // Set up emscripten virtual file system - (mujoco as any).FS.mkdir('/working'); - (mujoco as any).FS.mount((mujoco as any).MEMFS, {root: '.'}, '/working'); - app = new App(); setupWindowEvents(); diff --git a/wasm/package.npm.json b/wasm/package.npm.json index 4554b2cc..6301897d 100644 --- a/wasm/package.npm.json +++ b/wasm/package.npm.json @@ -1,10 +1,10 @@ { - "name": "mujoco", + "name": "@mujoco/mujoco", "version": "0.0.0", "description": "MuJoCo WASM bindings", "repository": { "type": "git", - "url": "https://github.com/google-deepmind/mujoco.git", + "url": "git+https://github.com/google-deepmind/mujoco.git", "directory": "wasm" }, "homepage": "https://github.com/google-deepmind/mujoco/tree/main/wasm", diff --git a/wasm/tests/bindings_test.ts b/wasm/tests/bindings_test.ts index 4cff1ec9..08d9864a 100644 --- a/wasm/tests/bindings_test.ts +++ b/wasm/tests/bindings_test.ts @@ -683,24 +683,24 @@ describe('MuJoCo WASM Bindings', () => { it('should check constants values', () => { expect(mujoco.mjNEQDATA).toBe(11); - expect(mujoco.get_mjDISABLESTRING()).toEqual([ + expect(mujoco.mjDISABLESTRING).toEqual([ 'Constraint', 'Equality', 'Frictionloss', 'Limit', 'Contact', 'Spring', 'Damper', 'Gravity', 'Clampctrl', 'Warmstart', 'Filterparent', 'Actuation', 'Refsafe', 'Sensor', 'Midphase', 'Eulerdamp', 'AutoReset', 'NativeCCD', 'Island' ]); - expect(mujoco.get_mjRNDSTRING()).toEqual([ + expect(mujoco.mjRNDSTRING).toEqual([ ['Shadow', '1', 'S'], ['Wireframe', '0', 'W'], ['Reflection', '1', 'R'], ['Additive', '0', 'L'], ['Skybox', '1', 'K'], ['Fog', '0', 'G'], ['Haze', '1', '/'], ['Depth', '0', ''], ['Segment', '0', ','], ['Id Color', '0', ''], ['Cull Face', '1', ''] ]); - expect(mujoco.get_mjFRAMESTRING().length) - .toEqual(mujoco.mjtFrame.mjNFRAME.value); - expect(mujoco.get_mjVISSTRING().length) + expect(mujoco.mjFRAMESTRING.length).toEqual(mujoco.mjtFrame.mjNFRAME.value); + expect(mujoco.mjVISSTRING.length) .toEqual(mujoco.mjtVisFlag.mjNVISFLAG.value); - expect(mujoco.get_mjVISSTRING()[mujoco.mjtVisFlag.mjVIS_INERTIA.value]) - .toEqual(['Inertia', '0', 'I']); + expect(mujoco.mjVISSTRING[mujoco.mjtVisFlag.mjVIS_INERTIA.value]).toEqual([ + 'Inertia', '0', 'I' + ]); }); it('should create a spec from XML', () => { @@ -2603,4 +2603,56 @@ describe('MuJoCo WASM Bindings', () => { } }); + it('should load a model from an XML string', () => { + let model: MjModel|null = null; + try { + model = mujoco.from_xml_string(TEST_XML); + assertExists(model); + expect(model.nbody).toBe(5); + expect(model.ngeom).toBe(3); + } finally { + model?.delete(); + } + }); + + it('should load a model from an XML string with VFS', () => { + const xml = ` + + + + + + + + `; + + const cube1 = ` + v -1 -1 1 + v 1 -1 1 + v -1 1 1 + v 1 1 1 + v -1 1 -1 + v 1 1 -1 + v -1 -1 -1 + v 1 -1 -1`; + + let model: MjModel|null = null; + let vfs: MjVFS|null = null; + try { + vfs = new mujoco.MjVFS(); + vfs.addBuffer('cube.obj', new TextEncoder().encode(cube1)); + assertExists(vfs); + + model = mujoco.from_xml_string(xml, vfs); + assertExists(model); + expect(model.nmesh).toBe(1); + + const meshId = + mujoco.mj_name2id(model, mujoco.mjtObj.mjOBJ_MESH.value, 'cube'); + expect(meshId).toBeGreaterThanOrEqual(0); + } finally { + model?.delete(); + vfs?.delete(); + } + }); }); diff --git a/wasm/tests/sandbox/main.ts b/wasm/tests/sandbox/main.ts index 60fc5e95..ed312512 100644 --- a/wasm/tests/sandbox/main.ts +++ b/wasm/tests/sandbox/main.ts @@ -20,9 +20,6 @@ declare function loadMujoco(): Promise; async function main() { const mujoco: MainModule = await loadMujoco(); - (mujoco as any).FS.mkdir('/working'); - (mujoco as any).FS.mount((mujoco as any).MEMFS, {root: '.'}, '/working'); - const xmlContent = ` `; - (mujoco as any).FS.writeFile('/working/hello.xml', xmlContent); let model: MjModel|undefined; let data: MjData|undefined; try { console.log('Hello world!: Loading model'); - model = mujoco.MjModel.mj_loadXML('/working/hello.xml'); + model = mujoco.MjModel.from_xml_string(xmlContent); if (!model) { throw new Error('Failed to load model'); } @@ -57,7 +53,6 @@ async function main() { } finally { model?.delete(); data?.delete(); - (mujoco as any).FS.unmount('/working'); } }