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/package.npm.json wasm/dist/package.json
cp wasm/README.md wasm/dist/README.md 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 npm --prefix wasm/dist version "${VERSION}" --no-git-tag-version
echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ~/.npmrc echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > ~/.npmrc
+16 -1
View File
@@ -5,10 +5,25 @@ on:
tags: tags:
- '[0-9]*.[0-9]*.[0-9]*' - '[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: jobs:
publish: publish:
name: Publish MuJoCo WASM package name: Publish MuJoCo WASM package
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
env:
VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_version || github.ref_name }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -32,5 +47,5 @@ jobs:
- name: Package WASM bindings - name: Package WASM bindings
env: env:
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
GITHUB_REF: ${{ github.ref }} VERSION: ${{ env.VERSION }}
run: bash ./.github/workflows/build_steps.sh package_wasm run: bash ./.github/workflows/build_steps.sh package_wasm
+1 -1
View File
@@ -29,7 +29,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON)
project( project(
mujoco mujoco
VERSION 3.6.0 VERSION 3.7.0
DESCRIPTION "MuJoCo Physics Simulator" DESCRIPTION "MuJoCo Physics Simulator"
HOMEPAGE_URL "https://mujoco.org" HOMEPAGE_URL "https://mujoco.org"
) )
+4 -4
View File
@@ -1,6 +1,6 @@
1 VERSIONINFO 1 VERSIONINFO
FILEVERSION 3,6,0,0 FILEVERSION 3,7,0,0
PRODUCTVERSION 3,6,0,0 PRODUCTVERSION 3,7,0,0
FILEOS 0x4 FILEOS 0x4
FILETYPE 0x1 FILETYPE 0x1
{ {
@@ -9,9 +9,9 @@ FILETYPE 0x1
BLOCK "040904b0" BLOCK "040904b0"
{ {
VALUE "ProductName", "MuJoCo" VALUE "ProductName", "MuJoCo"
VALUE "ProductVersion", "3.6.0" VALUE "ProductVersion", "3.7.0"
VALUE "FileDescription", "MuJoCo" VALUE "FileDescription", "MuJoCo"
VALUE "FileVersion", "3.6.0" VALUE "FileVersion", "3.7.0"
VALUE "InternalName", "mujoco.dll" VALUE "InternalName", "mujoco.dll"
VALUE "OriginalFilename", "mujoco.dll" VALUE "OriginalFilename", "mujoco.dll"
VALUE "CompanyName", "Google DeepMind" VALUE "CompanyName", "Google DeepMind"
+4 -4
View File
@@ -1,8 +1,8 @@
MUJOCO ICON "mujoco.ico" MUJOCO ICON "mujoco.ico"
1 VERSIONINFO 1 VERSIONINFO
FILEVERSION 3,6,0,0 FILEVERSION 3,7,0,0
PRODUCTVERSION 3,6,0,0 PRODUCTVERSION 3,7,0,0
FILEOS 0x4 FILEOS 0x4
FILETYPE 0x1 FILETYPE 0x1
{ {
@@ -11,9 +11,9 @@ FILETYPE 0x1
BLOCK "040904b0" BLOCK "040904b0"
{ {
VALUE "ProductName", "MuJoCo" VALUE "ProductName", "MuJoCo"
VALUE "ProductVersion", "3.6.0" VALUE "ProductVersion", "3.7.0"
VALUE "FileDescription", "MuJoCo" VALUE "FileDescription", "MuJoCo"
VALUE "FileVersion", "3.6.0" VALUE "FileVersion", "3.7.0"
VALUE "InternalName", "simulate.exe" VALUE "InternalName", "simulate.exe"
VALUE "OriginalFilename", "simulate.exe" VALUE "OriginalFilename", "simulate.exe"
VALUE "CompanyName", "Google DeepMind" 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 - value
- description - description
* - ``mjVERSION_HEADER`` * - ``mjVERSION_HEADER``
- 3006000 - 3007000
- The version of the MuJoCo headers. This is an integer calculated from the version string "S.M.P" - 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. 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 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); int objtype, int objid, mjtNum res[6], int flg_local);
void mj_objectAcceleration(const mjModel* m, const mjData* d, void mj_objectAcceleration(const mjModel* m, const mjData* d,
int objtype, int objid, mjtNum res[6], int flg_local); int objtype, int objid, mjtNum res[6], int flg_local);
mjtNum mj_geomDistance(const mjModel* m, const mjData* d, int geom1, int geom2, mjtNum mj_geomDistance(const mjModel* m, mjData* d, int geom1, int geom2, mjtNum distmax,
mjtNum distmax, mjtNum fromto[6]); mjtNum fromto[6]);
void mj_contactForce(const mjModel* m, const mjData* d, int id, mjtNum result[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, void mj_differentiatePos(const mjModel* m, mjtNum* qvel, mjtNum dt,
const mjtNum* qpos1, const mjtNum* qpos2); 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 mjwarp-testspeed benchmark/humanoid/humanoid.xml
.. _mjwViewer:
Interactive environment simulation with MJWarp Interactive environment simulation with MJWarp
.. code-block:: shell .. 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>`__. `Warp's BVHs <https://nvidia.github.io/warp/api_reference/_generated/warp.Bvh.html#warp.Bvh>`__.
Basic Usage Basic Usage
---------- -----------
Rendering or raycasting requires a :class:`mjw.RenderContext <mujoco_warp.RenderContext>` which contains BVH structures, Rendering or raycasting requires a :class:`mjw.RenderContext <mujoco_warp.RenderContext>` which contains BVH structures,
rendering specific fields, and output buffers. 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. * ``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 * ``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``! occur. Scale ``naconmax`` by the number of environments you'll eventually need in a ``jax.vmap``!
Contacts 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 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 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``. found by browsing the contents of ``MuJoCo.app``) and rename it as ``mujoco.dylib``.
Linux Linux
_____ _____
Expand the ``tar.gz`` archive to ``~/.mujoco``. Then copy the dynamic library from 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 Windows
_______ _______
+3 -3
View File
@@ -16,7 +16,7 @@
#define MUJOCO_MUJOCO_H_ #define MUJOCO_MUJOCO_H_
// header version; should match the library version as returned by mj_version() // 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 // needed to define size_t, fabs and log10
#include <stdlib.h> #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. // Return smallest signed distance between two geoms and optionally segment from geom1 to geom2.
// Nullable: fromto // Nullable: fromto
MJAPI mjtNum mj_geomDistance(const mjModel* m, const mjData* d, int geom1, int geom2, MJAPI mjtNum mj_geomDistance(const mjModel* m, mjData* d, int geom1, int geom2, mjtNum distmax,
mjtNum distmax, mjtNum fromto[6]); mjtNum fromto[6]);
// Extract 6D force:torque given contact id, in the contact frame. // 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]); 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] [project]
name="mujoco-mjx" name="mujoco-mjx"
version = "3.6.0" version = "3.7.0"
authors = [ authors = [
{name = "Google DeepMind", email = "mujoco@deepmind.com"}, {name = "Google DeepMind", email = "mujoco@deepmind.com"},
] ]
@@ -29,7 +29,7 @@ dependencies = [
"etils[epath]", "etils[epath]",
"jax", "jax",
"jaxlib", "jaxlib",
"mujoco>=3.6.0.dev0", "mujoco>=3.7.0.dev0",
"scipy", "scipy",
"trimesh", "trimesh",
] ]
@@ -45,9 +45,9 @@ mjx-viewer = "mujoco.mjx.viewer:main"
[project.urls] [project.urls]
Homepage = "https://github.com/google-deepmind/mujoco/tree/main/mjx" 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" 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] [tool.isort]
force_single_line = true force_single_line = true
+2 -2
View File
@@ -86,7 +86,7 @@ if(NOT TARGET mujoco)
if(MUJOCO_FRAMEWORK) if(MUJOCO_FRAMEWORK)
message("MuJoCo framework is at ${MUJOCO_FRAMEWORK}/mujoco.framework") message("MuJoCo framework is at ${MUJOCO_FRAMEWORK}/mujoco.framework")
set(MUJOCO_LIBRARY 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}) target_compile_options(mujoco INTERFACE -F${MUJOCO_FRAMEWORK})
endif() endif()
@@ -94,7 +94,7 @@ if(NOT TARGET mujoco)
if(NOT MUJOCO_FRAMEWORK) if(NOT MUJOCO_FRAMEWORK)
find_library( 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) find_path(MUJOCO_INCLUDE mujoco/mujoco.h HINTS ${MUJOCO_INCLUDE_DIR} REQUIRED)
message("MuJoCo is at ${MUJOCO_LIBRARY}") 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_contactForce>(pymodule);
Def<traits::mj_geomDistance>( Def<traits::mj_geomDistance>(
pymodule, pymodule,
[](const raw::MjModel* m, const raw::MjData* d, [](const raw::MjModel* m, raw::MjData* d,
int geom1, int geom2, mjtNum distmax, int geom1, int geom2, mjtNum distmax,
std::optional<Eigen::Ref<EigenArrayXX>> fromto) { std::optional<Eigen::Ref<EigenArrayXX>> fromto) {
if (fromto.has_value() && fromto->size() != 6) { if (fromto.has_value() && fromto->size() != 6) {
+1 -1
View File
@@ -3626,7 +3626,7 @@ FUNCTIONS: Mapping[str, FunctionDecl] = dict([
FunctionParameterDecl( FunctionParameterDecl(
name='d', name='d',
type=PointerType( type=PointerType(
inner_type=ValueType(name='mjData', is_const=True), inner_type=ValueType(name='mjData'),
), ),
), ),
FunctionParameterDecl( FunctionParameterDecl(
+4 -4
View File
@@ -7,13 +7,13 @@
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>org.mujoco.mjpython</string> <string>org.mujoco.mjpython</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>3.6.0</string> <string>3.7.0</string>
<key>CFBundleGetInfoString</key> <key>CFBundleGetInfoString</key>
<string>3.6.0</string> <string>3.7.0</string>
<key>CFBundleLongVersionString</key> <key>CFBundleLongVersionString</key>
<string>3.6.0</string> <string>3.7.0</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>3.6.0</string> <string>3.7.0</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>mjpython</string> <string>mjpython</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
+3 -3
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "mujoco" name = "mujoco"
version = "3.6.0" version = "3.7.0"
authors = [ authors = [
{name = "Google DeepMind", email = "mujoco@deepmind.com"}, {name = "Google DeepMind", email = "mujoco@deepmind.com"},
] ]
@@ -34,9 +34,9 @@ dynamic = ["readme", "scripts"]
[project.urls] [project.urls]
Homepage = "https://github.com/google-deepmind/mujoco" 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" 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] [tool.setuptools]
include-package-data = false include-package-data = false
+1 -1
View File
@@ -24,7 +24,7 @@ set(MSVC_INCREMENTAL_DEFAULT ON)
project( project(
mujoco_samples mujoco_samples
VERSION 3.6.0 VERSION 3.7.0
DESCRIPTION "MuJoCo samples binaries" DESCRIPTION "MuJoCo samples binaries"
HOMEPAGE_URL "https://mujoco.org" HOMEPAGE_URL "https://mujoco.org"
) )
+1 -1
View File
@@ -29,7 +29,7 @@ set(MUJOCO_DEP_VERSION_lodepng
project( project(
mujoco_simulate mujoco_simulate
VERSION 3.6.0 VERSION 3.7.0
DESCRIPTION "MuJoCo simulate binaries" DESCRIPTION "MuJoCo simulate binaries"
HOMEPAGE_URL "https://mujoco.org" HOMEPAGE_URL "https://mujoco.org"
) )
+4 -4
View File
@@ -43,8 +43,8 @@
//-------------------------- Constants ------------------------------------------------------------- //-------------------------- Constants -------------------------------------------------------------
#define mjVERSION 3006000 #define mjVERSION 3007000
#define mjVERSIONSTRING "3.6.0" #define mjVERSIONSTRING "3.7.0"
// names of disable flags // names of disable flags
const char* mjDISABLESTRING[mjNDISABLE] = { const char* mjDISABLESTRING[mjNDISABLE] = {
@@ -524,7 +524,7 @@ void mj_xfrcAccumulate(const mjModel* m, mjData* d, mjtNum* qfrc) {
//-------------------------- miscellaneous --------------------------------------------------------- //-------------------------- miscellaneous ---------------------------------------------------------
// returns the smallest distance between two geoms (using nativeccd) // 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]) { mjtNum distmax, mjtNum fromto[6]) {
mjCCDConfig config; mjCCDConfig config;
mjCCDStatus status; 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 // 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]) { mjtNum fromto[6]) {
mjContact con[mjMAXCONPAIR]; mjContact con[mjMAXCONPAIR];
mjtNum dist = distmax; mjtNum dist = distmax;
+2 -2
View File
@@ -86,8 +86,8 @@ void mj_xfrcAccumulate(const mjModel* m, mjData* d, mjtNum* qfrc);
//-------------------------- miscellaneous --------------------------------------------------------- //-------------------------- miscellaneous ---------------------------------------------------------
// returns the smallest distance between two geoms // returns the smallest distance between two geoms
MJAPI mjtNum mj_geomDistance(const mjModel* m, const mjData* d, int geom1, int geom2, MJAPI mjtNum mj_geomDistance(const mjModel* m, mjData* d, int geom1, int geom2, mjtNum distmax,
mjtNum distmax, mjtNum fromto[6]); mjtNum fromto[6]);
// compute velocity by finite-differencing two positions // compute velocity by finite-differencing two positions
MJAPI void mj_differentiatePos(const mjModel* m, mjtNum* qvel, mjtNum dt, 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."); 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_->SetViewport(viewport);
scene_view_->UpdateScene(con, scene); scene_view_->UpdateScene(con, scene);
// Update the UX renderable entity after processing the scene in case there // 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<ObjectManager> object_manager_;
std::unique_ptr<SceneView> scene_view_; std::unique_ptr<SceneView> scene_view_;
std::unique_ptr<GuiView> gui_view_; std::unique_ptr<GuiView> gui_view_;
int window_width_ = 0;
int window_height_ = 0;
}; };
} // namespace mujoco } // 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 // 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 // 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_); const std::filesystem::path path(name_);
root_model_ = (path / path.stem()).string() + ".xml"; root_model_ = (path / path.stem()).string() + ".xml";
if (!Contains(root_model_)) { 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_)) { if (!Contains(root_model_)) {
SetError(error, error_sz, "Zip error: no root XML file found."); SetError(error, error_sz, "Zip error: no root XML file found.");
return; return;
+342 -166
View File
@@ -26,6 +26,7 @@
#include <implot.h> #include <implot.h>
#include <mujoco/mujoco.h> #include <mujoco/mujoco.h>
#include "experimental/platform/helpers.h" #include "experimental/platform/helpers.h"
#include "experimental/platform/interaction.h"
#include "experimental/platform/imgui_widgets.h" #include "experimental/platform/imgui_widgets.h"
namespace mujoco::platform { namespace mujoco::platform {
@@ -41,70 +42,70 @@ void SetupTheme(GuiTheme theme) {
ImVec4* c = s.Colors; ImVec4* c = s.Colors;
if (theme == GuiTheme::kDark) { if (theme == GuiTheme::kDark) {
ImGui::StyleColorsDark(&s); ImGui::StyleColorsDark(&s);
c[ImGuiCol_Text] = ImVec4(1.00, 1.00, 1.00, 1.00); 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_TextDisabled] = ImVec4(0.40, 0.40, 0.40, 1.00);
c[ImGuiCol_ChildBg] = ImVec4(0.25, 0.25, 0.25, 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_WindowBg] = ImVec4(0.25, 0.25, 0.25, 1.00);
c[ImGuiCol_PopupBg] = 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_Border] = ImVec4(0.12, 0.12, 0.12, 0.71);
c[ImGuiCol_BorderShadow] = ImVec4(1.00, 1.00, 1.00, 0.06); 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_FrameBg] = ImVec4(0.42, 0.42, 0.42, 0.54);
c[ImGuiCol_FrameBgHovered] = ImVec4(0.42, 0.42, 0.42, 0.40); 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_FrameBgActive] = ImVec4(0.56, 0.56, 0.56, 0.67);
c[ImGuiCol_TitleBg] = ImVec4(0.19, 0.19, 0.19, 1.00); 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_TitleBgActive] = ImVec4(0.22, 0.22, 0.22, 1.00);
c[ImGuiCol_TitleBgCollapsed] = ImVec4(0.17, 0.17, 0.17, 0.90); 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_MenuBarBg] = ImVec4(0.34, 0.34, 0.34, 1.00);
c[ImGuiCol_ScrollbarBg] = ImVec4(0.24, 0.24, 0.24, 0.53); 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_ScrollbarGrab] = ImVec4(0.41, 0.41, 0.41, 1.00);
c[ImGuiCol_ScrollbarGrabHovered] = ImVec4(0.52, 0.52, 0.52, 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_ScrollbarGrabActive] = ImVec4(0.76, 0.76, 0.76, 1.00);
c[ImGuiCol_CheckMark] = ImVec4(0.65, 0.65, 0.65, 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_SliderGrab] = ImVec4(0.52, 0.52, 0.52, 1.00);
c[ImGuiCol_SliderGrabActive] = ImVec4(0.64, 0.64, 0.64, 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_Button] = ImVec4(0.54, 0.54, 0.54, 0.35);
c[ImGuiCol_ButtonHovered] = ImVec4(0.52, 0.52, 0.52, 0.59); 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_ButtonActive] = ImVec4(0.76, 0.76, 0.76, 1.00);
c[ImGuiCol_Header] = ImVec4(0.38, 0.38, 0.38, 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_HeaderHovered] = ImVec4(0.47, 0.47, 0.47, 1.00);
c[ImGuiCol_HeaderActive] = ImVec4(0.76, 0.76, 0.76, 0.77); 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_Separator] = ImVec4(0.00, 0.00, 0.00, 0.18);
c[ImGuiCol_SeparatorHovered] = ImVec4(0.70, 0.67, 0.60, 0.29); 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_SeparatorActive] = ImVec4(0.70, 0.67, 0.60, 0.67);
c[ImGuiCol_ResizeGrip] = ImVec4(0.26, 0.59, 0.98, 0.25); 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_ResizeGripHovered] = ImVec4(0.26, 0.59, 0.98, 0.67);
c[ImGuiCol_ResizeGripActive] = ImVec4(0.26, 0.59, 0.98, 0.95); 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_PlotLines] = ImVec4(0.61, 0.61, 0.61, 1.00);
c[ImGuiCol_PlotLinesHovered] = ImVec4(1.00, 0.43, 0.35, 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_PlotHistogram] = ImVec4(0.90, 0.70, 0.00, 1.00);
c[ImGuiCol_PlotHistogramHovered] = ImVec4(1.00, 0.60, 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_TextSelectedBg] = ImVec4(0.73, 0.73, 0.73, 0.35);
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0.80, 0.80, 0.80, 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_DragDropTarget] = ImVec4(1.00, 1.00, 0.00, 0.90);
c[ImGuiCol_NavHighlight] = ImVec4(0.26, 0.59, 0.98, 1.00); 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_NavWindowingHighlight] = ImVec4(1.00, 1.00, 1.00, 0.70);
c[ImGuiCol_NavWindowingDimBg] = ImVec4(0.80, 0.80, 0.80, 0.20); 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_DockingEmptyBg] = ImVec4(0.38, 0.38, 0.38, 1.00);
c[ImGuiCol_Tab] = ImVec4(0.25, 0.25, 0.25, 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_TabHovered] = ImVec4(0.40, 0.40, 0.40, 1.00);
c[ImGuiCol_TabActive] = ImVec4(0.33, 0.33, 0.33, 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_TabUnfocused] = ImVec4(0.25, 0.25, 0.25, 1.00);
c[ImGuiCol_TabUnfocusedActive] = ImVec4(0.33, 0.33, 0.33, 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_DockingPreview] = ImVec4(0.85, 0.85, 0.85, 0.28);
c[ImGuiCol_WindowBg].w = 1.0f; c[ImGuiCol_WindowBg].w = 1.0f;
} else if (theme == GuiTheme::kLight) { } else if (theme == GuiTheme::kLight) {
ImGui::StyleColorsLight(&s); 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 transparent = ImVec4(0.00, 0.00, 0.00, 0.00);
ImVec4 dark = ImVec4(0.00, 0.00, 0.00, 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 darker = ImVec4(0.00, 0.00, 0.00, 0.50);
ImVec4 background = ImVec4(0.95, 0.95, 0.95, 1.00); ImVec4 background = ImVec4(0.95, 0.95, 0.95, 1.00);
ImVec4 text = ImVec4(0.10, 0.10, 0.10, 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 border = ImVec4(0.60, 0.60, 0.60, 1.00);
ImVec4 grab = ImVec4(0.69, 0.69, 0.69, 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 header = ImVec4(0.86, 0.86, 0.86, 1.00);
ImVec4 active = ImVec4(0.00, 0.47, 0.84, 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 hover = ImVec4(0.00, 0.47, 0.84, 0.20);
c[ImGuiCol_Text] = text; c[ImGuiCol_Text] = text;
c[ImGuiCol_WindowBg] = background; c[ImGuiCol_WindowBg] = background;
@@ -129,46 +130,46 @@ void SetupTheme(GuiTheme theme) {
c[ImGuiCol_ScrollbarGrab] = grab; c[ImGuiCol_ScrollbarGrab] = grab;
c[ImGuiCol_ScrollbarGrabHovered] = dark; c[ImGuiCol_ScrollbarGrabHovered] = dark;
c[ImGuiCol_ScrollbarGrabActive] = darker; c[ImGuiCol_ScrollbarGrabActive] = darker;
} else { } else {
ImGui::StyleColorsDark(&s); ImGui::StyleColorsDark(&s);
ImVec4 black = ImVec4(0.00, 0.00, 0.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 window = ImVec4(0.25, 0.25, 0.25, 1.0);
ImVec4 font_active = ImVec4(1.00, 1.00, 1.00, 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 font_inactive = ImVec4(0.50, 0.50, 0.50, 1.0);
ImVec4 thumb = ImVec4(0.12, 0.12, 0.12, 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 section = ImVec4(0.40, 0.15, 0.15, 1.0);
ImVec4 button = ImVec4(0.60, 0.40, 0.40, 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 check = ImVec4(0.40, 0.40, 0.70, 1.0);
ImVec4 frame = ImVec4(0.40, 0.30, 0.40, 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 slider = ImVec4(0.60, 0.40, 0.60, 1.0);
c[ImGuiCol_WindowBg] = window; c[ImGuiCol_WindowBg] = window;
c[ImGuiCol_ChildBg] = black; c[ImGuiCol_ChildBg] = black;
c[ImGuiCol_PopupBg] = window; c[ImGuiCol_PopupBg] = window;
c[ImGuiCol_Text] = font_active; c[ImGuiCol_Text] = font_active;
c[ImGuiCol_TextDisabled] = font_inactive; c[ImGuiCol_TextDisabled] = font_inactive;
c[ImGuiCol_CheckMark] = font_active; c[ImGuiCol_CheckMark] = font_active;
c[ImGuiCol_Header] = section; c[ImGuiCol_Header] = section;
c[ImGuiCol_HeaderHovered] = section; c[ImGuiCol_HeaderHovered] = section;
c[ImGuiCol_HeaderActive] = section; c[ImGuiCol_HeaderActive] = section;
c[ImGuiCol_TitleBgActive] = window; c[ImGuiCol_TitleBgActive] = window;
c[ImGuiCol_ScrollbarBg] = window; c[ImGuiCol_ScrollbarBg] = window;
c[ImGuiCol_ScrollbarGrab] = thumb; c[ImGuiCol_ScrollbarGrab] = thumb;
c[ImGuiCol_ScrollbarGrabHovered] = thumb; c[ImGuiCol_ScrollbarGrabHovered] = thumb;
c[ImGuiCol_ScrollbarGrabActive] = thumb; c[ImGuiCol_ScrollbarGrabActive] = thumb;
c[ImGuiCol_FrameBg] = frame; c[ImGuiCol_FrameBg] = frame;
c[ImGuiCol_FrameBgHovered] = frame; c[ImGuiCol_FrameBgHovered] = frame;
c[ImGuiCol_FrameBgActive] = frame; c[ImGuiCol_FrameBgActive] = frame;
c[ImGuiCol_SliderGrab] = slider; c[ImGuiCol_SliderGrab] = slider;
c[ImGuiCol_SliderGrabActive] = slider; c[ImGuiCol_SliderGrabActive] = slider;
c[ImGuiCol_Button] = window; c[ImGuiCol_Button] = window;
c[ImGuiCol_ButtonHovered] = button; c[ImGuiCol_ButtonHovered] = button;
c[ImGuiCol_ButtonActive] = button; c[ImGuiCol_ButtonActive] = button;
c[ImGuiCol_Tab] = window; c[ImGuiCol_Tab] = window;
c[ImGuiCol_TabHovered] = check; c[ImGuiCol_TabHovered] = check;
c[ImGuiCol_TabSelected] = check; c[ImGuiCol_TabSelected] = check;
c[ImGuiCol_TabDimmed] = window; c[ImGuiCol_TabDimmed] = window;
c[ImGuiCol_TabDimmedSelected] = check; c[ImGuiCol_TabDimmedSelected] = check;
} }
int hspacing = 4; int hspacing = 4;
@@ -207,14 +208,11 @@ ImVec4 ConfigureDockingLayout() {
const float kToolsBarHeight = 48.f * scale; const float kToolsBarHeight = 48.f * scale;
const float kStatusBarHeight = 32.f * scale; const float kStatusBarHeight = 32.f * scale;
const ImVec2 dockspace_pos{ const ImVec2 dockspace_pos{viewport->WorkPos.x,
viewport->WorkPos.x, viewport->WorkPos.y + kToolsBarHeight};
viewport->WorkPos.y + kToolsBarHeight
};
const ImVec2 dockspace_size{ const ImVec2 dockspace_size{
viewport->WorkSize.x, viewport->WorkSize.x,
viewport->WorkSize.y - kToolsBarHeight - kStatusBarHeight viewport->WorkSize.y - kToolsBarHeight - kStatusBarHeight};
};
ImGuiID root = ImGui::GetID("Root"); ImGuiID root = ImGui::GetID("Root");
const bool first_time = (ImGui::DockBuilderGetNode(root) == nullptr); const bool first_time = (ImGui::DockBuilderGetNode(root) == nullptr);
@@ -228,16 +226,16 @@ ImVec4 ConfigureDockingLayout() {
ImGuiID main = root; ImGuiID main = root;
ImGuiID options = 0; ImGuiID options = 0;
ImGui::DockBuilderSplitNode(main, ImGuiDir_Left, kOptionsRelWidth, ImGui::DockBuilderSplitNode(main, ImGuiDir_Left, kOptionsRelWidth, &options,
&options, &main); &main);
ImGuiID inspector = 0; ImGuiID inspector = 0;
ImGui::DockBuilderSplitNode(main, ImGuiDir_Right, kInspectorRelWidth, ImGui::DockBuilderSplitNode(main, ImGuiDir_Right, kInspectorRelWidth,
&inspector, &main); &inspector, &main);
ImGuiID stats = 0; ImGuiID stats = 0;
ImGui::DockBuilderSplitNode(options, ImGuiDir_Down, kStatsRelHeight, ImGui::DockBuilderSplitNode(options, ImGuiDir_Down, kStatsRelHeight, &stats,
&stats, &options); &options);
ImGuiID properties = 0; ImGuiID properties = 0;
ImGui::DockBuilderSplitNode(inspector, ImGuiDir_Down, kStatsRelHeight, ImGui::DockBuilderSplitNode(inspector, ImGuiDir_Down, kStatsRelHeight,
@@ -265,11 +263,10 @@ ImVec4 ConfigureDockingLayout() {
ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground; ImGuiWindowFlags_NoNavFocus | ImGuiWindowFlags_NoBackground;
const ImGuiWindowFlags kFixedFlags = ImGuiWindowFlags_NoTitleBar | const ImGuiWindowFlags kFixedFlags =
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoDocking;
ImGuiWindowFlags_NoDocking;
// Main workspace area in which we can dock other windows. // 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_WindowBorderSize, 1.0f);
style.Var(ImGuiStyleVar_WindowRounding, 0.0f); style.Var(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::SetNextWindowPos(viewport->WorkPos, ImGuiCond_Always); 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::Begin("ToolBar", nullptr, kFixedFlags);
ImGui::End(); ImGui::End();
} }
@@ -302,8 +300,10 @@ ImVec4 ConfigureDockingLayout() {
platform::ScopedStyle style; platform::ScopedStyle style;
style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f); style.Var(ImGuiStyleVar_WindowBorderSize, 1.0f);
style.Var(ImGuiStyleVar_WindowRounding, 0.0f); style.Var(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::SetNextWindowPos(ImVec2(0, viewport->Size.y - kStatusBarHeight), ImGuiCond_Always); ImGui::SetNextWindowPos(ImVec2(0, viewport->Size.y - kStatusBarHeight),
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kStatusBarHeight), ImGuiCond_Always); ImGuiCond_Always);
ImGui::SetNextWindowSize(ImVec2(viewport->Size.x, kStatusBarHeight),
ImGuiCond_Always);
ImGui::Begin("StatusBar", nullptr, kFixedFlags); ImGui::Begin("StatusBar", nullptr, kFixedFlags);
ImGui::End(); ImGui::End();
} }
@@ -317,6 +317,148 @@ ImVec4 ConfigureDockingLayout() {
return ImVec4(workspace_x, workspace_y, workspace_w, workspace_h); 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) { void SensorGui(const mjModel* model, const mjData* data) {
if (model->nsensor == 0) { if (model->nsensor == 0) {
return; return;
@@ -392,11 +534,12 @@ void StateGui(const mjModel* model, mjData* data, std::vector<mjtNum>& state,
ImGui::Unindent(0.5f * ImGui::GetTreeNodeToLabelSpacing()); ImGui::Unindent(0.5f * ImGui::GetTreeNodeToLabelSpacing());
// State component names and tooltips. // State component names and tooltips.
static constexpr const char* name_and_tooltip[][2] = { static constexpr const char* name_and_tooltip[mjNSTATE][2] = {
{"TIME", "Time"}, {"TIME", "Time"},
{"QPOS", "Position"}, {"QPOS", "Position"},
{"QVEL", "Velocity"}, {"QVEL", "Velocity"},
{"ACT", "Actuator activation"}, {"ACT", "Actuator activation"},
{"HISTORY", "History buffers (control, sensor)"},
{"WARMSTART", "Acceleration used for warmstart"}, {"WARMSTART", "Acceleration used for warmstart"},
{"CTRL", "Control"}, {"CTRL", "Control"},
{"QFRC_APPLIED", "Applied generalized force"}, {"QFRC_APPLIED", "Applied generalized force"},
@@ -935,36 +1078,49 @@ void ConvergenceGui(const mjModel* model, mjData* data) {
ImPlot::SetupLegend(ImPlotLocation_NorthEast); ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish(); 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++) { for (int k = 0; k < nisland; k++) {
mjSolverStat* stats = data->solver + k * mjNSOLVER; mjSolverStat* stats = data->solver + k * mjNSOLVER;
const int npoints = const int npoints =
mjMIN(mjMIN(data->solver_niter[k], mjNSOLVER), mjMAXLINEPNT); mjMIN(mjMIN(data->solver_niter[k], mjNSOLVER), mjMAXLINEPNT);
ImPlot::PlotLineG("improvement", +[](int i, void* user_data) { ImPlot::PlotLineG(
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data); "improvement",
const float x = static_cast<float>(i); +[](int i, void* user_data) {
const float y = mju_log10(mju_max(mjMINVAL, stats[i].improvement)); const mjSolverStat* stats =
return ImPlotPoint{x, y}; static_cast<const mjSolverStat*>(user_data);
}, stats, npoints); 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) { if (model->opt.solver == mjSOL_PGS) {
continue; continue;
} }
ImPlot::PlotLineG("gradient", +[](int i, void* user_data) { ImPlot::PlotLineG(
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data); "gradient",
const float x = static_cast<float>(i); +[](int i, void* user_data) {
const float y = mju_log10(mju_max(mjMINVAL, stats[i].gradient)); const mjSolverStat* stats =
return ImPlotPoint{x, y}; static_cast<const mjSolverStat*>(user_data);
}, stats, npoints); 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) { ImPlot::PlotLineG(
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data); "lineslope",
const float x = static_cast<float>(i); +[](int i, void* user_data) {
const float y = mju_log10(mju_max(mjMINVAL, stats[i].lineslope)); const mjSolverStat* stats =
return ImPlotPoint{x, y}; static_cast<const mjSolverStat*>(user_data);
}, stats, npoints); 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(); ImPlot::PopStyleVar();
@@ -982,56 +1138,76 @@ void CountsGui(const mjModel* model, mjData* data) {
ImPlot::SetupLegend(ImPlotLocation_NorthEast); ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish(); 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++) { for (int k = 0; k < nisland; k++) {
const int npoints = const int npoints =
mjMIN(mjMIN(data->solver_niter[k], mjNSOLVER), mjMAXLINEPNT); 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]; int nefc = nisland == 1 ? data->nefc : data->island_nefc[k];
ImPlot::PlotLineG("total", +[](int i, void* user_data) { ImPlot::PlotLineG(
const float x = static_cast<float>(i); "total",
const float y = *(static_cast<int*>(user_data)); +[](int i, void* user_data) {
return ImPlotPoint{x, y}; const float x = static_cast<float>(i);
}, &nefc, npoints); const float y = *(static_cast<int*>(user_data));
return ImPlotPoint{x, y};
},
&nefc, npoints);
ImPlot::PlotLineG("active", +[](int i, void* user_data) { ImPlot::PlotLineG(
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data); "active",
const float x = static_cast<float>(i); +[](int i, void* user_data) {
const float y = stats[i].nactive; const mjSolverStat* stats =
return ImPlotPoint{x, y}; static_cast<const mjSolverStat*>(user_data);
}, stats, npoints); 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) { ImPlot::PlotLineG(
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data); "changed",
const float x = static_cast<float>(i); +[](int i, void* user_data) {
const float y = stats[i].nchange; const mjSolverStat* stats =
return ImPlotPoint{x, y}; static_cast<const mjSolverStat*>(user_data);
}, stats, npoints); 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) { if (model->opt.solver == mjSOL_PGS) {
continue; continue;
} }
ImPlot::PlotLineG("evals", +[](int i, void* user_data) { ImPlot::PlotLineG(
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data); "evals",
const float x = static_cast<float>(i); +[](int i, void* user_data) {
const float y = stats[i].neval; const mjSolverStat* stats =
return ImPlotPoint{x, y}; static_cast<const mjSolverStat*>(user_data);
}, stats, npoints); 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) { if (model->opt.solver == mjSOL_CG) {
continue; continue;
} }
ImPlot::PlotLineG("updates", +[](int i, void* user_data) { ImPlot::PlotLineG(
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data); "updates",
const float x = static_cast<float>(i); +[](int i, void* user_data) {
const float y = stats[i].nupdate; const mjSolverStat* stats =
return ImPlotPoint{x, y}; static_cast<const mjSolverStat*>(user_data);
}, stats, npoints); const float x = static_cast<float>(i);
const float y = stats[i].nupdate;
return ImPlotPoint{x, y};
},
stats, npoints);
} }
ImPlot::PopStyleVar(); ImPlot::PopStyleVar();
+13
View File
@@ -62,6 +62,19 @@ void SetupTheme(GuiTheme theme);
// be used to place additional elements (e.g. floating charts). // be used to place additional elements (e.g. floating charts).
ImVec4 ConfigureDockingLayout(); 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, // UX for controlling the physics simulation parameters (e.g. integrator,
// solver, etc.) in mjModel. // solver, etc.) in mjModel.
void PhysicsGui(mjModel* model, float min_width); 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)) { if (ImGui::SmallButton(ICON_FA_PLUS)) {
ImGui::OpenPopupOnItemClick("BodyAddChild", 0); 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(); ImGui::SameLine();
} }
if (ImGui::SmallButton(ICON_FA_TRASH_CAN)) { if (ImGui::SmallButton(ICON_FA_TRASH_CAN)) {
editor.DeleteActiveElement(); editor.DeleteActiveElement();
*selected_element = nullptr; *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, static void SelectableElement(mjsElement* element,
+4 -3
View File
@@ -61,6 +61,10 @@ std::unique_ptr<ModelHolder> ModelHolder::FromBuffer(
return mh; return mh;
} }
ModelHolder::ModelHolder() {
mj_defaultVFS(&vfs_);
}
ModelHolder::~ModelHolder() { ModelHolder::~ModelHolder() {
if (data_) { if (data_) {
mj_deleteData(data_); mj_deleteData(data_);
@@ -94,7 +98,6 @@ void ModelHolder::InitFromSpec(mjSpec* spec) {
} }
void ModelHolder::InitFromFile(std::string_view filepath) { void ModelHolder::InitFromFile(std::string_view filepath) {
mj_defaultVFS(&vfs_);
if (filepath.ends_with(".mjb")) { if (filepath.ends_with(".mjb")) {
model_ = mj_loadModel(filepath.data(), &vfs_); model_ = mj_loadModel(filepath.data(), &vfs_);
} else { } else {
@@ -108,8 +111,6 @@ void ModelHolder::InitFromFile(std::string_view filepath) {
void ModelHolder::InitFromBuffer(std::span<const std::byte> buffer, void ModelHolder::InitFromBuffer(std::span<const std::byte> buffer,
std::string_view content_type, std::string_view content_type,
std::string_view filename) { std::string_view filename) {
mj_defaultVFS(&vfs_);
if (content_type == "text/xml") { if (content_type == "text/xml") {
const char* ptr = reinterpret_cast<const char*>(buffer.data()); const char* ptr = reinterpret_cast<const char*>(buffer.data());
spec_ = mj_parseXMLString(ptr, nullptr, error_, sizeof(error_)); spec_ = mj_parseXMLString(ptr, nullptr, error_, sizeof(error_));
+1 -1
View File
@@ -64,7 +64,7 @@ class ModelHolder {
mjModel* ReleaseModel(); mjModel* ReleaseModel();
private: private:
ModelHolder() = default; ModelHolder();
void InitFromSpec(mjSpec* spec); void InitFromSpec(mjSpec* spec);
void InitFromFile(std::string_view filepath); void InitFromFile(std::string_view filepath);
void InitFromBuffer(std::span<const std::byte> buffer, 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_COPY_CAMERA = platform::ICON_FA_COPY;
static constexpr const char* ICON_UNLOAD_MODEL = platform::ICON_FA_EJECT; 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_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_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_PREV_FRAME = platform::ICON_FA_CARET_LEFT;
static constexpr const char* ICON_NEXT_FRAME = platform::ICON_FA_CARET_RIGHT; static constexpr const char* ICON_NEXT_FRAME = platform::ICON_FA_CARET_RIGHT;
static constexpr const char* ICON_CURR_FRAME = platform::ICON_FA_FAST_FORWARD; 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_UNDO_SPEC = platform::ICON_FA_UNDO;
static constexpr const char* ICON_REDO_SPEC = platform::ICON_FA_REPEAT; 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) // logarithmically spaced real-time slow-down coefficients (percent)
// clang-format off // clang-format off
static constexpr std::array<const char*, 31> kPercentRealTime = { static constexpr std::array<const char*, 31> kPercentRealTime = {
@@ -846,7 +830,10 @@ void App::BuildGui() {
return; return;
} }
SetupTheme(ui_.theme); if (!tmp_.style_editor) {
platform::SetupTheme(ui_.theme);
}
const ImVec4 workspace_rect = platform::ConfigureDockingLayout(); const ImVec4 workspace_rect = platform::ConfigureDockingLayout();
// Place charts in bottom right corner of the workspace. // 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() { void App::ModelOptionsGui() {
const float min_width = GetExpectedLabelWidth(); const float min_width = GetExpectedLabelWidth();
const ImGuiChildFlags child_flags = const ImGuiChildFlags child_flags =
@@ -1449,7 +1428,7 @@ void App::ToolBarGui() {
const float label_width = GetExpectedLabelWidth(); const float label_width = GetExpectedLabelWidth();
const float copy_btn_width = ImGui::CalcTextSize(ICON_COPY_CAMERA).x + const float copy_btn_width = ImGui::CalcTextSize(ICON_COPY_CAMERA).x +
ImGui::GetStyle().FramePadding.x * 2; 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; ImGui::GetStyle().FramePadding.x * 2;
const float sp = ImGui::GetStyle().ItemSpacing.x; const float sp = ImGui::GetStyle().ItemSpacing.x;
const float right_width = label_width + sp + label_width + sp + const float right_width = label_width + sp + label_width + sp +
@@ -1576,84 +1555,30 @@ void App::ToolBarGui() {
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ImGui::SetCursorPosY(ImGui::GetCursorPosY() +
(button_size.y - ImGui::GetFrameHeight()) * 0.5f); (button_size.y - ImGui::GetFrameHeight()) * 0.5f);
// Camera selection.
if (ImGui::Button(ICON_COPY_CAMERA)) { if (ImGui::Button(ICON_COPY_CAMERA)) {
std::string camera_string = platform::CameraToString(data(), &camera_); std::string camera_string = platform::CameraToString(data(), &camera_);
platform::MaybeSaveToClipboard(camera_string); platform::MaybeSaveToClipboard(camera_string);
} }
ImGui::SetItemTooltip("%s", "Copy Camera"); ImGui::SetItemTooltip("%s", "Copy Camera");
ImGui::SameLine(0, 0); ImGui::SameLine(0, 0);
ImGui::SetNextItemWidth(GetExpectedLabelWidth()); ImGui::SetNextItemWidth(GetExpectedLabelWidth());
int camera_idx = ui_.camera_idx - platform::kTumbleCameraIdx; platform::CameraSelectionGui(model(), data(), camera_, ui_.camera_idx);
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");
// Label selection.
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetNextItemWidth(GetExpectedLabelWidth()); ImGui::SetNextItemWidth(GetExpectedLabelWidth());
std::string label_preview = platform::LabelSelectionGui(&vis_options_);
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");
// Frame selection.
ImGui::SameLine(); ImGui::SameLine();
ImGui::SetNextItemWidth(GetExpectedLabelWidth()); ImGui::SetNextItemWidth(GetExpectedLabelWidth());
std::string frame_preview = platform::FrameSelectionGui(&vis_options_);
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");
// Theme selection.
ImGui::SameLine(); ImGui::SameLine();
const char* theme_icons[] = {ICON_LIGHTMODE, ICON_DARKMODE, ImGui::SetNextItemWidth(GetExpectedLabelWidth());
ICON_CLASSICMODE}; if (platform::ThemeSelectGui(&ui_.theme)) {
const char* theme_tooltips[] = {"Light Mode", "Dark Mode", "Classic Mode"}; platform::SetupTheme(ui_.theme);
const platform::GuiTheme theme_values[] = { ImGui::GetIO().WantSaveIniSettings = true;
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::SetItemTooltip("%s", "Theme");
ImGui::EndTable(); ImGui::EndTable();
} }
@@ -2011,34 +1936,6 @@ float App::GetExpectedLabelWidth() {
return tmp_.expected_label_width; 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 { App::UiState::Dict App::UiState::ToDict() const {
return { return {
{"theme", std::to_string(static_cast<int>(theme))}, {"theme", std::to_string(static_cast<int>(theme))},
-1
View File
@@ -223,7 +223,6 @@ class App {
void SpecEditorGui(); void SpecEditorGui();
float GetExpectedLabelWidth(); float GetExpectedLabelWidth();
std::vector<const char*> GetCameraNames();
mjSpec* spec() { return model_holder_->spec(); } mjSpec* spec() { return model_holder_->spec(); }
mjModel* model() { return model_holder_->model(); } 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 // copy vertices
point = mesh.Vert(); point.assign(mesh.Vert().begin(), mesh.Vert().end());
if (mesh.HasTexcoord()) { if (mesh.HasTexcoord()) {
texcoord = mesh.Texcoord(); texcoord = mesh.Texcoord();
+122 -115
View File
@@ -492,7 +492,7 @@ void mjCMesh::CacheMesh(mjCCache* cache, const mjResource* resource) {
// calculate estimated size of mesh // calculate estimated size of mesh
std::size_t size = sizeof(mjCMesh) std::size_t size = sizeof(mjCMesh)
+ (sizeof(double) * vert_.size()) + (sizeof(float) * vert_.size())
+ (sizeof(float) * normal_.size()) + (sizeof(float) * normal_.size())
+ (sizeof(float) * texcoord_.size()) + (sizeof(float) * texcoord_.size())
+ (sizeof(int) * face_.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) { void mjCMesh::ProcessVertices(const std::vector<float>& vert, bool remove_repeated) {
vert_.clear(); vert_.clear();
int nvert = vert.size(); 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()); facenormal_.assign(src_mesh->userfacenormal->begin(), src_mesh->userfacenormal->end());
facetexcoord_.assign(src_mesh->userfacetexcoord->begin(), src_mesh->userfacetexcoord->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()); std::vector<float> vert(src_mesh->uservert->begin(), src_mesh->uservert->end());
mj_deleteSpec(mesh_spec); mj_deleteSpec(mesh_spec);
@@ -788,24 +775,24 @@ void mjCMesh::TryCompile(const mjVFS* vfs) {
// compute mesh properties // compute mesh properties
if (!fromCache) { if (!fromCache) {
Process(); Process();
}
// make octree if (!file_.empty()) {
if (!needsdf) { CacheMesh(cache, resource_);
octree_.Clear(); // this occurs when a non-SDF mesh is loaded from a cached SDF mesh }
} else if (octree_.NumNodes() == 0) { } else {
octree_.SetFace(vert_, face_); // When a mesh is loaded from the cache, has no octree but needs one,
octree_.CreateOctree(aamm_); // we need to compute it here. If inversely it has an octree but we *do not*
// need one, we clear it.
// compute sdf coefficients if (!needsdf) {
if (!plugin.active) { octree_.Clear();
octree_.ComputeSdfCoeffs(vert_.data(), nvert(), face_.data(), nface(), tree_); } 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 // close resource
@@ -818,18 +805,18 @@ void mjCMesh::TryCompile(const mjVFS* vfs) {
// get bounding volume // get bounding volume
void mjCMesh::SetBoundingVolume(int faceid) { void mjCMesh::SetBoundingVolume(int faceid, const double* dvert) {
constexpr double kMaxVal = std::numeric_limits<double>::max(); constexpr double kMaxVal = std::numeric_limits<double>::max();
double face_aamm[6] = {kMaxVal, kMaxVal, kMaxVal, -kMaxVal, -kMaxVal, -kMaxVal}; double face_aamm[6] = {kMaxVal, kMaxVal, kMaxVal, -kMaxVal, -kMaxVal, -kMaxVal};
for (int j = 0; j < 3; j++) { for (int j = 0; j < 3; j++) {
int vertid = face_[3*faceid + j]; int vertid = face_[3*faceid + j];
face_aamm[0] = std::min(face_aamm[0], vert_[3*vertid + 0]); face_aamm[0] = std::min(face_aamm[0], dvert[3*vertid + 0]);
face_aamm[1] = std::min(face_aamm[1], vert_[3*vertid + 1]); face_aamm[1] = std::min(face_aamm[1], dvert[3*vertid + 1]);
face_aamm[2] = std::min(face_aamm[2], vert_[3*vertid + 2]); face_aamm[2] = std::min(face_aamm[2], dvert[3*vertid + 2]);
face_aamm[3] = std::max(face_aamm[3], vert_[3*vertid + 0]); face_aamm[3] = std::max(face_aamm[3], dvert[3*vertid + 0]);
face_aamm[4] = std::max(face_aamm[4], vert_[3*vertid + 1]); face_aamm[4] = std::max(face_aamm[4], dvert[3*vertid + 1]);
face_aamm[5] = std::max(face_aamm[5], vert_[3*vertid + 2]); face_aamm[5] = std::max(face_aamm[5], dvert[3*vertid + 2]);
} }
face_aabb_.push_back(.5 * (face_aamm[0] + face_aamm[3])); 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 { void mjCMesh::CopyVert(float* arr) const {
for (int i = 0; i < vert_.size(); ++i) { std::copy(vert_.begin(), vert_.end(), arr);
arr[i] = (float)vert_[i];
}
} }
@@ -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 // 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; double normal[3], center[3], total_volume = 0;
CoM[0] = CoM[1] = CoM[2] = 0; CoM[0] = CoM[1] = CoM[2] = 0;
int nf = (inertia == mjMESH_INERTIA_CONVEX) ? graph_[1] : nface(); int nf = (inertia == mjMESH_INERTIA_CONVEX) ? graph_[1] : nface();
const int* f = (inertia == mjMESH_INERTIA_CONVEX) ? GraphFaces() : face_.data(); const int* f = (inertia == mjMESH_INERTIA_CONVEX) ? GraphFaces() : face_.data();
for (int i = 0; i < nf; i++) { for (int i = 0; i < nf; i++) {
// get area, normal and center double area = triangle(normal, center, &dvert[3*f[3*i]], &dvert[3*f[3*i + 1]],
double area = triangle(normal, center, &vert_[3*f[3*i]], &vert_[3*f[3*i + 1]], &dvert[3*f[3*i + 2]]);
&vert_[3*f[3*i + 2]]);
// compute and add volume // compute and add volume
double vec[3] = {center[0] - facecen[0], center[1] - facecen[1], center[2] - facecen[2]}; 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 // 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; double surface = 0;
CoM[0] = CoM[1] = CoM[2] = 0; CoM[0] = CoM[1] = CoM[2] = 0;
for (int i = 0; i < nface(); i++) { for (int i = 0; i < nface(); i++) {
// get area and center
double area, center[3]; double area, center[3];
area = triangle(nullptr, center, &vert_[3*face_[3*i]], area = triangle(nullptr, center, &dvert[3*face_[3*i]],
&vert_[3*face_[3*i + 1]], &vert_[3*face_[3*i + 2]]); &dvert[3*face_[3*i + 1]], &dvert[3*face_[3*i + 2]]);
// add pyramid com // add pyramid com
surface += area; surface += area;
@@ -1267,28 +1250,24 @@ double mjCMesh::ComputeSurfaceArea(double CoM[3], const double facecen[3]) const
// apply transformations // apply transformations
void mjCMesh::ApplyTransformations() { void mjCMesh::ApplyTransformations(double* dvert) {
// translate
if (refpos[0] != 0 || refpos[1] != 0 || refpos[2] != 0) { if (refpos[0] != 0 || refpos[1] != 0 || refpos[2] != 0) {
int nv = nvert(); int nv = nvert();
for (int i = 0; i < nv; i++) { for (int i = 0; i < nv; i++) {
vert_[3*i + 0] -= refpos[0]; dvert[3*i + 0] -= refpos[0];
vert_[3*i + 1] -= refpos[1]; dvert[3*i + 1] -= refpos[1];
vert_[3*i + 2] -= refpos[2]; dvert[3*i + 2] -= refpos[2];
} }
} }
// rotate
if (refquat[0] != 1 || refquat[1] != 0 || refquat[2] != 0 || refquat[3] != 0) { 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 quat[4] = {refquat[0], refquat[1], refquat[2], refquat[3]};
double mat[9]; double mat[9];
mjuu_normvec(quat, 4); mjuu_normvec(quat, 4);
mjuu_quat2mat(mat, quat); mjuu_quat2mat(mat, quat);
// process vertices
for (int i = 0; i < nvert(); i++) { 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 // process normals
@@ -1304,9 +1283,9 @@ void mjCMesh::ApplyTransformations() {
// scale // scale
if (scale[0] != 1 || scale[1] != 1 || scale[2] != 1) { if (scale[0] != 1 || scale[1] != 1 || scale[2] != 1) {
for (int i = 0; i < nvert(); i++) { for (int i = 0; i < nvert(); i++) {
vert_[3*i + 0] *= scale[0]; dvert[3*i + 0] *= scale[0];
vert_[3*i + 1] *= scale[1]; dvert[3*i + 1] *= scale[1];
vert_[3*i + 2] *= scale[2]; dvert[3*i + 2] *= scale[2];
} }
for (int i = 0; i < nnormal(); i++) { for (int i = 0; i < nnormal(); i++) {
@@ -1338,14 +1317,13 @@ void mjCMesh::ApplyTransformations() {
// find centroid of faces, return total area // 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; double total_area = 0;
for (int i = 0; i < nface(); i++) { for (int i = 0; i < nface(); i++) {
// get area and center
double area, center[3]; double area, center[3];
area = triangle(nullptr, center, &vert_[3*face_[3*i]], area = triangle(nullptr, center, &dvert[3*face_[3*i]],
&vert_[3*face_[3*i + 1]], &vert_[3*face_[3*i + 2]]); &dvert[3*face_[3*i + 1]], &dvert[3*face_[3*i + 2]]);
// accumulate // accumulate
facecen[0] += area * center[0]; facecen[0] += area * center[0];
@@ -1366,13 +1344,15 @@ double mjCMesh::ComputeFaceCentroid(double facecen[3]) const {
void mjCMesh::Process() { void mjCMesh::Process() {
std::vector<double> dvert(vert_.begin(), vert_.end());
// create half-edge structure (if mesh was in XML) // create half-edge structure (if mesh was in XML)
if (halfedge_.empty()) { if (halfedge_.empty()) {
for (int i = 0; i < nface(); i++) { for (int i = 0; i < nface(); i++) {
int v0 = face_[3*i + 0]; int v0 = face_[3*i + 0];
int v1 = face_[3*i + 1]; int v1 = face_[3*i + 1];
int v2 = face_[3*i + 2]; 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({v0, v1});
halfedge_.push_back({v1, v2}); halfedge_.push_back({v1, v2});
halfedge_.push_back({v2, v0}); halfedge_.push_back({v2, v0});
@@ -1396,7 +1376,7 @@ void mjCMesh::Process() {
// make graph describing convex hull // make graph describing convex hull
if (needhull_ || face_.empty()) { if (needhull_ || face_.empty()) {
MakeGraph(); MakeGraph(dvert.data());
} }
// no faces: copy from convex hull // no faces: copy from convex hull
@@ -1406,7 +1386,7 @@ void mjCMesh::Process() {
// no normals: make // no normals: make
if (normal_.empty()) { if (normal_.empty()) {
MakeNormal(); MakeNormal(dvert.data());
} }
// check facenormal size // check facenormal size
@@ -1429,17 +1409,31 @@ void mjCMesh::Process() {
} }
if (szgraph_) { if (szgraph_) {
MakePolygons(); MakePolygons(dvert.data());
} else { } else {
polygon_map_.resize(nvert()); 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 // user offset, rotation, scaling
ApplyTransformations(); ApplyTransformations(dvert.data());
// find centroid of faces // find centroid of faces
double facecen[3] = {0, 0, 0}; 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()); 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 // compute CoM and volume/area
if (inertia == mjMESH_INERTIA_SHELL) { if (inertia == mjMESH_INERTIA_SHELL) {
surface_ = ComputeSurfaceArea(CoM, facecen); surface_ = ComputeSurfaceArea(CoM, facecen, dvert.data());
if (surface_ < mjMINVAL) { if (surface_ < mjMINVAL) {
throw mjCError(this, "mesh surface area is too small: %s", name.c_str()); throw mjCError(this, "mesh surface area is too small: %s", name.c_str());
} }
} else { } else {
if ((volume_ = ComputeVolume(CoM, facecen)) < mjMINVAL) { if ((volume_ = ComputeVolume(CoM, facecen, dvert.data())) < mjMINVAL) {
if (volume_ < 0) { if (volume_ < 0) {
throw mjCError(this, "mesh volume is negative (misoriented triangles): %s", name.c_str()); throw mjCError(this, "mesh volume is negative (misoriented triangles): %s", name.c_str());
} else { } else {
@@ -1466,7 +1460,7 @@ void mjCMesh::Process() {
} }
// compute inertia // compute inertia
double total_volume = ComputeInertia(inert, CoM); double total_volume = ComputeInertia(inert, CoM, dvert.data());
if (inertia == mjMESH_INERTIA_SHELL) { if (inertia == mjMESH_INERTIA_SHELL) {
surface_ = total_volume; surface_ = total_volume;
} else { } else {
@@ -1510,11 +1504,11 @@ void mjCMesh::Process() {
// transform CoM to origin // transform CoM to origin
for (int i=0; i < nvert(); i++) { for (int i=0; i < nvert(); i++) {
vert_[3*i + 0] -= CoM[0]; dvert[3*i + 0] -= CoM[0];
vert_[3*i + 1] -= CoM[1]; dvert[3*i + 1] -= CoM[1];
vert_[3*i + 2] -= CoM[2]; dvert[3*i + 2] -= CoM[2];
} }
Rotate(quattmp); Rotate(quattmp, dvert.data());
// save the pos and quat that was used to transform the mesh // save the pos and quat that was used to transform the mesh
mjuu_copyvec(pos_, CoM, 3); mjuu_copyvec(pos_, CoM, 3);
@@ -1524,11 +1518,11 @@ void mjCMesh::Process() {
// no radii: make // no radii: make
if (!center_) { if (!center_) {
MakeCenter(); MakeCenter(dvert.data());
} }
// recompute polygon normals // recompute polygon normals
MakePolygonNormals(); MakePolygonNormals(dvert.data());
// make bounding volume hierarchy // make bounding volume hierarchy
if (tree_.Bvh().empty()) { if (tree_.Bvh().empty()) {
@@ -1536,16 +1530,31 @@ void mjCMesh::Process() {
face_aabb_.reserve(3*face_.size()); face_aabb_.reserve(3*face_.size());
tree_.AllocateBoundingVolumes(nface()); tree_.AllocateBoundingVolumes(nface());
for (int i = 0; i < nface(); i++) { for (int i = 0; i < nface(); i++) {
SetBoundingVolume(i); SetBoundingVolume(i, dvert.data());
} }
tree_.CreateBVH(); 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 // 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; double total_volume = 0;
// copy vertices to avoid modifying the original mesh // 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 // translate vertices to origin in order to compute inertia
for (int i = 0; i < nvert(); i++) { for (int i = 0; i < nvert(); i++) {
vert_centered.push_back(vert_[3*i + 0] - CoM[0]); vert_centered.push_back(dvert[3*i + 0] - CoM[0]);
vert_centered.push_back(vert_[3*i + 1] - CoM[1]); vert_centered.push_back(dvert[3*i + 1] - CoM[1]);
vert_centered.push_back(vert_[3*i + 2] - CoM[2]); vert_centered.push_back(dvert[3*i + 2] - CoM[2]);
} }
// accumulate products of inertia, recompute volume // 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]) { void mjCMesh::Rotate(double quat[4], double* dvert) {
// rotate vertices and normals of mesh by quaternion
double neg[4] = {quat[0], -quat[1], -quat[2], -quat[3]}; double neg[4] = {quat[0], -quat[1], -quat[2], -quat[3]};
double mat[9]; double mat[9];
mjuu_quat2mat(mat, neg); mjuu_quat2mat(mat, neg);
int nv = nvert(); int nv = nvert();
for (int i = 0; i < nv; i++) { 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], dvert[3*i + 0]);
aamm_[0] = std::min(aamm_[0], vert_[3*i + 0]); aamm_[3] = std::max(aamm_[3], dvert[3*i + 0]);
aamm_[3] = std::max(aamm_[3], vert_[3*i + 0]); aamm_[1] = std::min(aamm_[1], dvert[3*i + 1]);
aamm_[1] = std::min(aamm_[1], vert_[3*i + 1]); aamm_[4] = std::max(aamm_[4], dvert[3*i + 1]);
aamm_[4] = std::max(aamm_[4], vert_[3*i + 1]); aamm_[2] = std::min(aamm_[2], dvert[3*i + 2]);
aamm_[2] = std::min(aamm_[2], vert_[3*i + 2]); aamm_[5] = std::max(aamm_[5], dvert[3*i + 2]);
aamm_[5] = std::max(aamm_[5], vert_[3*i + 2]);
} }
for (int i=0; i < nnormal(); i++) { for (int i=0; i < nnormal(); i++) {
@@ -1698,7 +1705,7 @@ double mjCMesh::GetVolumeRef() const {
// make graph describing convex hull // make graph describing convex hull
void mjCMesh::MakeGraph() { void mjCMesh::MakeGraph(const double* dvert) {
int adr, ok, curlong, totlong, exitcode; int adr, ok, curlong, totlong, exitcode;
facetT* facet, **facetp; facetT* facet, **facetp;
vertexT* vertex, *vertex1, **vertex1p; vertexT* vertex, *vertex1, **vertex1p;
@@ -1727,7 +1734,7 @@ void mjCMesh::MakeGraph() {
if (!exitcode) { if (!exitcode) {
// actual init // actual init
qh_initflags(qh, const_cast<char*>(qhopt.c_str())); 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 // construct convex hull
qh_qhull(qh); qh_qhull(qh);
@@ -2402,7 +2409,7 @@ void mjCMesh::MakeCone(int nedge, double radius) {
// compute vertex normals // compute vertex normals
void mjCMesh::MakeNormal() { void mjCMesh::MakeNormal(const double* dvert) {
// only if normal data is missing // only if normal data is missing
if (!normal_.empty()) { if (!normal_.empty()) {
return; return;
@@ -2426,8 +2433,8 @@ void mjCMesh::MakeNormal() {
// get triangle edges // get triangle edges
double vec01[3], vec02[3]; double vec01[3], vec02[3];
for (int j=0; j < 3; j++) { for (int j=0; j < 3; j++) {
vec01[j] = vert_[3*vertid[1]+j] - vert_[3*vertid[0]+j]; vec01[j] = dvert[3*vertid[1]+j] - dvert[3*vertid[0]+j];
vec02[j] = vert_[3*vertid[2]+j] - vert_[3*vertid[0]+j]; vec02[j] = dvert[3*vertid[2]+j] - dvert[3*vertid[0]+j];
} }
// compute face normal // compute face normal
@@ -2461,8 +2468,8 @@ void mjCMesh::MakeNormal() {
// get triangle edges // get triangle edges
double vec01[3], vec02[3]; double vec01[3], vec02[3];
for (int j=0; j < 3; j++) { for (int j=0; j < 3; j++) {
vec01[j] = vert_[3*vertid[1]+j] - vert_[3*vertid[0]+j]; vec01[j] = dvert[3*vertid[1]+j] - dvert[3*vertid[0]+j];
vec02[j] = vert_[3*vertid[2]+j] - vert_[3*vertid[0]+j]; vec02[j] = dvert[3*vertid[2]+j] - dvert[3*vertid[0]+j];
} }
// compute face normal // compute face normal
@@ -2514,7 +2521,7 @@ void mjCMesh::MakeNormal() {
// compute face circumradii // compute face circumradii
void mjCMesh::MakeCenter() { void mjCMesh::MakeCenter(const double* dvert) {
if (center_) { if (center_) {
return; return;
} }
@@ -2530,8 +2537,8 @@ void mjCMesh::MakeCenter() {
// get triangle edges // get triangle edges
double a[3], b[3]; double a[3], b[3];
for (int j=0; j < 3; j++) { for (int j=0; j < 3; j++) {
a[j] = vert_[3*vertid[0]+j] - vert_[3*vertid[2]+j]; a[j] = dvert[3*vertid[0]+j] - dvert[3*vertid[2]+j];
b[j] = vert_[3*vertid[1]+j] - vert_[3*vertid[2]+j]; b[j] = dvert[3*vertid[1]+j] - dvert[3*vertid[2]+j];
} }
// compute face normal // compute face normal
@@ -2550,20 +2557,20 @@ void mjCMesh::MakeCenter() {
norm_a_2 * b[2] - norm_b_2 * a[2] norm_a_2 * b[2] - norm_b_2 * a[2]
}; };
mjuu_crossvec(res, vec, nrm); mjuu_crossvec(res, vec, nrm);
center_[3*i+0] = res[0]/(2*area*area) + vert_[3*vertid[2]+0]; center_[3*i+0] = res[0]/(2*area*area) + dvert[3*vertid[2]+0];
center_[3*i+1] = res[1]/(2*area*area) + vert_[3*vertid[2]+1]; center_[3*i+1] = res[1]/(2*area*area) + dvert[3*vertid[2]+1];
center_[3*i+2] = res[2]/(2*area*area) + vert_[3*vertid[2]+2]; center_[3*i+2] = res[2]/(2*area*area) + dvert[3*vertid[2]+2];
} }
} }
// compute the normals of the polygons // compute the normals of the polygons
void mjCMesh::MakePolygonNormals() { void mjCMesh::MakePolygonNormals(const double* dvert) {
for (int i = 0; i < polygons_.size(); ++i) { for (int i = 0; i < polygons_.size(); ++i) {
double n[3]; double n[3];
mjuu_makenormal(n, &vert_[3*polygons_[i][0]], &vert_[3*polygons_[i][1]], mjuu_makenormal(n, &dvert[3*polygons_[i][0]], &dvert[3*polygons_[i][1]],
&vert_[3*polygons_[i][2]]); &dvert[3*polygons_[i][2]]);
polygon_normals_[3*i + 0] = n[0]; polygon_normals_[3*i + 0] = n[0];
polygon_normals_[3*i + 1] = n[1]; polygon_normals_[3*i + 1] = n[1];
polygon_normals_[3*i + 2] = n[2]; 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 // 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; constexpr double kAngleTol = 0.01;
std::unordered_map<std::pair<double, double>, MeshPolygon, PairHash> mesh_polygons; std::unordered_map<std::pair<double, double>, MeshPolygon, PairHash> mesh_polygons;
polygons_.clear(); polygons_.clear();
@@ -2819,9 +2826,9 @@ void mjCMesh::MakePolygons() {
int vi1 = faces[3*i + 0]; int vi1 = faces[3*i + 0];
int vi2 = faces[3*i + 1]; int vi2 = faces[3*i + 1];
int vi3 = faces[3*i + 2]; int vi3 = faces[3*i + 2];
double* v1 = &vert_[3*vi1]; const double* v1 = &dvert[3*vi1];
double* v2 = &vert_[3*vi2]; const double* v2 = &dvert[3*vi2];
double* v3 = &vert_[3*vi3]; const double* v3 = &dvert[3*vi3];
std::pair<double, double> key; std::pair<double, double> key;
if (!MeshPolygonKey(key, v1, v2, v3, kAngleTol)) { 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) { 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) { 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> 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]}; 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]) { 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, 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}; (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], 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(); sizeof(Point) * vert_.size();
} }
void Clear() { void Clear() {
nnode_ = 0;
nvert_ = 0;
node_.clear(); node_.clear();
face_.clear(); vert_.clear();
hang_.clear();
} }
void AddCoeff(int n, int v, double coeff) { node_[n].coeff[v] = coeff; } 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]; } 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 content_type_ = ""; // content type of file
std::string file_; // mesh file std::string file_; // mesh file
mjResource* resource_ = nullptr; // resource for 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> normal_; // normal data
std::vector<float> texcoord_; // texcoord data std::vector<float> texcoord_; // texcoord data
std::vector<int> face_; // vertex indices std::vector<int> face_; // vertex indices
@@ -1156,8 +1159,8 @@ class mjCMesh: public mjCMesh_, private mjsMesh {
const double* Refquat() const { return refquat; } const double* Refquat() const { return refquat; }
const double* Scale() const { return scale; } const double* Scale() const { return scale; }
bool SmoothNormal() const { return smoothnormal; } bool SmoothNormal() const { return smoothnormal; }
const std::vector<double>& Vert() const { return vert_; } const std::vector<float>& Vert() const { return vert_; }
double Vert(int i) const { return vert_[i]; } float Vert(int i) const { return vert_[i]; }
const std::vector<float>& UserVert() const { return spec_vert_; } const std::vector<float>& UserVert() const { return spec_vert_; }
const std::vector<float>& UserNormal() const { return spec_normal_; } const std::vector<float>& UserNormal() const { return spec_normal_; }
const std::vector<float>& Texcoord() const { return texcoord_; } const std::vector<float>& Texcoord() const { return texcoord_; }
@@ -1233,7 +1236,7 @@ class mjCMesh: public mjCMesh_, private mjsMesh {
void CopyPolygonNormals(mjtNum* arr); void CopyPolygonNormals(mjtNum* arr);
// sets properties of a bounding volume given a face id // 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 // load from OBJ, STL, or MSH file; throws mjCError on failure
void LoadFromResource(mjResource* resource, bool remove_repeated = false); 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 LoadMSH(mjResource* resource, bool remove_repeated); // load mesh in MSH BIN format
void LoadSDF(); // generate mesh using marching cubes 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 CopyGraph(); // copy graph into face data
void MakeNormal(); // compute vertex normals void MakeNormal(const double* dvert); // compute vertex normals
void MakeCenter(); // compute face circumcircle data void MakeCenter(const double* dvert); // compute face circumcircle data
void Process(); // compute inertial properties void Process(); // compute inertial properties
void ApplyTransformations(); // apply user transformations void ApplyTransformations(double* dvert); // apply user transformations
double ComputeFaceCentroid(double[3]) const; // compute centroid of all faces double ComputeFaceCentroid(double[3], const double* dvert) const;
void CheckInitialMesh() const; // check if initial mesh is valid void CheckInitialMesh() const; // check if initial mesh is valid
void CopyPlugin(); 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 Transform(double pos[3], double quat[4]); // transform mesh by position and quaternion
void MakePolygons(); // compute the polygon sides of the mesh void MakePolygons(const double* dvert); // compute the polygon sides of the mesh
void MakePolygonNormals(); // compute the normals of the polygons void MakePolygonNormals(const double* dvert); // compute the normals of the polygons
// computes the inertia matrix of the mesh given the type of inertia // 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 { int* GraphFaces() const {
return graph_ + 2 + 3*(graph_[0] + graph_[1]); 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 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 // compute the volume and center-of-mass of the mesh given the face centroid
double ComputeVolume(double CoM[3], const double facecen[3]) const; double ComputeVolume(double CoM[3], const double facecen[3], const double* dvert) 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* dvert) const;
double ComputeSurfaceArea(double CoM[3], const double facecen[3]) const;
}; };
+3 -3
View File
@@ -44,9 +44,9 @@ mjResource* mju_openResource(const char* dir, const char* name,
if (non_const_vfs == nullptr) { if (non_const_vfs == nullptr) {
mjVFS* local_vfs = (mjVFS*)mju_malloc(sizeof(mjVFS)); mjVFS* local_vfs = (mjVFS*)mju_malloc(sizeof(mjVFS));
mj_defaultVFS(local_vfs); mj_defaultVFS(local_vfs);
mujoco::user::VFS::Upcast(local_vfs)->SetToSelfDestruct([](mjVFS* ptr) { mujoco::user::VFS::Upcast(local_vfs)->SetToSelfDestruct([=]() {
mj_deleteVFS(ptr); mj_deleteVFS(local_vfs);
mju_free(ptr); mju_free(local_vfs);
}); });
non_const_vfs = local_vfs; non_const_vfs = local_vfs;
+9 -5
View File
@@ -105,7 +105,8 @@ std::string StripPathAndLower(std::string path) {
namespace mujoco::user { namespace mujoco::user {
VFS::VFS(mjVFS* vfs) : self_(vfs) { VFS::VFS(mjVFS* vfs) {
wrapped_vfs_.impl_ = this;
mjp_defaultResourceProvider(&default_provider_); mjp_defaultResourceProvider(&default_provider_);
default_provider_.open = [](mjResource* res) { default_provider_.open = [](mjResource* res) {
return OpenFile(res->name, res); return OpenFile(res->name, res);
@@ -121,7 +122,7 @@ VFS::VFS(mjVFS* vfs) : self_(vfs) {
}; };
default_provider_.prefix = nullptr; default_provider_.prefix = nullptr;
default_mount_.vfs = self_; default_mount_.vfs = &wrapped_vfs_;
default_mount_.provider = &default_provider_; default_mount_.provider = &default_provider_;
default_mount_.data = nullptr; default_mount_.data = nullptr;
default_mount_.name = 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, VFS::ResourcePtr VFS::CreateResource(std::string_view name,
const mjpResourceProvider* provider) { const mjpResourceProvider* provider) {
mjResource* res = new mjResource(); mjResource* res = new mjResource();
res->vfs = self_; res->vfs = &wrapped_vfs_;
res->provider = provider; res->provider = provider;
res->data = nullptr; res->data = nullptr;
res->name = new char[name.size() + 1]; res->name = new char[name.size() + 1];
@@ -312,11 +313,14 @@ mjResource* VFS::FindMount(const std::string& fullpath) {
void VFS::MaybeSelfDestruct() { void VFS::MaybeSelfDestruct() {
if (destructor_) { 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); 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 // 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` // 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. // 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. // Converts the public C-API pointer to the internal C++ class.
static VFS* Upcast(mjVFS* vfs); static VFS* Upcast(mjVFS* vfs);
@@ -117,13 +117,13 @@ class VFS {
// that `this` will be invalidated after this call. // that `this` will be invalidated after this call.
void MaybeSelfDestruct(); void MaybeSelfDestruct();
mjVFS* self_; mjVFS wrapped_vfs_;
std::mutex mutex_; // Protects open_resources_ and mounts_. std::mutex mutex_; // Protects open_resources_ and mounts_.
std::unordered_map<mjResource*, ResourcePtr> open_resources_; std::unordered_map<mjResource*, ResourcePtr> open_resources_;
std::unordered_map<std::string, ResourcePtr> mounts_; std::unordered_map<std::string, ResourcePtr> mounts_;
mjResource default_mount_; mjResource default_mount_;
mjpResourceProvider default_provider_; mjpResourceProvider default_provider_;
std::function<void(mjVFS*)> destructor_; std::function<void()> destructor_;
}; };
} // namespace mujoco::user } // namespace mujoco::user
+65
View File
@@ -1086,6 +1086,71 @@ TEST_F(MjCMeshTest, MeshScale) {
mj_deleteModel(model); 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) { TEST_F(MjCMeshTest, ShellInertiaTest) {
static constexpr char xml[] = R"( static constexpr char xml[] = R"(
<mujoco> <mujoco>
+24
View File
@@ -407,5 +407,29 @@ TEST_F(UserVfsTest, StackedMounts) {
EXPECT_EQ(test2, expect2); EXPECT_EQ(test2, expect2);
EXPECT_EQ(test3, expect3); 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
} // namespace mujoco } // namespace mujoco
@@ -37,7 +37,7 @@ public class MujocoBinaryRetriever {
if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/mujoco.dylib") == null) { if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/mujoco.dylib") == null) {
File.Copy( File.Copy(
"/Applications/MuJoCo.app/Contents/Frameworks" + "/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"); mujocoPath + "/mujoco.dylib");
AssetDatabase.Refresh(); AssetDatabase.Refresh();
} }
@@ -45,7 +45,7 @@ public class MujocoBinaryRetriever {
if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/libmujoco.so") == null) { if (AssetDatabase.LoadMainAssetAtPath(mujocoPath + "/libmujoco.so") == null) {
File.Copy( File.Copy(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + 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"); mujocoPath + "/libmujoco.so");
AssetDatabase.Refresh(); AssetDatabase.Refresh();
} }
+1 -1
View File
@@ -112,7 +112,7 @@ public const int mjMAXLINEPNT = 1001;
public const int mjMAXPLANEGRID = 200; public const int mjMAXPLANEGRID = 200;
public const bool THIRD_PARTY_MUJOCO_MJXMACRO_H_ = true; public const bool THIRD_PARTY_MUJOCO_MJXMACRO_H_ = true;
public const bool THIRD_PARTY_MUJOCO_MUJOCO_H_ = true; public const bool THIRD_PARTY_MUJOCO_MUJOCO_H_ = true;
public const int mjVERSION_HEADER = 3006000; public const int mjVERSION_HEADER = 3007000;
// ------------------------------------Enums------------------------------------ // ------------------------------------Enums------------------------------------
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "org.mujoco", "name": "org.mujoco",
"displayName": "MuJoCo", "displayName": "MuJoCo",
"version": "3.6.0", "version": "3.7.0",
"description": "MuJoCo importer and runtime plug-in", "description": "MuJoCo importer and runtime plug-in",
"dependencies": {}, "dependencies": {},
"author": { "author": {
+14 -6
View File
@@ -307,7 +307,7 @@ The function `mjv_updateScene` populates an `mjvScene` object with information
from `mjModel` and `mjData`. from `mjModel` and `mjData`.
```typescript ```typescript
// Create instances of the necessary structs. // 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 data = new mujoco.MjData(model);
const scene = new mujoco.MjvScene(model, 1000); const scene = new mujoco.MjvScene(model, 1000);
const option = new mujoco.MjvOption(); const option = new mujoco.MjvOption();
@@ -348,23 +348,31 @@ mujoco.mjtDisableBit.mjDSBL_CLAMPCTRL.value
``` ```
### Constants ### Constants
Scalar constants will be accessed the same way they are on python, simply: Scalar constants can be accessed as properties:
```javascript ```javascript
mujoco.mjNEQDATA mujoco.mjNEQDATA
``` ```
Due to Embind limitations, more complex constants that are not scalar, but are Non-scalar constants like `mjFRAMESTRING` are also accessed as properties, and
represented in more dimensions are exposed as functions. E.g. to use return JavaScript arrays:
`mujoco.mjFRAMESTRING` you will need to call a function:
```javascript ```javascript
mujoco.get_mjFRAMESTRING() mujoco.mjFRAMESTRING
``` ```
This will return a javascript array representation of the values in MuJoCo This will return a javascript array representation of the values in MuJoCo
`mjFRAMESTRING`. `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 ## Development
In order to change the bindings you will need to change the [`bindings.cc`](codegen/generated/bindings.cc) 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.h>
#include <emscripten/bind.h> #include <emscripten/bind.h>
#include <emscripten/em_asm.h>
#include <emscripten/val.h> #include <emscripten/val.h>
#include <algorithm> #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)); 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) { std::unique_ptr<MjSpec> parseXMLString_wrapper(const std::string &xml) {
char error[1000]; char error[1000];
mjSpec *ptr = mj_parseXMLString(xml.c_str(), nullptr, error, sizeof(error)); 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()); 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); UNPACK_NULLABLE_VALUE(mjtNum, fromto);
CHECK_SIZE(fromto, 6); CHECK_SIZE(fromto, 6);
return mj_geomDistance(m.get(), d.get(), geom1, geom2, distmax, fromto_.data()); 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("useexisting", &MjLROpt::useexisting, &MjLROpt::set_useexisting, reference())
.property("uselimit", &MjLROpt::uselimit, &MjLROpt::set_uselimit, reference()); .property("uselimit", &MjLROpt::uselimit, &MjLROpt::set_uselimit, reference());
emscripten::class_<MjModel>("MjModel") 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)>(&mj_loadXML_wrapper_1))
.class_function("mj_loadXML", emscripten::select_overload<std::unique_ptr<MjModel>(std::string, const MjVFS&)>(&mj_loadXML_wrapper_2)) .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("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 &>() .constructor<const MjModel &>()
// Binds the functions on MjModel that return accessors. // Binds the functions on MjModel that return accessors.
#define X_ACCESSOR(NAME, Name, OBJTYPE, field_name, nfield) \ #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. // 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&)>(&mj_compile_wrapper_1));
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&, const MjVFS&)>(&mj_compile_wrapper_2)); 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") emscripten::class_<WasmBuffer<float>>("FloatBuffer")
.constructor<int>() .constructor<int>()
@@ -13409,6 +13452,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.function("GetView", &WasmBuffer<uint8_t>::GetView); .function("GetView", &WasmBuffer<uint8_t>::GetView);
emscripten::register_vector<std::string>("mjStringVec"); emscripten::register_vector<std::string>("mjStringVec");
emscripten::register_vector<std::vector<std::string>>("mjStringVecVec");
emscripten::register_vector<int>("mjIntVec"); emscripten::register_vector<int>("mjIntVec");
emscripten::register_vector<mjIntVec>("mjIntVecVec"); emscripten::register_vector<mjIntVec>("mjIntVecVec");
emscripten::register_vector<float>("mjFloatVec"); emscripten::register_vector<float>("mjFloatVec");
@@ -13453,7 +13497,6 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
emscripten::constant("mjPI", mjPI); emscripten::constant("mjPI", mjPI);
emscripten::constant("mjVERSION_HEADER", mjVERSION_HEADER); 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_mjDISABLESTRING", &get_mjDISABLESTRING);
emscripten::function("get_mjENABLESTRING", &get_mjENABLESTRING); emscripten::function("get_mjENABLESTRING", &get_mjENABLESTRING);
emscripten::function("get_mjFRAMESTRING", &get_mjFRAMESTRING); emscripten::function("get_mjFRAMESTRING", &get_mjFRAMESTRING);
@@ -13461,6 +13504,31 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
emscripten::function("get_mjRNDSTRING", &get_mjRNDSTRING); emscripten::function("get_mjRNDSTRING", &get_mjRNDSTRING);
emscripten::function("get_mjTIMERSTRING", &get_mjTIMERSTRING); emscripten::function("get_mjTIMERSTRING", &get_mjTIMERSTRING);
emscripten::function("get_mjVISSTRING", &get_mjVISSTRING); 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 } // namespace mujoco::wasm
+27
View File
@@ -549,6 +549,9 @@ def _build_struct_bindings(
MJDATA_ACCESSORS MJDATA_ACCESSORS
#undef X_ACCESSOR""".lstrip()) #undef X_ACCESSOR""".lstrip())
elif w == "MjModel": elif w == "MjModel":
builder.line(
"// mj_loadXML is deprecated and will be removed in a future release"
)
builder.line( builder.line(
'.class_function("mj_loadXML",' '.class_function("mj_loadXML",'
" emscripten::select_overload<std::unique_ptr<MjModel>(std::string)>(&mj_loadXML_wrapper_1))" " 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( f2 = common.wrapped_function_name(
introspect_functions.FUNCTIONS["mj_loadModel"] 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("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(".constructor<const MjModel &>()")
builder.line(""" builder.line("""
// Binds the functions on MjModel that return accessors. // Binds the functions on MjModel that return accessors.
+62 -1
View File
@@ -17,6 +17,7 @@
#include <emscripten.h> #include <emscripten.h>
#include <emscripten/bind.h> #include <emscripten/bind.h>
#include <emscripten/em_asm.h>
#include <emscripten/val.h> #include <emscripten/val.h>
#include <algorithm> #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)); 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) { std::unique_ptr<MjSpec> parseXMLString_wrapper(const std::string &xml) {
char error[1000]; char error[1000];
mjSpec *ptr = mj_parseXMLString(xml.c_str(), nullptr, error, sizeof(error)); 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. // 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&)>(&mj_compile_wrapper_1));
function("mj_compile", emscripten::select_overload<std::unique_ptr<MjModel>(const MjSpec&, const MjVFS&)>(&mj_compile_wrapper_2)); 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") emscripten::class_<WasmBuffer<float>>("FloatBuffer")
.constructor<int>() .constructor<int>()
@@ -875,6 +911,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.function("GetView", &WasmBuffer<uint8_t>::GetView); .function("GetView", &WasmBuffer<uint8_t>::GetView);
emscripten::register_vector<std::string>("mjStringVec"); emscripten::register_vector<std::string>("mjStringVec");
emscripten::register_vector<std::vector<std::string>>("mjStringVecVec");
emscripten::register_vector<int>("mjIntVec"); emscripten::register_vector<int>("mjIntVec");
emscripten::register_vector<mjIntVec>("mjIntVecVec"); emscripten::register_vector<mjIntVec>("mjIntVecVec");
emscripten::register_vector<float>("mjFloatVec"); emscripten::register_vector<float>("mjFloatVec");
@@ -919,7 +956,6 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
emscripten::constant("mjPI", mjPI); emscripten::constant("mjPI", mjPI);
emscripten::constant("mjVERSION_HEADER", mjVERSION_HEADER); 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_mjDISABLESTRING", &get_mjDISABLESTRING);
emscripten::function("get_mjENABLESTRING", &get_mjENABLESTRING); emscripten::function("get_mjENABLESTRING", &get_mjENABLESTRING);
emscripten::function("get_mjFRAMESTRING", &get_mjFRAMESTRING); emscripten::function("get_mjFRAMESTRING", &get_mjFRAMESTRING);
@@ -927,6 +963,31 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
emscripten::function("get_mjRNDSTRING", &get_mjRNDSTRING); emscripten::function("get_mjRNDSTRING", &get_mjRNDSTRING);
emscripten::function("get_mjTIMERSTRING", &get_mjTIMERSTRING); emscripten::function("get_mjTIMERSTRING", &get_mjTIMERSTRING);
emscripten::function("get_mjVISSTRING", &get_mjVISSTRING); 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 } // namespace mujoco::wasm
+1 -10
View File
@@ -205,10 +205,7 @@ class App {
} }
loadModel(xmlContent: string) { loadModel(xmlContent: string) {
// Write xml as a file so that mujoco can find it this.mjModel = mujoco.MjModel.from_xml_string(xmlContent);
(mujoco as any).FS.writeFile('/working/model.xml', xmlContent);
this.mjModel = mujoco.MjModel.mj_loadXML('/working/model.xml');
if (!app.mjModel) { if (!app.mjModel) {
throw new Error('Failed to load model'); 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 // Tip: put "window.dispatchEvent(new Event('unload'))" in the console to test
window.addEventListener('unload', () => { window.addEventListener('unload', () => {
app.dispose(); app.dispose();
(mujoco as any).FS.unmount('/working');
}); });
window.addEventListener('keydown', (event) => { window.addEventListener('keydown', (event) => {
@@ -464,10 +459,6 @@ async function main() {
try { try {
mujoco = await loadMujoco(); 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(); app = new App();
setupWindowEvents(); setupWindowEvents();
+2 -2
View File
@@ -1,10 +1,10 @@
{ {
"name": "mujoco", "name": "@mujoco/mujoco",
"version": "0.0.0", "version": "0.0.0",
"description": "MuJoCo WASM bindings", "description": "MuJoCo WASM bindings",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/google-deepmind/mujoco.git", "url": "git+https://github.com/google-deepmind/mujoco.git",
"directory": "wasm" "directory": "wasm"
}, },
"homepage": "https://github.com/google-deepmind/mujoco/tree/main/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', () => { it('should check constants values', () => {
expect(mujoco.mjNEQDATA).toBe(11); expect(mujoco.mjNEQDATA).toBe(11);
expect(mujoco.get_mjDISABLESTRING()).toEqual([ expect(mujoco.mjDISABLESTRING).toEqual([
'Constraint', 'Equality', 'Frictionloss', 'Limit', 'Contact', 'Spring', 'Constraint', 'Equality', 'Frictionloss', 'Limit', 'Contact', 'Spring',
'Damper', 'Gravity', 'Clampctrl', 'Warmstart', 'Filterparent', 'Damper', 'Gravity', 'Clampctrl', 'Warmstart', 'Filterparent',
'Actuation', 'Refsafe', 'Sensor', 'Midphase', 'Eulerdamp', 'AutoReset', 'Actuation', 'Refsafe', 'Sensor', 'Midphase', 'Eulerdamp', 'AutoReset',
'NativeCCD', 'Island' 'NativeCCD', 'Island'
]); ]);
expect(mujoco.get_mjRNDSTRING()).toEqual([ expect(mujoco.mjRNDSTRING).toEqual([
['Shadow', '1', 'S'], ['Wireframe', '0', 'W'], ['Reflection', '1', 'R'], ['Shadow', '1', 'S'], ['Wireframe', '0', 'W'], ['Reflection', '1', 'R'],
['Additive', '0', 'L'], ['Skybox', '1', 'K'], ['Fog', '0', 'G'], ['Additive', '0', 'L'], ['Skybox', '1', 'K'], ['Fog', '0', 'G'],
['Haze', '1', '/'], ['Depth', '0', ''], ['Segment', '0', ','], ['Haze', '1', '/'], ['Depth', '0', ''], ['Segment', '0', ','],
['Id Color', '0', ''], ['Cull Face', '1', ''] ['Id Color', '0', ''], ['Cull Face', '1', '']
]); ]);
expect(mujoco.get_mjFRAMESTRING().length) expect(mujoco.mjFRAMESTRING.length).toEqual(mujoco.mjtFrame.mjNFRAME.value);
.toEqual(mujoco.mjtFrame.mjNFRAME.value); expect(mujoco.mjVISSTRING.length)
expect(mujoco.get_mjVISSTRING().length)
.toEqual(mujoco.mjtVisFlag.mjNVISFLAG.value); .toEqual(mujoco.mjtVisFlag.mjNVISFLAG.value);
expect(mujoco.get_mjVISSTRING()[mujoco.mjtVisFlag.mjVIS_INERTIA.value]) expect(mujoco.mjVISSTRING[mujoco.mjtVisFlag.mjVIS_INERTIA.value]).toEqual([
.toEqual(['Inertia', '0', 'I']); 'Inertia', '0', 'I'
]);
}); });
it('should create a spec from XML', () => { 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() { async function main() {
const mujoco: MainModule = await loadMujoco(); const mujoco: MainModule = await loadMujoco();
(mujoco as any).FS.mkdir('/working');
(mujoco as any).FS.mount((mujoco as any).MEMFS, {root: '.'}, '/working');
const xmlContent = ` const xmlContent = `
<mujoco model="Box falling"> <mujoco model="Box falling">
<option viscosity="1"/> <option viscosity="1"/>
@@ -37,13 +34,12 @@ async function main() {
</worldbody> </worldbody>
</mujoco>`; </mujoco>`;
(mujoco as any).FS.writeFile('/working/hello.xml', xmlContent);
let model: MjModel|undefined; let model: MjModel|undefined;
let data: MjData|undefined; let data: MjData|undefined;
try { try {
console.log('Hello world!: Loading model'); console.log('Hello world!: Loading model');
model = mujoco.MjModel.mj_loadXML('/working/hello.xml'); model = mujoco.MjModel.from_xml_string(xmlContent);
if (!model) { if (!model) {
throw new Error('Failed to load model'); throw new Error('Failed to load model');
} }
@@ -57,7 +53,6 @@ async function main() {
} finally { } finally {
model?.delete(); model?.delete();
data?.delete(); data?.delete();
(mujoco as any).FS.unmount('/working');
} }
} }