Move UX drawing functions into a shared library.

PiperOrigin-RevId: 837528296
Change-Id: I153a000cde424254384cf168606f32f7925ca076
This commit is contained in:
Haroon Qureshi
2025-11-27 07:33:57 -08:00
committed by Copybara-Service
parent 23825a5bcf
commit 5163dfa823
6 changed files with 949 additions and 856 deletions
File diff suppressed because it is too large Load Diff
+4 -18
View File
@@ -93,7 +93,7 @@ class App {
bool chart_cpu_time = false;
bool chart_dimensions = false;
bool chart_solver = false;
bool settings_panel = true;
bool options_panel = true;
bool inspector_panel = true;
bool style_editor = false;
bool imgui_demo = false;
@@ -149,28 +149,14 @@ class App {
void SetupStyle(Style style);
ImVec4 ConfigureDockingLayout();
void MainMenuGui();
void ToolBarGui();
void SettingsGui();
void InspectorGui();
void StatusBarGui();
void InfoGui();
void HelpGui();
void FileDialogGui();
void PlaybackGui();
void PhysicsGui();
void NoiseGui();
void RenderingGui();
void VisualizationGui();
void GroupsGui();
void WatchGui();
void SensorGui();
void StateGui();
void JointsGui();
void ControlsGui();
void ConvergenceGui();
void CountsGui();
void ModelOptionsGui();
void DataInspectorGui();
float GetExpectedLabelWidth();
std::vector<const char*> GetCameraNames();
+2
View File
@@ -20,6 +20,8 @@ add_library(${MUJOCO_TOOLBOX_TARGET_NAME} STATIC)
target_sources(${MUJOCO_TOOLBOX_TARGET_NAME}
PUBLIC
gui.cc
gui.h
helpers.cc
helpers.h
imgui_widgets.cc
+807
View File
@@ -0,0 +1,807 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "experimental/toolbox/gui.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <limits>
#include <string>
#include <vector>
#include <imgui.h>
#include <implot.h>
#include <mujoco/mujoco.h>
#include "experimental/toolbox/helpers.h"
#include "experimental/toolbox/imgui_widgets.h"
namespace mujoco::toolbox {
static ImVec2 GetFlexElementSize(int num_cols) {
const float width = (ImGui::GetContentRegionAvail().x / num_cols) -
ImGui::GetStyle().FramePadding.x * 2;
return ImVec2(width, 0);
}
void SensorGui(const mjModel* model, const mjData* data) {
ImPlot::PushStyleVar(ImPlotStyleVar_FitPadding, ImVec2(0.1f, 0.1f));
if (ImPlot::BeginPlot("Sensors", ImVec2(-1, 0),
ImPlotFlags_NoLegend | ImPlotFlags_NoMouseText)) {
ImPlot::SetupLegend(ImPlotLocation_NorthEast, ImPlotLegendFlags_None);
ImPlot::SetupAxis(ImAxis_X1, "sensor",
ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_NoLabel);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, 5, ImPlotCond_Once);
ImPlot::SetupAxis(ImAxis_Y1, "value",
ImPlotAxisFlags_AutoFit | ImPlotAxisFlags_NoLabel);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.1f");
ImPlot::SetupAxisLimits(ImAxis_Y1, -100, 100, ImPlotCond_Once);
ImPlot::SetupFinish();
// The values to be plotted.
std::vector<ImPlotPoint> sensor_values;
// The x-value of the bar to be plotted. Multiple bars will belong to the
// same sensor (i.e. the sensor_dim), but each group of bars will be appear
// in sequence along the x-axis.
float x_value = 0.f;
// The index of the sensor being plotted, based on sensor_type.
int sensor_index = 0;
// Function that plots the current group of sensor bars.
auto plot_lines = [](int sensor_idx, const ImPlotPoint* values, int count) {
constexpr float bar_weight = 5.0f;
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, bar_weight);
std::string sensor_label = "Sensor " + std::to_string(sensor_idx);
ImPlot::PlotLine(sensor_label.c_str(), &values->x, &values->y, count,
ImPlotLineFlags_Segments, 0, 2 * sizeof(double));
};
for (int n = 0; n < model->nsensor; n++) {
if (n > 0 && model->sensor_type[n] != model->sensor_type[n - 1]) {
plot_lines(sensor_index, sensor_values.data(), sensor_values.size());
sensor_values.clear();
++sensor_index;
}
const int adr = model->sensor_adr[n];
const int dim = model->sensor_dim[n];
const mjtNum cutoff =
(model->sensor_cutoff[n] > 0 ? model->sensor_cutoff[n] : 1);
for (int i = 0; i < dim; ++i) {
sensor_values.push_back({x_value, 0});
sensor_values.push_back({x_value, data->sensordata[adr + i] / cutoff});
x_value += 1.f;
}
}
// Ensure the last group of sensors is plotted.
plot_lines(sensor_index, sensor_values.data(), sensor_values.size());
ImPlot::EndPlot();
}
ImPlot::PopStyleVar();
}
void StateGui(const mjModel* model, mjData* data, std::vector<mjtNum>& state,
int& state_sig, float min_width) {
const float available_width =
ImGui::GetContentRegionAvail().x - ImGui::GetTreeNodeToLabelSpacing();
const int num_cols = std::clamp(
static_cast<int>(std::floor(available_width / min_width)), 1, 4);
const ImVec2 size = GetFlexElementSize(num_cols);
ImGui::Unindent(0.5f * ImGui::GetTreeNodeToLabelSpacing());
// State component names and tooltips.
static constexpr const char* name_and_tooltip[][2] = {
{"TIME", "Time"},
{"QPOS", "Position"},
{"QVEL", "Velocity"},
{"ACT", "Actuator activation"},
{"WARMSTART", "Acceleration used for warmstart"},
{"CTRL", "Control"},
{"QFRC_APPLIED", "Applied generalized force"},
{"XFRC_APPLIED", "Applied Cartesian force/torque"},
{"EQ_ACTIVE", "Enable/disable constraints"},
{"MOCAP_POS", "Positions of mocap bodies"},
{"MOCAP_QUAT", "Orientations of mocap bodies"},
{"USERDATA", "User data"},
{"PLUGIN", "Plugin state"},
};
int prev_state_sig = state_sig;
// State component checkboxes.
if (ImGui::BeginTable("##StateSignature", num_cols)) {
for (int i = 0; i < mjNSTATE; ++i) {
ImGui::TableNextColumn();
bool checked = state_sig & (1 << i);
ImGui::Checkbox(name_and_tooltip[i][0], &checked);
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("%s", name_and_tooltip[i][1]);
}
state_sig = checked ? (state_sig | (1 << i)) : (state_sig & ~(1 << i));
}
ImGui::EndTable();
}
// Buttons to select commonly used state signatures.
if (ImGui::BeginTable("##CommonSignatures", num_cols)) {
ImGui::TableNextColumn();
if (ImGui::Button("Physics", size)) {
state_sig = (state_sig == mjSTATE_PHYSICS) ? 0 : mjSTATE_PHYSICS;
}
ImGui::TableNextColumn();
if (ImGui::Button("Full Physics", size)) {
state_sig = (state_sig == mjSTATE_FULLPHYSICS) ? 0 : mjSTATE_FULLPHYSICS;
}
ImGui::TableNextColumn();
if (ImGui::Button("User", size)) {
state_sig = (state_sig == mjSTATE_USER) ? 0 : mjSTATE_USER;
}
ImGui::TableNextColumn();
if (ImGui::Button("Integration", size)) {
state_sig = (state_sig == mjSTATE_INTEGRATION) ? 0 : mjSTATE_INTEGRATION;
}
ImGui::EndTable();
}
if (state_sig != prev_state_sig) {
const int size = mj_stateSize(model, state_sig);
state.resize(size);
}
if (state.empty()) {
// The state size is 0, let the user know why.
ImGui::Separator();
ImGui::BeginDisabled();
ImGui::TextWrapped(
state_sig == 0
? "No state components are selected."
: "Selected state components do not exist in the model.");
ImGui::EndDisabled();
} else {
mj_getState(model, data, state.data(), state_sig);
bool changed = false;
if (ImGui::BeginTable(
"State", 3,
ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersOuter |
ImGuiTableFlags_BordersV | ImGuiTableFlags_Resizable |
ImGuiTableFlags_ScrollY,
ImVec2(0, ImGui::GetTextLineHeightWithSpacing() * 20))) {
ImGui::TableSetupColumn("Index");
ImGui::TableSetupColumn("Name");
ImGui::TableSetupColumn("Value");
ImGui::TableSetupScrollFreeze(0, 1);
ImGui::TableHeadersRow();
ImGuiListClipper clipper;
clipper.Begin(state.size());
while (clipper.Step()) {
int global = 0;
for (int i = 0; i < mjNSTATE; ++i) {
if (state_sig & (1 << i)) {
for (int local = 0; local < mj_stateSize(model, (1 << i));
++local, ++global) {
if (global < clipper.DisplayStart) {
continue;
}
if (global >= clipper.DisplayEnd) {
break;
}
ImGui::TableNextRow();
ImGui::TableNextColumn();
ImGui::Text("%d", global);
ImGui::TableNextColumn();
ImGui::Text("%s[%d]", name_and_tooltip[i][0], local);
ImGui::TableNextColumn();
float value = state[global];
ImGui::PushItemWidth(-std::numeric_limits<float>::min());
ImGui::PushID(global);
if (ImGui::DragFloat("##value", &value, 0.01f, 0, 0, "%.3f")) {
changed = true;
}
ImGui::PopID();
ImGui::PopItemWidth();
state[global] = value;
}
}
}
}
ImGui::EndTable();
}
if (changed) {
mj_setState(model, data, state.data(), state_sig);
}
}
ImGui::Indent(0.5f * ImGui::GetTreeNodeToLabelSpacing());
}
void WatchGui(const mjModel* model, const mjData* data, char* field_name,
int field_len, int& field_index) {
ImGui::InputText("Field", field_name, field_len);
ImGui::InputInt("Index", &field_index);
const mjtNum* value = static_cast<const mjtNum*>(
GetValue(model, data, field_name, field_index));
ScopedStyle style;
style.Color(ImGuiCol_FrameBg, ImGui::GetStyle().Colors[ImGuiCol_WindowBg]);
if (value) {
char buf[100];
int size = std::snprintf(buf, sizeof(buf), "%0.3f", *value);
ImGui::InputText("Value", buf, size, ImGuiInputTextFlags_ReadOnly);
} else {
ImGui::BeginDisabled();
style.Color(ImGuiCol_Text, ImColor(255, 0, 0, 255));
char buf[] = "Invalid field/index!";
ImGui::InputText("Value", buf, sizeof(buf), ImGuiInputTextFlags_ReadOnly);
ImGui::EndDisabled();
}
}
void PhysicsGui(mjModel* model, float min_width) {
const float available_width =
ImGui::GetContentRegionAvail().x - ImGui::GetTreeNodeToLabelSpacing();
const int num_cols = std::clamp(
static_cast<int>(std::floor(available_width / min_width)), 1, 6);
auto& opt = model->opt;
const char* opts0[] = {"Euler", "RK4", "implicit", "implicitfast"};
ImGui::Combo("Integrator", &opt.integrator, opts0, IM_ARRAYSIZE(opts0));
const char* opts1[] = {"Pyramidal", "Elliptic"};
ImGui::Combo("Cone", &opt.cone, opts1, IM_ARRAYSIZE(opts1));
const char* opts2[] = {"Dense", "Sparse", "Auto"};
ImGui::Combo("Jacobian", &opt.jacobian, opts2, IM_ARRAYSIZE(opts2));
const char* opts3[] = {"PGS", "CG", "Newton"};
ImGui::Combo("Solver", &opt.solver, opts3, IM_ARRAYSIZE(opts3));
if (ImGui::TreeNodeEx("Disable Flags", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::BeginTable("##DisableFlagsTable", num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < mjNDISABLE; ++i) {
ImGui::TableNextColumn();
ImGui_BitToggle(mjDISABLESTRING[i], &opt.disableflags, 1 << i, size);
}
ImGui::EndTable();
}
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Enable Flags", ImGuiTreeNodeFlags_DefaultOpen)) {
if (ImGui::BeginTable("##EnableFlagsTable", num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < mjNENABLE; ++i) {
ImGui::TableNextColumn();
ImGui_BitToggle(mjENABLESTRING[i], &opt.enableflags, 1 << i, size);
}
ImGui::EndTable();
}
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Actuator Group Disable")) {
if (ImGui::BeginTable("##EnableFlagsTable", num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < 6; ++i) {
char label[64];
std::snprintf(label, sizeof(label), "Act Group %d", i);
ImGui::TableNextColumn();
ImGui_BitToggle(label, &opt.disableactuator, 1 << i, size);
}
ImGui::EndTable();
}
ImGui::TreePop();
};
if (ImGui::TreeNodeEx("Algorithmic Parameters")) {
float w = ImGui::GetWindowWidth() * .6f;
ImGui_Input("Timestep", &opt.timestep, {0, 1, 0.01, 0.1, w});
ImGui_Input("Iterations", &opt.iterations, {0, 1000, 1, 10, w});
ImGui_Input("Tolerance", &opt.tolerance, {0, 1, 1e-7, 1e-6, w});
ImGui_Input("LS Iter", &opt.ls_iterations, {0, 100, 1, 0.1, w});
ImGui_Input("LS Tol", &opt.ls_tolerance, {0, 0.1, 0.01, 0.1, w});
ImGui_Input("Noslip Iter", &opt.noslip_iterations, {0, 1000, 1, 100, w});
ImGui_Input("Noslip Tol", &opt.noslip_tolerance, {0, 1, 0.01, 0.1, w});
ImGui_Input("CCD Iter", &opt.ccd_iterations, {0, 1000, 1, 100, w});
ImGui_Input("CCD Tol", &opt.ccd_tolerance, {0, 1, 0.01, 0.1, w});
ImGui_Input("Sleep Tol", &opt.sleep_tolerance, {0, 1, 0.01, 0.1, w});
ImGui_Input("SDF Iter", &opt.sdf_iterations, {1, 20, 1, 10, w});
ImGui_Input("SDF Init", &opt.sdf_initpoints, {1, 100, 1, 10, w});
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Physical Parameters")) {
float w = ImGui::GetWindowWidth() * .6f;
ImGui_InputN("Gravity", opt.gravity, 3, {.width = w});
ImGui_InputN("Wind", opt.wind, 3, {.width = w});
ImGui_InputN("Magnetic", opt.magnetic, 3, {.width = w});
ImGui_Input("Density", &opt.density, {.min = .1, .max = 1, .width = w});
ImGui_Input("Viscosity", &opt.viscosity, {.min = .1, .max = 1, .width = w});
ImGui_Input("Imp Ratio", &opt.impratio, {.min = .1, .max = 1, .width = w});
ImGui::TreePop();
};
if (ImGui::TreeNodeEx("Contact Override")) {
float w = ImGui::GetWindowWidth() * .6f;
ImGui_Input("Margin", &opt.o_margin, {.min = 0.1, .max = 1, .width = w});
ImGui_InputN("Sol Imp", opt.o_solimp, 5, {.width = w, .format = "%0.1f"});
ImGui_InputN("Sol Ref", opt.o_solref, 2, {.width = w, .format = "%0.1f"});
ImGui_InputN("Friction", opt.o_friction, 5, {.width = w, .format = "%.1f"});
ImGui::TreePop();
}
}
void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera,
float min_width) {
auto& vis = model->vis;
auto& stat = model->stat;
ImGui::SliderInt("Tree depth", &vis_options->bvh_depth, 0, 20);
ImGui::SliderInt("Flex layer", &vis_options->flex_layer, 0, 10);
if (ImGui::TreeNodeEx("Headlight")) {
ImGui_SwitchToggle("Active", &vis.headlight.active);
ImGui::ColorEdit3("Ambient", vis.headlight.ambient);
ImGui::ColorEdit3("Diffuse", vis.headlight.diffuse);
ImGui::ColorEdit3("Specular", vis.headlight.specular);
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Free Camera")) {
ImGui_SwitchToggle("Orthographic", &vis.global.orthographic);
ImGui_Input("FOV", &vis.global.fovy, {.format = "%0.2f"});
ImGui_InputN("Center", stat.center, 3, {.format = "%0.2f"});
ImGui_Input("Azimuth", &vis.global.azimuth, {.format = "%0.2f"});
ImGui_Input("Elevation", &vis.global.elevation, {.format = "%0.2f"});
if (ImGui::Button("Align")) {
mjv_defaultFreeCamera(model, camera);
}
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Global")) {
ImGui_Input("Extent", &stat.extent);
const char* opts[] = {"Box", "Ellipsoid"};
ImGui::SliderInt("Inertia", &vis.global.ellipsoidinertia, 0, 1,
opts[vis.global.ellipsoidinertia]);
ImGui_ButtonToggle("BVH active", &vis.global.bvactive);
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Mapping")) {
ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.3f);
ImGui_Input("Stiffness", &vis.map.stiffness);
ImGui_Input("Rot stiffness", &vis.map.stiffnessrot);
ImGui_Input("Force", &vis.map.force);
ImGui_Input("Torque", &vis.map.torque);
ImGui_Input("Alpha", &vis.map.alpha);
ImGui_Input("Fog start", &vis.map.fogstart);
ImGui_Input("Fog end", &vis.map.fogend);
ImGui_Input("Z near", &vis.map.znear);
ImGui_Input("Z far", &vis.map.zfar);
ImGui_Input("Haze", &vis.map.haze);
ImGui_Input("Shadow clip", &vis.map.shadowclip);
ImGui_Input("Shadow scale", &vis.map.shadowscale);
ImGui::PopItemWidth();
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Scale")) {
ImGui::PushItemWidth(ImGui::GetWindowWidth() * 0.3f);
ImGui_Input("All (meansize)", &stat.meansize, {.format = "%0.3f"});
ImGui_Input("Force width", &vis.scale.forcewidth);
ImGui_Input("Contact width", &vis.scale.contactwidth);
ImGui_Input("Contact height", &vis.scale.contactheight);
ImGui_Input("Connect", &vis.scale.connect);
ImGui_Input("Com", &vis.scale.com);
ImGui_Input("Camera", &vis.scale.camera);
ImGui_Input("Light", &vis.scale.light);
ImGui_Input("Select point", &vis.scale.selectpoint);
ImGui_Input("Joint length", &vis.scale.jointlength);
ImGui_Input("Joint width", &vis.scale.jointwidth);
ImGui_Input("Actuator length", &vis.scale.actuatorlength);
ImGui_Input("Actuator width", &vis.scale.actuatorwidth);
ImGui_Input("Frame length", &vis.scale.framelength);
ImGui_Input("Frame width", &vis.scale.framewidth);
ImGui_Input("Constraint", &vis.scale.constraint);
ImGui_Input("Slider-crank", &vis.scale.slidercrank);
ImGui::PopItemWidth();
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Colors")) {
ImGui::ColorEdit4("Fog", vis.rgba.fog);
ImGui::ColorEdit4("Haze", vis.rgba.haze);
ImGui::ColorEdit4("Force", vis.rgba.force);
ImGui::ColorEdit4("Inertia", vis.rgba.inertia);
ImGui::ColorEdit4("Joint", vis.rgba.joint);
ImGui::ColorEdit4("Actuator", vis.rgba.actuator);
ImGui::ColorEdit4("Act. Negative", vis.rgba.actuatornegative);
ImGui::ColorEdit4("Act. Positive", vis.rgba.actuatorpositive);
ImGui::ColorEdit4("Center of Mass", vis.rgba.com);
ImGui::ColorEdit4("Camera", vis.rgba.camera);
ImGui::ColorEdit4("Light", vis.rgba.light);
ImGui::ColorEdit4("Select Point", vis.rgba.selectpoint);
ImGui::ColorEdit4("Auto Connect", vis.rgba.connect);
ImGui::ColorEdit4("Contact Point", vis.rgba.contactpoint);
ImGui::ColorEdit4("Contact Force", vis.rgba.contactforce);
ImGui::ColorEdit4("Contact Friction", vis.rgba.contactfriction);
ImGui::ColorEdit4("Contact Torque", vis.rgba.contacttorque);
ImGui::ColorEdit4("Contact Gap", vis.rgba.contactgap);
ImGui::ColorEdit4("Range Finder", vis.rgba.rangefinder);
ImGui::ColorEdit4("Constraint", vis.rgba.constraint);
ImGui::ColorEdit4("Slider Crank", vis.rgba.slidercrank);
ImGui::ColorEdit4("Crank Broken", vis.rgba.crankbroken);
ImGui::ColorEdit4("Frustum", vis.rgba.frustum);
ImGui::ColorEdit4("Bounding Vol.", vis.rgba.bv);
ImGui::ColorEdit4("BV Active", vis.rgba.bvactive);
ImGui::TreePop();
}
}
void RenderingGui(const mjModel* model, mjvOption* vis_options,
mjtByte* render_flags, float min_width) {
const float available_width =
ImGui::GetContentRegionAvail().x - ImGui::GetTreeNodeToLabelSpacing();
const int num_cols = std::clamp(
static_cast<int>(std::floor(available_width / min_width)), 1, 6);
if (ImGui::TreeNodeEx("Model Elements", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing() / 2);
if (ImGui::BeginTable("##ModelElementsTable", num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < mjNVISFLAG; ++i) {
ImGui::TableNextColumn();
ImGui_ButtonToggle(mjVISSTRING[i][0], &vis_options->flags[i], size);
}
ImGui::EndTable();
}
ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing() / 2);
ImGui::TreePop();
}
if (ImGui::TreeNodeEx("Render Flags", ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing() / 2);
if (ImGui::BeginTable("##RenderFlagsTable", num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < mjNRNDFLAG; ++i) {
ImGui::TableNextColumn();
ImGui_ButtonToggle(mjRNDSTRING[i][0], &render_flags[i], size);
}
ImGui::EndTable();
}
ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing() / 2);
ImGui::TreePop();
}
}
void GroupsGui(const mjModel* model, mjvOption* vis_options, float min_width) {
const float available_width = ImGui::GetContentRegionAvail().x;
// We limit the number of columns to 1, 2, 3, or 6 depending on how much
// space the window has available.
int num_cols = std::clamp(
static_cast<int>(std::floor(available_width / min_width)), 1, 6);
if (num_cols == 4 || num_cols == 5) {
num_cols = 3;
}
auto GroupGui = [&](const char* name, mjtByte* group) {
if (ImGui::TreeNodeEx(name, ImGuiTreeNodeFlags_DefaultOpen)) {
ImGui::Unindent(ImGui::GetTreeNodeToLabelSpacing() / 2);
char label[64];
std::snprintf(label, sizeof(label), "##%s", name);
if (ImGui::BeginTable(label, num_cols)) {
const ImVec2 size = GetFlexElementSize(num_cols);
for (int i = 0; i < 6; ++i) {
ImGui::TableNextColumn();
std::snprintf(label, sizeof(label), "%s %d", name, i);
ImGui_ButtonToggle(label, &group[i], size);
}
ImGui::EndTable();
}
ImGui::Indent(ImGui::GetTreeNodeToLabelSpacing() / 2);
ImGui::TreePop();
}
};
GroupGui("Geoms", vis_options->geomgroup);
GroupGui("Sites", vis_options->sitegroup);
GroupGui("Joints", vis_options->jointgroup);
GroupGui("Tendons", vis_options->tendongroup);
GroupGui("Actuators", vis_options->actuatorgroup);
GroupGui("Flexes", vis_options->flexgroup);
GroupGui("Skins", vis_options->skingroup);
}
void NoiseGui(const mjModel* model, const mjData* data, float& noise_scale,
float& noise_rate) {
ImGui::SliderFloat("Scale", &noise_scale, 0, 1);
ImGui::SliderFloat("Rate", &noise_rate, 0, 4);
}
void JointsGui(const mjModel* model, const mjData* data,
const mjvOption* vis_options) {
char name[100];
for (int i = 0; i < model->njnt; ++i) {
if (model->jnt_type[i] != mjJNT_HINGE &&
model->jnt_type[i] != mjJNT_SLIDE) {
continue;
}
const int group = std::clamp(model->jnt_group[i], 0, mjNGROUP - 1);
if (!vis_options->jointgroup[group]) {
continue;
}
const char* jnt_name = model->names + model->name_jntadr[i];
if (*jnt_name) {
std::snprintf(name, sizeof(name), "%s", jnt_name);
} else {
std::snprintf(name, sizeof(name), "joint %d", i);
}
double min = -1.0;
double max = 1.0;
if (model->jnt_limited[i]) {
min = model->jnt_range[2 * i + 0];
max = model->jnt_range[2 * i + 1];
} else if (model->jnt_type[i] == mjJNT_SLIDE) {
min = -1.0;
max = 1.0;
} else {
min = -3.1416;
max = 3.1416;
}
const int data_adr = model->jnt_qposadr[i];
ImGui_Slider(name, &data->qpos[data_adr], min, max);
}
}
void ControlsGui(const mjModel* model, const mjData* data,
const mjvOption* vis_options) {
if (ImGui::Button("Clear All")) {
mju_zero(data->ctrl, model->nu);
}
char name[100];
for (int i = 0; i < model->nu; i++) {
int group = std::clamp(model->actuator_group[i], 0, mjNGROUP - 1);
if (!vis_options->actuatorgroup[group]) {
continue;
}
if (group >= 0 && group <= 30 &&
model->opt.disableactuator & (1 << group)) {
continue;
}
const char* ctrl_name = model->names + model->name_actuatoradr[i];
if (*ctrl_name) {
std::snprintf(name, sizeof(name), "%s", ctrl_name);
} else {
std::snprintf(name, sizeof(name), "control %d", i);
}
double min = -1.0;
double max = 1.0;
if (!model->actuator_ctrllimited[i]) {
min = model->actuator_ctrlrange[2 * i + 0];
max = model->actuator_ctrlrange[2 * i + 1];
}
ImGui_Slider(name, &data->ctrl[i], min, max);
}
}
void ConvergenceGui(const mjModel* model, mjData* data) {
if (ImPlot::BeginPlot("Convergence (log 10)", ImVec2(-1, 0))) {
ImPlot::SetupAxis(ImAxis_X1, "iteration", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, 20, ImPlotCond_Always);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.1f");
ImPlot::SetupAxisLimits(ImAxis_Y1, -20, 5, ImPlotCond_Always);
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish();
const int nisland = data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0;
for (int k = 0; k < nisland; k++) {
mjSolverStat* stats = data->solver + k * mjNSOLVER;
const int npoints =
mjMIN(mjMIN(data->solver_niter[k], mjNSOLVER), mjMAXLINEPNT);
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 3.0f);
ImPlot::PlotLineG("improvement", +[](int i, void* user_data) {
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = mju_log10(mju_max(mjMINVAL, stats[i].improvement));
return ImPlotPoint{x, y};
}, stats, npoints);
if (model->opt.solver == mjSOL_PGS) {
continue;
}
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 3.0f);
ImPlot::PlotLineG("gradient", +[](int i, void* user_data) {
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = mju_log10(mju_max(mjMINVAL, stats[i].gradient));
return ImPlotPoint{x, y};
}, stats, npoints);
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 3.0f);
ImPlot::PlotLineG("lineslope", +[](int i, void* user_data) {
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = mju_log10(mju_max(mjMINVAL, stats[i].lineslope));
return ImPlotPoint{x, y};
}, stats, npoints);
}
ImPlot::EndPlot();
}
}
void CountsGui(const mjModel* model, mjData* data) {
if (ImPlot::BeginPlot("Counts", ImVec2(-1, 0))) {
ImPlot::SetupAxis(ImAxis_X1, "iteration", ImPlotAxisFlags_AutoFit);
ImPlot::SetupAxisLimits(ImAxis_X1, 0, 20, ImPlotCond_Always);
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0f");
ImPlot::SetupAxisLimits(ImAxis_Y1, 0, 80, ImPlotCond_Always);
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
ImPlot::SetupFinish();
const int nisland = data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0;
for (int k = 0; k < nisland; k++) {
const int npoints =
mjMIN(mjMIN(data->solver_niter[k], mjNSOLVER), mjMAXLINEPNT);
mjSolverStat* stats = data->solver + k*mjNSOLVER;
int nefc = nisland == 1 ? data->nefc : data->island_nefc[k];
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 3.0f);
ImPlot::PlotLineG("total", +[](int i, void* user_data) {
const float x = static_cast<float>(i);
const float y = *(static_cast<int*>(user_data));
return ImPlotPoint{x, y};
}, &nefc, npoints);
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 3.0f);
ImPlot::PlotLineG("active", +[](int i, void* user_data) {
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = stats[i].nactive;
return ImPlotPoint{x, y};
}, stats, npoints);
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 3.0f);
ImPlot::PlotLineG("changed", +[](int i, void* user_data) {
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = stats[i].nchange;
return ImPlotPoint{x, y};
}, stats, npoints);
if (model->opt.solver == mjSOL_PGS) {
continue;
}
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 3.0f);
ImPlot::PlotLineG("evals", +[](int i, void* user_data) {
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = stats[i].neval;
return ImPlotPoint{x, y};
}, stats, npoints);
if (model->opt.solver == mjSOL_CG) {
continue;
}
ImPlot::SetNextLineStyle(IMPLOT_AUTO_COL, 3.0f);
ImPlot::PlotLineG("updates", +[](int i, void* user_data) {
const mjSolverStat* stats = static_cast<const mjSolverStat*>(user_data);
const float x = static_cast<float>(i);
const float y = stats[i].nupdate;
return ImPlotPoint{x, y};
}, stats, npoints);
}
ImPlot::EndPlot();
}
}
void InfoGui(const mjModel* model, const mjData* data, bool paused, float fps) {
const int num_islands = std::clamp(data->nisland, 1, mjNISLAND);
// compute solver error (maximum over islands)
mjtNum solver_err = 0;
int solver_iter = 0;
for (int i = 0; i < num_islands; i++) {
solver_iter += data->solver_niter[i];
mjtNum solerr_i = 0;
if (data->solver_niter[i]) {
const int ind = mjMIN(data->solver_niter[i], mjNSOLVER) - 1;
const mjSolverStat* stat = data->solver + i * mjNSOLVER + ind;
solerr_i = mju_min(stat->improvement, stat->gradient);
if (solerr_i == 0) {
solerr_i = mju_max(stat->improvement, stat->gradient);
}
}
solver_err = mju_max(solver_err, solerr_i);
}
solver_err = mju_log10(mju_max(mjMINVAL, solver_err));
auto type = paused ? mjTIMER_FORWARD : mjTIMER_STEP;
auto cpu = data->timer[type].duration / mjMAX(1, data->timer[type].number);
auto mempct = 100 * data->maxuse_arena / (double)(data->narena);
auto memlimit = mju_writeNumBytes(data->narena);
ImGui::Columns(2);
ImGui::SetColumnWidth(0, ImGui::GetWindowWidth() * 0.4f);
ImGui::SetColumnWidth(1, ImGui::GetWindowWidth() * 0.6f);
ImGui::Text("Time");
ImGui::Text("Size");
ImGui::Text("CPU");
ImGui::Text("Solver");
ImGui::Text("FPS");
ImGui::Text("Memory");
if (model->opt.enableflags & mjENBL_ENERGY) {
ImGui::Text("Energy");
}
if (model->opt.enableflags & mjENBL_FWDINV) {
ImGui::Text("FwdInv");
}
if (!(model->opt.disableflags & mjDSBL_ISLAND)) {
ImGui::Text("Islands");
}
ImGui::NextColumn();
ImGui::Text("%-9.3f", data->time);
ImGui::Text("%d (%d con)", data->nefc, data->ncon);
ImGui::Text("%.3f", cpu);
ImGui::Text("%.1f (%d it)", solver_err, solver_iter);
ImGui::Text("%0.1f", fps);
ImGui::Text("%.1f%% of %s", mempct, memlimit);
if (model->opt.enableflags & mjENBL_ENERGY) {
ImGui::Text("%.3f", data->energy[0] + data->energy[1]);
}
if (model->opt.enableflags & mjENBL_FWDINV) {
ImGui::Text("%.1f %.1f",
mju_log10(mju_max(mjMINVAL, data->solver_fwdinv[0])),
mju_log10(mju_max(mjMINVAL, data->solver_fwdinv[1])));
}
if (!(model->opt.disableflags & mjDSBL_ISLAND)) {
ImGui::Text("%d", data->nisland);
}
ImGui::Columns();
}
} // namespace mujoco::toolbox
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef MUJOCO_SRC_EXPERIMENTAL_TOOLBOX_GUI_H_
#define MUJOCO_SRC_EXPERIMENTAL_TOOLBOX_GUI_H_
// A collection of functions for building ImGui panels for common MuJoCo
// visualization and manipulation UX. These functions are primarily used by
// Studio, but are available for other applications.
//
// Like most ImGui functions, the actual "storage" for the GUI state is managed
// by the caller. In most cases, this is already stored in mjModel, mjData,
// mjvOption, etc. But, some functions take additional arguments as needed.
#include <vector>
#include <mujoco/mujoco.h>
namespace mujoco::toolbox {
// UX for controlling the physics simulation parameters (e.g. integrator,
// solver, etc.) in mjModel.
void PhysicsGui(mjModel* model, float min_width);
// UX for enabling/disabling visualization groups in mjvOption.
void GroupsGui(const mjModel* model, mjvOption* vis_options, float min_width);
// UX for enabling/disabling rendering (mjtRndFlag) and visualization
// (mjtVisFlag) flags. We combine these into a single function because the sets
// of flags are closely related.
void RenderingGui(const mjModel* model, mjvOption* vis_options,
mjtByte* render_flags, float min_width);
// UX for controlling the mjvOption and mjvCamera settings used for visualizing
// scenes (mjvScene).
void VisualizationGui(mjModel* model, mjvOption* vis_options, mjvCamera* camera,
float min_width);
// UX for visualizing actuator controls data in mjData.
void ControlsGui(const mjModel* model, const mjData* data,
const mjvOption* vis_options);
// UX for visualizing joint data in mjData.
void JointsGui(const mjModel* model, const mjData* data,
const mjvOption* vis_options);
// UX for visualizing sensor data in mjData.
void SensorGui(const mjModel* model, const mjData* data);
// UX for visualizing the data as returned from mj_getState(). We use a
// user-supplied vector here to avoid allocating memory every frame.
void StateGui(const mjModel* model, mjData* data, std::vector<mjtNum>& state,
int& state_sig, float min_width);
// UX for visualizing a named field from mjData. `field_name` and `field_index`
// are used to index into the data buffer.
void WatchGui(const mjModel* model, const mjData* data, char* field_name,
int field_len, int& field_index);
// UX for controlling noise parameters which can then be applied to the
// simulation via StepControl::SetNoiseParameters / StepControl::InjectNoise.
void NoiseGui(const mjModel* model, const mjData* data, float& noise_scale,
float& noise_rate);
// UX for the solver convergence chart.
void ConvergenceGui(const mjModel* model, mjData* data);
// UX for the solver counts chart.
void CountsGui(const mjModel* model, mjData* data);
// UX for displaying basic simulation information. Note that the pause state and
// FPS needs to be tracked by the caller and passed here to be displayed.
void InfoGui(const mjModel* model, const mjData* data, bool paused, float fps);
} // namespace mujoco::toolbox
#endif // MUJOCO_SRC_EXPERIMENTAL_TOOLBOX_GUI_H_
+1 -2
View File
@@ -52,8 +52,7 @@ class Renderer {
void SaveScreenshot(const std::string& filename, int width, int height);
// Rendering flags.
mjtByte GetFlag(mjtRndFlag flag) const { return scene_.flags[flag];}
void SetFlag(mjtRndFlag flag, mjtByte value) { scene_.flags[flag] = value; }
mjtByte* GetRenderFlags() { return scene_.flags; }
// Returns the current, average frame rate.
double GetFrameRate() const { return fps_; }