Merge main branch from mujoco

This commit is contained in:
Vidur
2025-06-03 13:00:27 -05:00
289 changed files with 29274 additions and 11525 deletions
+2 -1
View File
@@ -29,7 +29,7 @@ set(MUJOCO_DEP_VERSION_lodepng
project(
mujoco_simulate
VERSION 3.3.1
VERSION 3.3.3
DESCRIPTION "MuJoCo simulate binaries"
HOMEPAGE_URL "https://mujoco.org"
)
@@ -114,6 +114,7 @@ target_sources(
target_compile_options(platform_ui_adapter PRIVATE ${MUJOCO_SIMULATE_COMPILE_OPTIONS})
if(APPLE)
target_sources(platform_ui_adapter PUBLIC glfw_corevideo.h PRIVATE glfw_corevideo.mm)
set_source_files_properties(glfw_corevideo.mm PROPERTIES COMPILE_FLAGS -Wno-deprecated-declarations)
target_link_libraries(platform_ui_adapter PUBLIC "-framework CoreVideo")
endif()
target_include_directories(
+1 -1
View File
@@ -39,7 +39,7 @@ extern "C" {
#if defined(__APPLE__)
#include <mach-o/dyld.h>
#endif
#include <sys/errno.h>
#include <errno.h>
#include <unistd.h>
#endif
}
+198 -119
View File
@@ -298,7 +298,7 @@ void UpdateProfiler(mj::Simulate* sim, const mjModel* m, const mjData* d) {
memset(sim->figcost.linepnt, 0, mjMAXLINE*sizeof(int));
// number of islands that have diagnostics
int nisland = mjMIN(d->solver_nisland, mjNISLAND);
int nisland = mjMAX(1, mjMIN(d->nisland, mjNISLAND));
// iterate over islands
for (int k=0; k < nisland; k++) {
@@ -328,7 +328,7 @@ void UpdateProfiler(mj::Simulate* sim, const mjModel* m, const mjData* d) {
sim->figconstraint.linedata[start + 4][2*i] = i;
// y
int nefc = nisland == 1 ? d->nefc : d->island_efcnum[k];
int nefc = nisland == 1 ? d->nefc : d->island_nefc[k];
sim->figconstraint.linedata[start + 0][2*i+1] = nefc;
const mjSolverStat* stat = d->solver + k*mjNSOLVER + i;
sim->figconstraint.linedata[start + 1][2*i+1] = stat->nactive;
@@ -413,7 +413,7 @@ void UpdateProfiler(mj::Simulate* sim, const mjModel* m, const mjData* d) {
static_cast<float>(d->nefc),
static_cast<float>(sqrt_nnz),
static_cast<float>(d->ncon),
static_cast<float>(solver_niter)
static_cast<float>(solver_niter) / nisland
};
// update figsize
@@ -464,7 +464,7 @@ void InitializeSensor(mj::Simulate* sim) {
// title
mju::strcpy_arr(figsensor.title, "Sensor data");
// y-tick nubmer format
// y-tick number format
mju::strcpy_arr(figsensor.yformat, "%.1f");
// grid size
@@ -546,6 +546,16 @@ void ShowFigure(mj::Simulate* sim, mjrRect viewport, mjvFigure* fig){
mjr_figure(viewport, fig, &sim->platform_ui->mjr_context());
}
void ShowOverlayText(mj::Simulate* sim, mjrRect viewport, int font, int gridpos,
std::string text1, std::string text2) {
mjr_overlay(font, gridpos, viewport, text1.c_str(), text2.c_str(),
&sim->platform_ui->mjr_context());
}
void ShowImage(mj::Simulate* sim, mjrRect viewport, const unsigned char* image) {
mjr_drawPixels(image, nullptr, viewport, &sim->platform_ui->mjr_context());
}
// load state from history buffer
static void LoadScrubState(mj::Simulate* sim) {
// get index into circular buffer
@@ -572,7 +582,7 @@ void UpdateInfoText(mj::Simulate* sim, const mjModel* m, const mjData* d,
char tmp[20];
// number of islands with statistics
int nisland = mjMIN(d->solver_nisland, mjNISLAND);
int nisland = mjMAX(1, mjMIN(d->nisland, mjNISLAND));
// compute solver error (maximum over islands)
mjtNum solerr = 0;
@@ -679,7 +689,7 @@ void UpdateWatch(mj::Simulate* sim, const mjModel* m, const mjData* d) {
// make physics section of UI
void MakePhysicsSection(mj::Simulate* sim) {
mjOption* opt = sim->is_passive_ ? &sim->scnstate_.model.opt : &sim->m_->opt;
mjOption* opt = sim->is_passive_ ? &sim->m_passive_->opt : &sim->m_->opt;
mjuiDef defPhysics[] = {
{mjITEM_SECTION, "Physics", mjPRESERVE, nullptr, "AP"},
{mjITEM_SELECT, "Integrator", 2, &(opt->integrator), "Euler\nRK4\nimplicit\nimplicitfast"},
@@ -863,13 +873,13 @@ void MakeRenderingSection(mj::Simulate* sim, const mjModel* m) {
// make visualization section of UI
void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m) {
mjStatistic* stat = sim->is_passive_ ? &sim->scnstate_.model.stat : &sim->m_->stat;
mjVisual* vis = sim->is_passive_ ? &sim->scnstate_.model.vis : &sim->m_->vis;
mjStatistic* stat = sim->is_passive_ ? &sim->m_passive_->stat : &sim->m_->stat;
mjVisual* vis = sim->is_passive_ ? &sim->m_passive_->vis : &sim->m_->vis;
mjuiDef defVisualization[] = {
{mjITEM_SECTION, "Visualization", mjPRESERVE, nullptr, "AV"},
{mjITEM_SEPARATOR, "Headlight", 1},
{mjITEM_RADIO, "Active", 5, &(vis->headlight.active), "Off\nOn"},
{mjITEM_RADIO, "Active", 2, &(vis->headlight.active), "Off\nOn"},
{mjITEM_EDITFLOAT, "Ambient", 2, &(vis->headlight.ambient), "3"},
{mjITEM_EDITFLOAT, "Diffuse", 2, &(vis->headlight.diffuse), "3"},
{mjITEM_EDITFLOAT, "Specular", 2, &(vis->headlight.specular), "3"},
@@ -882,7 +892,7 @@ void MakeVisualizationSection(mj::Simulate* sim, const mjModel* m) {
{mjITEM_BUTTON, "Align", 2, nullptr, "CA"},
{mjITEM_SEPARATOR, "Global", 1},
{mjITEM_EDITNUM, "Extent", 2, &(stat->extent), "1"},
{mjITEM_RADIO, "Inertia", 5, &(vis->global.ellipsoidinertia), "Box\nEllipsoid"},
{mjITEM_RADIO, "Inertia", 2, &(vis->global.ellipsoidinertia), "Box\nEllipsoid"},
{mjITEM_RADIO, "BVH active", 5, &(vis->global.bvactive), "False\nTrue"},
{mjITEM_SEPARATOR, "Map", 1},
{mjITEM_EDITFLOAT, "Stiffness", 2, &(vis->map.stiffness), "1"},
@@ -1141,17 +1151,74 @@ void AlignAndScaleView(mj::Simulate* sim, const mjModel* m) {
}
// copy qpos to clipboard as key
void CopyPose(mj::Simulate* sim, const mjModel* m, const mjData* d) {
char clipboard[5000] = "<key qpos='";
// copy state to clipboard as key
void CopyKey(mj::Simulate* sim, const mjModel* m, const mjData* d, bool fp) {
char clipboard[5000] = "<key\n";
char buf[200];
const char p_regular[] = "%g";
const char p_full[] = "%-22.16g";
const char* format = fp ? p_full : p_regular;
// prepare string
for (int i=0; i<m->nq; i++) {
mju::sprintf_arr(buf, i==m->nq-1 ? "%g" : "%g ", d->qpos[i]);
// time
mju::strcat_arr(clipboard, " time=\"");
mju::sprintf_arr(buf, format, d->time);
mju::strcat_arr(clipboard, buf);
// qpos
mju::strcat_arr(clipboard, "\"\n qpos=\"");
for (int i = 0; i < m->nq; i++) {
mju::sprintf_arr(buf, format, d->qpos[i]);
if (i < m->nq-1) mju::strcat_arr(buf, " ");
mju::strcat_arr(clipboard, buf);
}
mju::strcat_arr(clipboard, "'/>");
// qvel
mju::strcat_arr(clipboard, "\"\n qvel=\"");
for (int i = 0; i < m->nv; i++) {
mju::sprintf_arr(buf, format, d->qvel[i]);
if (i < m->nv-1) mju::strcat_arr(buf, " ");
mju::strcat_arr(clipboard, buf);
}
// act
if (m->na > 0) {
mju::strcat_arr(clipboard, "\"\n act=\"");
for (int i = 0; i < m->na; i++) {
mju::sprintf_arr(buf, format, d->act[i]);
if (i < m->na-1) mju::strcat_arr(buf, " ");
mju::strcat_arr(clipboard, buf);
}
}
// ctrl
if (m->nu > 0) {
mju::strcat_arr(clipboard, "\"\n ctrl=\"");
for (int i = 0; i < m->nu; i++) {
mju::sprintf_arr(buf, format, d->ctrl[i]);
if (i < m->nu-1) mju::strcat_arr(buf, " ");
mju::strcat_arr(clipboard, buf);
}
}
if (m->nmocap > 0) {
// mocap_pos
mju::strcat_arr(clipboard, "\"\n mpos=\"");
for (int i = 0; i < 3*m->nmocap; i++) {
mju::sprintf_arr(buf, format, d->mocap_pos[i]);
if (i < 3*m->nmocap-1) mju::strcat_arr(buf, " ");
mju::strcat_arr(clipboard, buf);
}
// mocap_quat
mju::strcat_arr(clipboard, "\"\n mquat=\"");
for (int i = 0; i < 4*m->nmocap; i++) {
mju::sprintf_arr(buf, format, d->mocap_quat[i]);
if (i < 4*m->nmocap-1) mju::strcat_arr(buf, " ");
mju::strcat_arr(clipboard, buf);
}
}
mju::strcat_arr(clipboard, "\"\n/>");
// copy to clipboard
sim->platform_ui->SetClipboardString(clipboard);
@@ -1412,8 +1479,9 @@ void UiEvent(mjuiState* state) {
sim->pending_.align = true;
break;
case 4: // Copy pose
sim->pending_.copy_pose = true;
case 4: // Copy key
sim->pending_.copy_key = true;
sim->pending_.copy_key_full_precision = sim->platform_ui->IsShiftKeyPressed();
break;
case 5: // Adjust key
@@ -1435,7 +1503,7 @@ void UiEvent(mjuiState* state) {
// physics section
else if (it && it->sectionid==SECT_PHYSICS && sim->m_) {
mjOption* opt = sim->is_passive_ ? &sim->scnstate_.model.opt : &sim->m_->opt;
mjOption* opt = sim->is_passive_ ? &sim->m_passive_->opt : &sim->m_->opt;
// update disable flags in mjOption
opt->disableflags = 0;
@@ -1697,15 +1765,14 @@ void UiEvent(mjuiState* state) {
return;
}
// local pointers used below
mjModel* model = sim->is_passive_ ? sim->m_passive_ : sim->m_;
mjData* data = sim->is_passive_ ? sim->d_passive_ : sim->d_;
// 3D scroll
if (state->type==mjEVENT_SCROLL && state->mouserect==3) {
if (state->type==mjEVENT_SCROLL && state->mouserect==3 && model) {
// emulate vertical mouse motion = 2% of window height
if (sim->m_ && !sim->is_passive_) {
mjv_moveCamera(sim->m_, mjMOUSE_ZOOM, 0, -zoom_increment*state->sy, &sim->scn, &sim->cam);
} else {
mjv_moveCameraFromState(
&sim->scnstate_, mjMOUSE_ZOOM, 0, -zoom_increment*state->sy, &sim->scn, &sim->cam);
}
mjv_moveCamera(model, mjMOUSE_ZOOM, 0, -zoom_increment*state->sy, &sim->scn, &sim->cam);
return;
}
@@ -1761,25 +1828,11 @@ void UiEvent(mjuiState* state) {
// move perturb or camera
mjrRect r = state->rect[3];
if (sim->pert.active) {
if (!sim->is_passive_) {
mjv_movePerturb(
sim->m_, sim->d_, action, state->dx / r.height, -state->dy / r.height,
&sim->scn, &sim->pert);
} else {
mjv_movePerturbFromState(
&sim->scnstate_, action, state->dx / r.height, -state->dy / r.height,
&sim->scn, &sim->pert);
}
mjv_movePerturb(model, data, action, state->dx / r.height, -state->dy / r.height,
&sim->scn, &sim->pert);
} else {
if (!sim->is_passive_) {
mjv_moveCamera(
sim->m_, action, state->dx / r.height, -state->dy / r.height,
&sim->scn, &sim->cam);
} else {
mjv_moveCameraFromState(
&sim->scnstate_, action, state->dx / r.height, -state->dy / r.height,
&sim->scn, &sim->cam);
}
mjv_moveCamera(model, action, state->dx / r.height, -state->dy / r.height,
&sim->scn, &sim->cam);
}
return;
}
@@ -1813,10 +1866,11 @@ Simulate::Simulate(std::unique_ptr<PlatformUIAdapter> platform_ui,
platform_ui(std::move(platform_ui)),
uistate(this->platform_ui->state()) {
mjv_defaultScene(&scn);
mjv_defaultSceneState(&scnstate_);
}
// synchronize model and data
//------------------------- Synchronize render and physics threads ---------------------------------
// operations which require holding the mutex, prevents racing with physics thread
void Simulate::Sync() {
MutexLock lock(this->mtx);
@@ -1877,48 +1931,30 @@ void Simulate::Sync() {
}
}
// in passive mode, synchronize user's mjModel with changes made via the UI
if (is_passive_) {
// synchronize m_->opt with changes made via the UI
#define X(name) \
if (IsDifferent(scnstate_.model.opt.name, mjopt_prev_.name)) { \
pending_.ui_update_physics = true; \
Copy(m_->opt.name, scnstate_.model.opt.name); \
}
// synchronize mjModel.opt
if (std::memcmp(&m_passive_->opt, &mjopt_prev_, sizeof(mjOption))) {
pending_.ui_update_physics = true;
m_->opt = m_passive_->opt;
}
X(timestep);
X(apirate);
X(impratio);
X(tolerance);
X(noslip_tolerance);
X(ccd_tolerance);
X(gravity);
X(wind);
X(magnetic);
X(density);
X(viscosity);
X(o_margin);
X(o_solref);
X(o_solimp);
X(o_friction);
X(integrator);
X(cone);
X(jacobian);
X(solver);
X(iterations);
X(noslip_iterations);
X(ccd_iterations);
X(disableflags);
X(enableflags);
X(disableactuator);
X(sdf_initpoints);
X(sdf_iterations);
// synchronize mjModel.vis
if (std::memcmp(&m_passive_->vis, &mjvis_prev_, sizeof(mjVisual))) {
pending_.ui_update_visualization = true;
m_->vis = m_passive_->vis;
}
#undef X
// synchronize mjModel.stat
if (std::memcmp(&m_passive_->stat, &mjstat_prev_, sizeof(mjStatistic))) {
pending_.ui_update_visualization = true;
m_->stat = m_passive_->stat;
}
// synchronize number of mjWARN_VGEOMFULL warnings
if (scnstate_.data.warning[mjWARN_VGEOMFULL].number > warn_vgeomfull_prev_) {
if (d_passive_->warning[mjWARN_VGEOMFULL].number > warn_vgeomfull_prev_) {
d_->warning[mjWARN_VGEOMFULL].number +=
scnstate_.data.warning[mjWARN_VGEOMFULL].number - warn_vgeomfull_prev_;
d_passive_->warning[mjWARN_VGEOMFULL].number - warn_vgeomfull_prev_;
}
}
@@ -1967,9 +2003,10 @@ void Simulate::Sync() {
pending_.align = false;
}
if (pending_.copy_pose) {
CopyPose(this, m_, d_);
pending_.copy_pose = false;
if (pending_.copy_key) {
CopyKey(this, m_, d_, pending_.copy_key_full_precision);
pending_.copy_key = false;
pending_.copy_key_full_precision = false;
}
if (pending_.load_from_history) {
@@ -2066,25 +2103,19 @@ void Simulate::Sync() {
pending_.select = false;
}
// update scene
// update scene or sync data from user in passive mode
if (!is_passive_) {
mjv_updateScene(m_, d_, &this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn);
} else {
mjv_updateSceneState(m_, d_, &this->opt, &scnstate_);
mjv_copyModel(m_passive_, m_);
mjv_copyData(d_passive_, m_passive_, d_);
// append geoms from user_scn to scnstate_ scratch space
// append geoms from user_scn to scratch space
if (user_scn) {
int ngeom = user_scn->ngeom;
int maxgeom = scnstate_.scratch.maxgeom - scnstate_.scratch.ngeom;
if (ngeom > maxgeom) {
mj_warning(d_, mjWARN_VGEOMFULL, scnstate_.scratch.maxgeom);
ngeom = maxgeom;
}
if (ngeom > 0) {
std::memcpy(scnstate_.scratch.geoms + scnstate_.scratch.ngeom,
user_scn->geoms,
sizeof(mjvGeom) * ngeom);
scnstate_.scratch.ngeom += ngeom;
user_scn_geoms_.clear();
user_scn_geoms_.reserve(user_scn->ngeom);
for (int i = 0; i < user_scn->ngeom; ++i) {
user_scn_geoms_.push_back(user_scn->geoms[i]);
}
}
@@ -2100,8 +2131,10 @@ void Simulate::Sync() {
Copy(user_scn_flags_prev_, user_scn->flags);
}
mjopt_prev_ = scnstate_.model.opt;
warn_vgeomfull_prev_ = scnstate_.data.warning[mjWARN_VGEOMFULL].number;
mjopt_prev_ = m_passive_->opt;
mjvis_prev_ = m_passive_->vis;
mjstat_prev_ = m_passive_->stat;
warn_vgeomfull_prev_ = d_passive_->warning[mjWARN_VGEOMFULL].number;
}
// update settings
@@ -2260,15 +2293,8 @@ void Simulate::LoadOnRenderThread() {
}
}
// re-create scene and context
// re-create scene
mjv_makeScene(this->m_, &this->scn, kMaxGeom);
if (this->is_passive_) {
mjopt_prev_ = m_->opt;
opt_prev_ = opt;
cam_prev_ = cam;
warn_vgeomfull_prev_ = d_->warning[mjWARN_VGEOMFULL].number;
mjv_makeSceneState(this->m_, this->d_, &this->scnstate_, kMaxGeom);
}
this->platform_ui->RefreshMjrContext(this->m_, 50*(this->font+1));
UiModify(&this->ui0, &this->uistate, &this->platform_ui->mjr_context());
@@ -2297,12 +2323,18 @@ void Simulate::LoadOnRenderThread() {
mju::strcpy_arr(this->previous_filename, this->filename);
}
// update scene
// update scene in managed mode, in passive mode copy data from user (update in RenderLoop)
if (!is_passive_) {
mjv_updateScene(this->m_, this->d_,
&this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn);
mjv_updateScene(this->m_, this->d_, &this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn);
} else {
mjv_updateSceneState(this->m_, this->d_, &this->opt, &this->scnstate_);
mjopt_prev_ = m_->opt;
opt_prev_ = opt;
cam_prev_ = cam;
warn_vgeomfull_prev_ = d_->warning[mjWARN_VGEOMFULL].number;
// full copy on init
m_passive_ = mj_copyModel(nullptr, m_);
d_passive_ = mj_copyData(nullptr, m_passive_, d_);
}
// set window title to model name
@@ -2423,6 +2455,13 @@ void Simulate::Render() {
pending_.ui_update_physics = false;
}
if (pending_.ui_update_visualization) {
if (this->ui0_enable && this->ui0.sect[SECT_VISUALIZATION].state) {
mjui0_update_section(this, SECT_VISUALIZATION);
}
pending_.ui_update_visualization = false;
}
if (is_passive_) {
if (this->ui0_enable && this->ui0.sect[SECT_RENDERING].state &&
(cam_prev_.type != cam.type ||
@@ -2593,10 +2632,38 @@ void Simulate::Render() {
}
// user figures
if (this->newfigurerequest.load() == 1) {
this->user_figures_.clear();
std::swap(this->user_figures_, this->user_figures_new_);
int value = 1;
this->newfigurerequest.compare_exchange_strong(value, 0);
}
for (auto& [viewport, figure] : this->user_figures_) {
ShowFigure(this, viewport, &figure);
}
// overlay text
if (this->newtextrequest.load() == 1) {
this->user_texts_.clear();
std::swap(this->user_texts_, this->user_texts_new_);
int value = 1;
this->newtextrequest.compare_exchange_strong(value, 0);
}
for (auto& [font, gridpos, text1, text2] : this->user_texts_) {
ShowOverlayText(this, rect, font, gridpos, text1, text2);
}
// user images
if (this->newimagerequest.load() == 1) {
this->user_images_.clear();
std::swap(this->user_images_, this->user_images_new_);
int value = 1;
this->newimagerequest.compare_exchange_strong(value, 0);
}
for (auto& [viewport, image] : this->user_images_) {
ShowImage(this, viewport, image.get());
}
// finalize
this->platform_ui->SwapBuffers();
}
@@ -2714,11 +2781,22 @@ void Simulate::RenderLoop() {
}
// update scene, doing a full sync if in fully managed mode
if (!this->is_passive_) {
if (!is_passive_) {
Sync();
} else {
scnstate_.data.warning[mjWARN_VGEOMFULL].number += mjv_updateSceneFromState(
&scnstate_, &this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn);
} else if (m_passive_ && d_passive_) {
// the user has called Sync() in their code
mjv_updateScene(m_passive_, d_passive_,
&this->opt, &this->pert, &this->cam, mjCAT_ALL, &this->scn);
// add user geoms to scene
int nusergeom = user_scn_geoms_.size();
int ngeom = std::min(nusergeom, this->scn.maxgeom - this->scn.ngeom);
if (ngeom < nusergeom) {
mj_warning(d_passive_, mjWARN_VGEOMFULL, this->scn.maxgeom);
}
std::memcpy(this->scn.geoms + this->scn.ngeom, user_scn_geoms_.data(),
ngeom * sizeof(mjvGeom));
this->scn.ngeom += ngeom;
}
} // MutexLock (unblocks simulation thread)
@@ -2739,7 +2817,8 @@ void Simulate::RenderLoop() {
const MutexLock lock(this->mtx);
mjv_freeScene(&this->scn);
if (is_passive_) {
mjv_freeSceneState(&scnstate_);
mj_deleteData(d_passive_);
mj_deleteModel(m_passive_);
}
this->exitrequest.store(2);
+22 -4
View File
@@ -23,6 +23,7 @@
#include <optional>
#include <ratio>
#include <string>
#include <tuple>
#include <utility>
#include <vector>
@@ -129,8 +130,15 @@ class Simulate {
std::vector<mjtNum> ctrl_;
std::vector<mjtNum> ctrl_prev_;
mjvSceneState scnstate_;
// in passive mode the user owns m_ and d_, these "passive" instances are
// owned by Simulate, updated from the user by the Sync() method
mjModel* m_passive_ = nullptr;
mjData* d_passive_ = nullptr;
std::vector<mjvGeom> user_scn_geoms_;
mjOption mjopt_prev_;
mjVisual mjvis_prev_;
mjStatistic mjstat_prev_;
mjvOption opt_prev_;
mjvCamera cam_prev_;
@@ -144,7 +152,8 @@ class Simulate {
std::optional<std::string> print_data;
bool reset;
bool align;
bool copy_pose;
bool copy_key;
bool copy_key_full_precision;
bool load_from_history;
bool load_key;
bool save_key;
@@ -155,6 +164,7 @@ class Simulate {
bool ui_update_simulation;
bool ui_update_physics;
bool ui_update_rendering;
bool ui_update_visualization;
bool ui_update_joint;
bool ui_update_ctrl;
bool ui_remake_ctrl;
@@ -196,6 +206,9 @@ class Simulate {
std::atomic_int droploadrequest = 0;
std::atomic_int screenshotrequest = 0;
std::atomic_int uiloadrequest = 0;
std::atomic_int newfigurerequest = 0;
std::atomic_int newtextrequest = 0;
std::atomic_int newimagerequest = 0;
// loadrequest
// 3: display a loading message
@@ -249,10 +262,15 @@ class Simulate {
mjvFigure figsize = {};
mjvFigure figsensor = {};
// additional user-defined visualization geoms (used in passive mode)
// additional user-defined visualization
mjvScene* user_scn = nullptr;
mjtByte user_scn_flags_prev_[mjNRNDFLAG];
std::vector<std::pair<mjrRect, mjvFigure>> user_figures_;
std::vector<std::pair<mjrRect, mjvFigure>> user_figures_new_;
std::vector<std::tuple<int, int, std::string, std::string>> user_texts_;
std::vector<std::tuple<int, int, std::string, std::string>> user_texts_new_;
std::vector<std::tuple<mjrRect, std::unique_ptr<unsigned char[]>>> user_images_;
std::vector<std::tuple<mjrRect, std::unique_ptr<unsigned char[]>>> user_images_new_;
// OpenGL rendering and UI
int refresh_rate = 60;
@@ -293,7 +311,7 @@ class Simulate {
{mjITEM_BUTTON, "Reset", 2, nullptr, " #259"},
{mjITEM_BUTTON, "Reload", 5, nullptr, "CL"},
{mjITEM_BUTTON, "Align", 2, nullptr, "CA"},
{mjITEM_BUTTON, "Copy pose", 2, nullptr, "CC"},
{mjITEM_BUTTON, "Copy state", 2, nullptr, "CC"},
{mjITEM_SLIDERINT, "Key", 3, &this->key, "0 0"},
{mjITEM_BUTTON, "Load key", 3},
{mjITEM_BUTTON, "Save key", 3},