Merge branch 'google-deepmind:main' into unity/passive-resubmit

This commit is contained in:
Balint Hodossy
2026-03-12 15:52:46 +00:00
committed by GitHub
51 changed files with 957 additions and 531 deletions
+1 -1
View File
@@ -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
+16 -1
View File
@@ -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
+1 -1
View File
@@ -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"
)
+4 -4
View File
@@ -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"
+4 -4
View File
@@ -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"
+1 -1
View File
@@ -388,7 +388,7 @@ Defined in `mujoco.h <https://github.com/google-deepmind/mujoco/blob/main/includ
- value
- description
* - ``mjVERSION_HEADER``
- 3006000
- 3007000
- The version of the MuJoCo headers. This is an integer calculated from the version string "S.M.P"
using the formula ``(S * 1e6) + (M * 1e3) + P``. For example, version 4.2.1 is represented as 4002001.
The API function :ref:`mj_version` returns a number with the same meaning
+2 -2
View File
@@ -3293,8 +3293,8 @@ void mj_objectVelocity(const mjModel* m, const mjData* d,
int objtype, int objid, mjtNum res[6], int flg_local);
void mj_objectAcceleration(const mjModel* m, const mjData* d,
int objtype, int objid, mjtNum res[6], int flg_local);
mjtNum mj_geomDistance(const mjModel* m, const mjData* d, int geom1, int geom2,
mjtNum distmax, mjtNum fromto[6]);
mjtNum mj_geomDistance(const mjModel* m, mjData* d, int geom1, int geom2, mjtNum distmax,
mjtNum fromto[6]);
void mj_contactForce(const mjModel* m, const mjData* d, int id, mjtNum result[6]);
void mj_differentiatePos(const mjModel* m, mjtNum* qvel, mjtNum dt,
const mjtNum* qpos1, const mjtNum* qpos2);
+3 -1
View File
@@ -216,6 +216,8 @@ Benchmark an environment with _`testspeed`
mjwarp-testspeed benchmark/humanoid/humanoid.xml
.. _mjwViewer:
Interactive environment simulation with MJWarp
.. code-block:: shell
@@ -489,7 +491,7 @@ Key features:
`Warp's BVHs <https://nvidia.github.io/warp/api_reference/_generated/warp.Bvh.html#warp.Bvh>`__.
Basic Usage
----------
-----------
Rendering or raycasting requires a :class:`mjw.RenderContext <mujoco_warp.RenderContext>` which contains BVH structures,
rendering specific fields, and output buffers.
+1 -1
View File
@@ -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 <MJW_Cli>` and increasing the values accordingly as overflows
should be tuned by loading them in the :ref:`viewer <mjwViewer>` and increasing the values accordingly as overflows
occur. Scale ``naconmax`` by the number of environments you'll eventually need in a ``jax.vmap``!
Contacts
+2 -2
View File
@@ -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
_______
+3 -3
View File
@@ -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 <stdlib.h>
@@ -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]);
+4 -4
View File
@@ -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
+2 -2
View File
@@ -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}")
+1 -1
View File
@@ -663,7 +663,7 @@ PYBIND11_MODULE(_functions, pymodule) {
Def<traits::mj_contactForce>(pymodule);
Def<traits::mj_geomDistance>(
pymodule,
[](const raw::MjModel* m, const raw::MjData* d,
[](const raw::MjModel* m, raw::MjData* d,
int geom1, int geom2, mjtNum distmax,
std::optional<Eigen::Ref<EigenArrayXX>> fromto) {
if (fromto.has_value() && fromto->size() != 6) {
+1 -1
View File
@@ -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(
+4 -4
View File
@@ -7,13 +7,13 @@
<key>CFBundleIdentifier</key>
<string>org.mujoco.mjpython</string>
<key>CFBundleVersion</key>
<string>3.6.0</string>
<string>3.7.0</string>
<key>CFBundleGetInfoString</key>
<string>3.6.0</string>
<string>3.7.0</string>
<key>CFBundleLongVersionString</key>
<string>3.6.0</string>
<string>3.7.0</string>
<key>CFBundleShortVersionString</key>
<string>3.6.0</string>
<string>3.7.0</string>
<key>CFBundleExecutable</key>
<string>mjpython</string>
<key>CFBundleIconFile</key>
+3 -3
View File
@@ -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
+1 -1
View File
@@ -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"
)
+1 -1
View File
@@ -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"
)
+4 -4
View File
@@ -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;
+2 -2
View File
@@ -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,
@@ -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
@@ -89,6 +89,8 @@ class FilamentContext {
std::unique_ptr<ObjectManager> object_manager_;
std::unique_ptr<SceneView> scene_view_;
std::unique_ptr<GuiView> gui_view_;
int window_width_ = 0;
int window_height_ = 0;
};
} // namespace mujoco
+2 -2
View File
@@ -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;
+342 -166
View File
@@ -26,6 +26,7 @@
#include <implot.h>
#include <mujoco/mujoco.h>
#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<int>(*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<mjtNum>& 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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<float>(i);
const float y = *(static_cast<int*>(user_data));
return ImPlotPoint{x, y};
}, &nefc, npoints);
ImPlot::PlotLineG(
"total",
+[](int i, void* user_data) {
const float x = static_cast<float>(i);
const float y = *(static_cast<int*>(user_data));
return ImPlotPoint{x, y};
},
&nefc, npoints);
ImPlot::PlotLineG("active", +[](int i, void* user_data) {
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(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<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = stats[i].nupdate;
return ImPlotPoint{x, y};
},
stats, npoints);
}
ImPlot::PopStyleVar();
+13
View File
@@ -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);
+17 -16
View File
@@ -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,
+4 -3
View File
@@ -61,6 +61,10 @@ std::unique_ptr<ModelHolder> 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<const std::byte> buffer,
std::string_view content_type,
std::string_view filename) {
mj_defaultVFS(&vfs_);
if (content_type == "text/xml") {
const char* ptr = reinterpret_cast<const char*>(buffer.data());
spec_ = mj_parseXMLString(ptr, nullptr, error_, sizeof(error_));
+1 -1
View File
@@ -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<const std::byte> buffer,
+13 -116
View File
@@ -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<const char*, 31> 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<const char*> 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<int>(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<const char*> 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<const char*> 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<int>(theme))},
-1
View File
@@ -223,7 +223,6 @@ class App {
void SpecEditorGui();
float GetExpectedLabelWidth();
std::vector<const char*> GetCameraNames();
mjSpec* spec() { return model_holder_->spec(); }
mjModel* model() { return model_holder_->model(); }
+1 -1
View File
@@ -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();
+122 -115
View File
@@ -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<float>& 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<float> 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<double> 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<double>::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<double> 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<char*>(qhopt.c_str()));
qh_init_B(qh, vert_.data(), nvert(), 3, qh_False);
qh_init_B(qh, const_cast<double*>(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<std::pair<double, double>, 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<double, double> key;
if (!MeshPolygonKey(key, v1, v2, v3, kAngleTol)) {
+3
View File
@@ -587,6 +587,7 @@ void mjCOctree::CopyCoeff(mjtNum* coeff) const {
void mjCOctree::SetFace(const std::vector<double>& vert, const std::vector<int>& face) {
face_.reserve(face.size()/3);
for (int i = 0; i < face.size(); i += 3) {
std::array<double, 3> v0 = {vert[3*face[i+0]], vert[3*face[i+0]+1], vert[3*face[i+0]+2]};
std::array<double, 3> 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<Triangle>& 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],
+19 -17
View File
@@ -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<double> vert_; // vertex data
std::vector<float> vert_; // vertex data
std::vector<float> normal_; // normal data
std::vector<float> texcoord_; // texcoord data
std::vector<int> 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<double>& Vert() const { return vert_; }
double Vert(int i) const { return vert_[i]; }
const std::vector<float>& Vert() const { return vert_; }
float Vert(int i) const { return vert_[i]; }
const std::vector<float>& UserVert() const { return spec_vert_; }
const std::vector<float>& UserNormal() const { return spec_normal_; }
const std::vector<float>& 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<std::vector<int>> 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;
};
+3 -3
View File
@@ -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;
+9 -5
View File
@@ -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<void(mjVFS*)> destructor) {
void VFS::SetToSelfDestruct(std::function<void()> destructor) {
destructor_ = std::move(destructor);
}
+3 -3
View File
@@ -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<void(mjVFS*)> destructor);
void SetToSelfDestruct(std::function<void()> 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<mjResource*, ResourcePtr> open_resources_;
std::unordered_map<std::string, ResourcePtr> mounts_;
mjResource default_mount_;
mjpResourceProvider default_provider_;
std::function<void(mjVFS*)> destructor_;
std::function<void()> destructor_;
};
} // namespace mujoco::user
+65
View File
@@ -1086,6 +1086,71 @@ TEST_F(MjCMeshTest, MeshScale) {
mj_deleteModel(model);
}
TEST_F(MjCMeshTest, NegativeScaleUserMeshCompiles) {
static constexpr char xml[] = R"(
<mujoco>
<asset>
<mesh name="example_mesh" scale="-1 1 1" inertia="exact"
vertex="0 0 0 1 0 0 0 1 0 0 0 1"
face="0 2 1 0 3 2 1 3 0 1 2 3" />
</asset>
<worldbody>
<geom type="mesh" mesh="example_mesh"/>
</worldbody>
</mujoco>
)";
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"(
<mujoco>
<asset>
<mesh name="pos_mesh"
vertex="0 0 0 1 0 0 0 1 0 0 0 1"
face="0 2 1 0 3 2 1 3 0 1 2 3" />
</asset>
<worldbody>
<geom type="mesh" mesh="pos_mesh"/>
</worldbody>
</mujoco>
)";
static constexpr char neg_xml[] = R"(
<mujoco>
<asset>
<mesh name="neg_mesh" scale="-1 1 1"
vertex="0 0 0 1 0 0 0 1 0 0 0 1"
face="0 2 1 0 3 2 1 3 0 1 2 3" />
</asset>
<worldbody>
<geom type="mesh" mesh="neg_mesh"/>
</worldbody>
</mujoco>
)";
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"(
<mujoco>
+24
View File
@@ -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 = "<mujoco/>";
mj_addBufferVFS(&vfs, "model", static_cast<const void*>(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
@@ -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();
}
+1 -1
View File
@@ -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------------------------------------
+1 -1
View File
@@ -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": {
+14 -6
View File
@@ -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)
+70 -2
View File
@@ -17,6 +17,7 @@
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/em_asm.h>
#include <emscripten/val.h>
#include <algorithm>
@@ -8361,6 +8362,39 @@ std::unique_ptr<MjModel> mj_loadModel_wrapper(std::string filename, const MjVFS&
return std::unique_ptr<MjModel>(new MjModel(model));
}
std::unique_ptr<MjModel> 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<MjModel>(new MjModel(model));
}
std::unique_ptr<MjModel> 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<MjModel>(new MjModel(model));
}
std::unique_ptr<MjSpec> 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>("MjModel")
// mj_loadXML is deprecated and will be removed in a future release
.class_function("mj_loadXML", emscripten::select_overload<std::unique_ptr<MjModel>(std::string)>(&mj_loadXML_wrapper_1))
.class_function("mj_loadXML", emscripten::select_overload<std::unique_ptr<MjModel>(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<std::unique_ptr<MjModel>(const std::string&)>(&from_xml_string_wrapper_1))
.class_function("from_xml_string", emscripten::select_overload<std::unique_ptr<MjModel>(const std::string&, const MjVFS&)>(&from_xml_string_wrapper_2))
.class_function("from_xml_path", emscripten::select_overload<std::unique_ptr<MjModel>(std::string)>(&mj_loadXML_wrapper_1))
.class_function("from_xml_path", emscripten::select_overload<std::unique_ptr<MjModel>(std::string, const MjVFS&)>(&mj_loadXML_wrapper_2))
.constructor<const MjModel &>()
// 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<MjVFS> caused memory errors due to missing copy/move constructors.
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&)>(&mj_compile_wrapper_1));
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&, const MjVFS&)>(&mj_compile_wrapper_2));
function("from_xml_string", emscripten::select_overload<std::unique_ptr<MjModel>(const std::string&)>(&from_xml_string_wrapper_1));
function("from_xml_string", emscripten::select_overload<std::unique_ptr<MjModel>(const std::string&, const MjVFS&)>(&from_xml_string_wrapper_2));
emscripten::class_<WasmBuffer<float>>("FloatBuffer")
.constructor<int>()
@@ -13409,6 +13452,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.function("GetView", &WasmBuffer<uint8_t>::GetView);
emscripten::register_vector<std::string>("mjStringVec");
emscripten::register_vector<std::vector<std::string>>("mjStringVecVec");
emscripten::register_vector<int>("mjIntVec");
emscripten::register_vector<mjIntVec>("mjIntVecVec");
emscripten::register_vector<float>("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
+27
View File
@@ -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::unique_ptr<MjModel>(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<std::unique_ptr<MjModel>(const"
" std::string&)>(&from_xml_string_wrapper_1))"
)
builder.line(
'.class_function("from_xml_string",'
" emscripten::select_overload<std::unique_ptr<MjModel>(const"
" std::string&, const MjVFS&)>(&from_xml_string_wrapper_2))"
)
builder.line(
'.class_function("from_xml_path",'
" emscripten::select_overload<std::unique_ptr<MjModel>(std::string)>(&mj_loadXML_wrapper_1))"
)
builder.line(
'.class_function("from_xml_path",'
" emscripten::select_overload<std::unique_ptr<MjModel>(std::string,"
" const MjVFS&)>(&mj_loadXML_wrapper_2))"
)
builder.line(".constructor<const MjModel &>()")
builder.line("""
// Binds the functions on MjModel that return accessors.
+62 -1
View File
@@ -17,6 +17,7 @@
#include <emscripten.h>
#include <emscripten/bind.h>
#include <emscripten/em_asm.h>
#include <emscripten/val.h>
#include <algorithm>
@@ -746,6 +747,39 @@ std::unique_ptr<MjModel> mj_loadModel_wrapper(std::string filename, const MjVFS&
return std::unique_ptr<MjModel>(new MjModel(model));
}
std::unique_ptr<MjModel> 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<MjModel>(new MjModel(model));
}
std::unique_ptr<MjModel> 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<MjModel>(new MjModel(model));
}
std::unique_ptr<MjSpec> 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<MjVFS> caused memory errors due to missing copy/move constructors.
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&)>(&mj_compile_wrapper_1));
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&, const MjVFS&)>(&mj_compile_wrapper_2));
function("from_xml_string", emscripten::select_overload<std::unique_ptr<MjModel>(const std::string&)>(&from_xml_string_wrapper_1));
function("from_xml_string", emscripten::select_overload<std::unique_ptr<MjModel>(const std::string&, const MjVFS&)>(&from_xml_string_wrapper_2));
emscripten::class_<WasmBuffer<float>>("FloatBuffer")
.constructor<int>()
@@ -875,6 +911,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.function("GetView", &WasmBuffer<uint8_t>::GetView);
emscripten::register_vector<std::string>("mjStringVec");
emscripten::register_vector<std::vector<std::string>>("mjStringVecVec");
emscripten::register_vector<int>("mjIntVec");
emscripten::register_vector<mjIntVec>("mjIntVecVec");
emscripten::register_vector<float>("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
+1 -10
View File
@@ -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();
+2 -2
View File
@@ -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",
+59 -7
View File
@@ -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 = `
<mujoco>
<asset>
<mesh file="cube.obj"/>
</asset>
<worldbody>
<geom type="mesh" mesh="cube"/>
</worldbody>
</mujoco>`;
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();
}
});
});
+1 -6
View File
@@ -20,9 +20,6 @@ declare function loadMujoco(): Promise<MainModule>;
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 model="Box falling">
<option viscosity="1"/>
@@ -37,13 +34,12 @@ async function main() {
</worldbody>
</mujoco>`;
(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');
}
}