Rename toolbox to platform.
PiperOrigin-RevId: 838701951 Change-Id: I339eafd35d919710cb47bb04e9a8e29f92739513
This commit is contained in:
committed by
Copybara-Service
parent
c6b587b515
commit
139a5b6494
@@ -0,0 +1,60 @@
|
||||
# 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
|
||||
#
|
||||
# https://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.
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(MUJOCO_PLATFORM_TARGET_NAME mujoco_platform)
|
||||
|
||||
add_library(${MUJOCO_PLATFORM_TARGET_NAME} STATIC)
|
||||
|
||||
target_sources(${MUJOCO_PLATFORM_TARGET_NAME}
|
||||
PUBLIC
|
||||
gui.cc
|
||||
gui.h
|
||||
helpers.cc
|
||||
helpers.h
|
||||
imgui_widgets.cc
|
||||
imgui_widgets.h
|
||||
interaction.cc
|
||||
interaction.h
|
||||
renderer.cc
|
||||
renderer.h
|
||||
sim_history.cc
|
||||
sim_history.h
|
||||
sim_profiler.cc
|
||||
sim_profiler.h
|
||||
step_control.cc
|
||||
step_control.h
|
||||
window.cc
|
||||
window.h
|
||||
)
|
||||
target_include_directories(${MUJOCO_PLATFORM_TARGET_NAME}
|
||||
PUBLIC
|
||||
${PROJECT_SOURCE_DIR}/include
|
||||
${PROJECT_SOURCE_DIR}/src
|
||||
)
|
||||
|
||||
include(third_party_deps/dear_imgui)
|
||||
include(third_party_deps/implot)
|
||||
include(third_party_deps/sdl2)
|
||||
include(third_party_deps/libwebp)
|
||||
|
||||
target_link_libraries(${MUJOCO_PLATFORM_TARGET_NAME}
|
||||
dear_imgui
|
||||
implot
|
||||
webp
|
||||
SDL2::SDL2-static
|
||||
)
|
||||
|
||||
add_library(mujoco::platform ALIAS ${MUJOCO_PLATFORM_TARGET_NAME})
|
||||
@@ -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/platform/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/platform/helpers.h"
|
||||
#include "experimental/platform/imgui_widgets.h"
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
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::platform
|
||||
@@ -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_PLATFORM_GUI_H_
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_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::platform {
|
||||
|
||||
// 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::platform
|
||||
|
||||
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_GUI_H_
|
||||
@@ -0,0 +1,222 @@
|
||||
// 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/platform/helpers.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <ios>
|
||||
#include <iterator>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "webp/encode.h"
|
||||
#include "webp/types.h"
|
||||
#include <mujoco/mjrender.h>
|
||||
#include <mujoco/mjxmacro.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "engine/engine_vis_visualize.h"
|
||||
#include "xml/xml_api.h"
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
mjModel* LoadMujocoModel(const std::string& model_file, const mjVFS* vfs) {
|
||||
mjModel* model = nullptr;
|
||||
|
||||
if (model_file.empty()) {
|
||||
auto spec = mj_makeSpec();
|
||||
model = mj_compile(spec, 0);
|
||||
mj_deleteSpec(spec);
|
||||
} else if (model_file.ends_with(".mjb")) {
|
||||
model = mj_loadModel(model_file.c_str(), 0);
|
||||
if (!model) {
|
||||
return nullptr;
|
||||
}
|
||||
} else if (model_file.ends_with(".xml")) {
|
||||
char error[1000] = "";
|
||||
model = mj_loadXML(model_file.c_str(), vfs, error, sizeof(error));
|
||||
if (!model) {
|
||||
return nullptr;
|
||||
}
|
||||
} else {
|
||||
char error[1000] = "";
|
||||
auto spec =
|
||||
mj_parseXMLString(model_file.c_str(), nullptr, error, sizeof(error));
|
||||
if (!spec) {
|
||||
return nullptr;
|
||||
}
|
||||
model = mj_compile(spec, 0);
|
||||
mj_deleteSpec(spec);
|
||||
}
|
||||
return model;
|
||||
}
|
||||
|
||||
void SaveText(const std::string& contents, const std::string& filename) {
|
||||
std::ofstream file(filename);
|
||||
file.write(contents.data(), contents.size());
|
||||
file.close();
|
||||
}
|
||||
|
||||
std::string LoadText(const std::string& filename) {
|
||||
std::ifstream file(filename);
|
||||
std::string contents((std::istreambuf_iterator<char>(file)),
|
||||
std::istreambuf_iterator<char>());
|
||||
file.close();
|
||||
return contents;
|
||||
}
|
||||
|
||||
void SaveColorToWebp(int width, int height, const unsigned char* data,
|
||||
const std::string& filename) {
|
||||
uint8_t* webp = nullptr;
|
||||
const size_t size =
|
||||
WebPEncodeLosslessRGB(data, width, height, width * 3, &webp);
|
||||
|
||||
std::ofstream file(filename, std::ios::binary);
|
||||
file.write(reinterpret_cast<const char*>(webp), size);
|
||||
file.close();
|
||||
WebPFree(webp);
|
||||
}
|
||||
|
||||
void SaveDepthToWebp(int width, int height, const float* data,
|
||||
const std::string& filename) {
|
||||
const int size = width * height;
|
||||
|
||||
// Turn the depth buffer into a greyscale color buffer.
|
||||
std::vector<unsigned char> byte_buffer;
|
||||
byte_buffer.reserve(size * 3);
|
||||
for (int i = 0; i < size; ++i) {
|
||||
auto byte = static_cast<int>(255.0 * data[i]);
|
||||
byte_buffer.push_back(byte);
|
||||
byte_buffer.push_back(byte);
|
||||
byte_buffer.push_back(byte);
|
||||
}
|
||||
SaveColorToWebp(width, height, byte_buffer.data(), filename);
|
||||
}
|
||||
|
||||
void SaveScreenshotToWebp(int width, int height, mjrContext* con,
|
||||
const std::string& filename) {
|
||||
mjr_setBuffer(mjFB_OFFSCREEN, con);
|
||||
auto rgb_buffer = std::vector<unsigned char>(3 * width * height);
|
||||
auto depth_buffer = std::vector<float>(width * height, 1.0f);
|
||||
mjrRect viewport = {0, 0, width, height};
|
||||
mjr_readPixels(rgb_buffer.data(), depth_buffer.data(), viewport, con);
|
||||
mjr_setBuffer(mjFB_WINDOW, con);
|
||||
SaveColorToWebp(width, height, rgb_buffer.data(), filename);
|
||||
}
|
||||
|
||||
const void* GetValue(const mjModel* model, const mjData* data,
|
||||
const char* field, int index) {
|
||||
MJDATA_POINTERS_PREAMBLE(model);
|
||||
#define X(TYPE, NAME, NR, NC) \
|
||||
if (!std::strcmp(#NAME, field) && !std::strcmp(#TYPE, "mjtNum")) { \
|
||||
if (index >= 0 && index < model->NR * NC) { \
|
||||
return &data->NAME[index]; \
|
||||
} else { \
|
||||
return nullptr; \
|
||||
} \
|
||||
}
|
||||
MJDATA_POINTERS
|
||||
#undef X
|
||||
return nullptr; // Invalid field.
|
||||
}
|
||||
|
||||
std::string CameraToString(const mjData* data, const mjvCamera* camera) {
|
||||
mjtNum pos[3], forward[3], up[3], right[3];
|
||||
mjv_cameraFrame(pos, forward, up, right, data, camera);
|
||||
char str[500];
|
||||
std::snprintf(str, sizeof(str),
|
||||
"<camera pos=\"%.3f %.3f %.3f\" xyaxes=\"%.3f %.3f %.3f %.3f "
|
||||
"%.3f %.3f\"/>\n",
|
||||
pos[0], pos[1], pos[2], right[0], right[1], right[2],
|
||||
up[0], up[1], up[2]);
|
||||
return str;
|
||||
}
|
||||
|
||||
std::string KeyframeToString(const mjModel* model, const mjData* data,
|
||||
bool full_precision) {
|
||||
const int kStrLen = 5000;
|
||||
|
||||
char buf[200];
|
||||
const char p_regular[] = "%g";
|
||||
const char p_full[] = "%-22.16g";
|
||||
const char* format = full_precision ? p_full : p_regular;
|
||||
|
||||
char str[kStrLen] = "<key\n";
|
||||
|
||||
// time
|
||||
std::strncat(str, " time=\"", kStrLen);
|
||||
std::snprintf(buf, sizeof(buf), format, data->time);
|
||||
std::strncat(str, buf, kStrLen);
|
||||
|
||||
// qpos
|
||||
std::strncat(str, "\"\n qpos=\"", kStrLen);
|
||||
for (int i = 0; i < model->nq; i++) {
|
||||
std::snprintf(buf, sizeof(buf), format, data->qpos[i]);
|
||||
if (i < model->nq - 1) std::strncat(buf, " ", 200);
|
||||
std::strncat(str, buf, kStrLen);
|
||||
}
|
||||
|
||||
// qvel
|
||||
std::strncat(str, "\"\n qvel=\"", kStrLen);
|
||||
for (int i = 0; i < model->nv; i++) {
|
||||
std::snprintf(buf, sizeof(buf), format, data->qvel[i]);
|
||||
if (i < model->nv - 1) std::strncat(buf, " ", 200);
|
||||
std::strncat(str, buf, kStrLen);
|
||||
}
|
||||
|
||||
// act
|
||||
if (model->na > 0) {
|
||||
std::strncat(str, "\"\n act=\"", kStrLen);
|
||||
for (int i = 0; i < model->na; i++) {
|
||||
std::snprintf(buf, sizeof(buf), format, data->act[i]);
|
||||
if (i < model->na - 1) std::strncat(buf, " ", 200);
|
||||
std::strncat(str, buf, kStrLen);
|
||||
}
|
||||
}
|
||||
|
||||
// ctrl
|
||||
if (model->nu > 0) {
|
||||
std::strncat(str, "\"\n ctrl=\"", kStrLen);
|
||||
for (int i = 0; i < model->nu; i++) {
|
||||
std::snprintf(buf, sizeof(buf), format, data->ctrl[i]);
|
||||
if (i < model->nu - 1) std::strncat(buf, " ", 200);
|
||||
std::strncat(str, buf, kStrLen);
|
||||
}
|
||||
}
|
||||
|
||||
if (model->nmocap > 0) {
|
||||
std::strncat(str, "\"\n mpos=\"", kStrLen);
|
||||
for (int i = 0; i < 3 * model->nmocap; i++) {
|
||||
std::snprintf(buf, sizeof(buf), format, data->mocap_pos[i]);
|
||||
if (i < 3 * model->nmocap - 1) std::strncat(buf, " ", 200);
|
||||
std::strncat(str, buf, kStrLen);
|
||||
}
|
||||
|
||||
// mocap_quat
|
||||
std::strncat(str, "\"\n mquat=\"", kStrLen);
|
||||
for (int i = 0; i < 4 * model->nmocap; i++) {
|
||||
std::snprintf(buf, sizeof(buf), format, data->mocap_quat[i]);
|
||||
if (i < 4 * model->nmocap - 1) std::strncat(buf, " ", 200);
|
||||
std::strncat(str, buf, kStrLen);
|
||||
}
|
||||
}
|
||||
|
||||
std::strncat(str, "\"\n/>", kStrLen);
|
||||
return str;
|
||||
}
|
||||
|
||||
} // namespace mujoco::platform
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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.
|
||||
|
||||
// Standalone functions used by Simulate.
|
||||
#ifndef MUJOCO_SRC_EXPERIMENTAL_PLATFORM_HELPERS_H_
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_HELPERS_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <mujoco/mjrender.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
// Function signature for loading assets from a given path.
|
||||
using LoadAssetFn = std::function<std::vector<std::byte>(std::string_view)>;
|
||||
|
||||
// Save/load for simple ascii files.
|
||||
void SaveText(const std::string& contents, const std::string& filename);
|
||||
std::string LoadText(const std::string& filename);
|
||||
|
||||
// Exports the given color buffer to a webp file.
|
||||
void SaveColorToWebp(int width, int height, const unsigned char* data,
|
||||
const std::string& filename);
|
||||
|
||||
// Exports the given depth buffer to a webp file.
|
||||
void SaveDepthToWebp(int width, int height, const float* data,
|
||||
const std::string& filename);
|
||||
|
||||
// Exports the current state of the mjrContext to a webp file.
|
||||
void SaveScreenshotToWebp(int width, int height, mjrContext* con,
|
||||
const std::string& filename);
|
||||
|
||||
// Loads a MuJoCo model from the given file.
|
||||
mjModel* LoadMujocoModel(const std::string& model_file, const mjVFS* vfs);
|
||||
|
||||
// Returns a pointer to the value of the given field in the given data.
|
||||
// Returns nullptr if the field is not found or the index is out of bounds.
|
||||
const void* GetValue(const mjModel* model, const mjData* data,
|
||||
const char* field, int index);
|
||||
|
||||
// Returns an XML string representation of the camera.
|
||||
std::string CameraToString(const mjData* data, const mjvCamera* camera);
|
||||
|
||||
// Returns an XML string representation of current data keyframe.
|
||||
std::string KeyframeToString(const mjModel* model, const mjData* data,
|
||||
bool full_precision = false);
|
||||
|
||||
} // namespace mujoco::platform
|
||||
|
||||
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_HELPERS_H_
|
||||
@@ -0,0 +1,96 @@
|
||||
// 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/platform/imgui_widgets.h"
|
||||
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
void AppendIniSection(std::string& ini, const std::string& section,
|
||||
const KeyValues& key_values) {
|
||||
if (section.front() != '[' || section.back() != ']') {
|
||||
mju_error("Section must be enclosed in square brackets.");
|
||||
}
|
||||
ini += "\n" + std::string(section) + "\n";
|
||||
for (auto& [key, value] : key_values) {
|
||||
ini += key + "=" + value + "\n";
|
||||
}
|
||||
}
|
||||
|
||||
KeyValues ReadIniSection(const std::string& contents,
|
||||
const std::string& section) {
|
||||
if (section.front() != '[' || section.back() != ']') {
|
||||
mju_error("Section must be enclosed in square brackets.");
|
||||
}
|
||||
bool in_section = false;
|
||||
|
||||
KeyValues key_values;
|
||||
std::istringstream f(contents);
|
||||
std::string line;
|
||||
while (std::getline(f, line)) {
|
||||
if (line[0] == '[') {
|
||||
in_section = (line == section);
|
||||
} else if (in_section) {
|
||||
std::string::size_type pos = line.find('=');
|
||||
if (pos != std::string::npos) {
|
||||
key_values[line.substr(0, pos)] = line.substr(pos + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return key_values;
|
||||
}
|
||||
|
||||
bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max) {
|
||||
float f = *value;
|
||||
const bool res = ImGui::SliderFloat(name, &f, min, max);
|
||||
if (res) {
|
||||
*value = f;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
bool ImGui_FileDialog(char* buf, int len) {
|
||||
bool ok = false;
|
||||
ImGui::Text("Filename");
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(600);
|
||||
if (ImGui::InputText("##Filename", buf, len, ImGuiInputTextFlags_EnterReturnsTrue)) {
|
||||
ok = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
};
|
||||
if (ImGui::Button("OK", ImVec2(120, 0)) || ImGui_IsChordJustPressed(ImGuiKey_Enter)) {
|
||||
ok = true;
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
ImGui::SetItemDefaultFocus();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(120, 0)) || ImGui_IsChordJustPressed(ImGuiKey_Escape)) {
|
||||
ImGui::CloseCurrentPopup();
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
void MaybeSaveToClipboard(const std::string& contents) {
|
||||
if (ImGui::GetIO().SetClipboardTextFn) {
|
||||
ImGui::GetIO().SetClipboardTextFn(nullptr, contents.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mujoco::platform
|
||||
@@ -0,0 +1,228 @@
|
||||
// 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_PLATFORM_IMGUI_WIDGETS_H_
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_IMGUI_WIDGETS_H_
|
||||
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h> // For ButtonEx and PressedOnClick
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
using KeyValues = std::unordered_map<std::string, std::string>;
|
||||
|
||||
// Appends key/value pairs to an Ini file.
|
||||
void AppendIniSection(std::string& ini, const std::string& section,
|
||||
const KeyValues& key_values);
|
||||
|
||||
// Reads key/value pairs from an Ini file section.
|
||||
KeyValues ReadIniSection(const std::string& contents,
|
||||
const std::string& section);
|
||||
|
||||
// Helper class for setting ImGui style options; automatically resets the
|
||||
// styles when going out of scope.
|
||||
struct ScopedStyle {
|
||||
ScopedStyle() = default;
|
||||
~ScopedStyle() {
|
||||
Reset();
|
||||
}
|
||||
|
||||
ScopedStyle(const ScopedStyle&) = delete;
|
||||
ScopedStyle& operator=(const ScopedStyle&) = delete;
|
||||
ScopedStyle(ScopedStyle&& other) { Swap(other); }
|
||||
ScopedStyle& operator=(ScopedStyle&& other) { Swap(other); return *this; }
|
||||
|
||||
void Swap(ScopedStyle& other) {
|
||||
std::swap(num_colors, other.num_colors);
|
||||
std::swap(num_vars, other.num_vars);
|
||||
}
|
||||
|
||||
ScopedStyle& Color(ImGuiCol col, ImColor color) {
|
||||
ImGui::PushStyleColor(col, (ImU32)color);
|
||||
++num_colors;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ScopedStyle& Var(ImGuiStyleVar var, float value) {
|
||||
ImGui::PushStyleVar(var, value);
|
||||
++num_vars;
|
||||
return *this;
|
||||
}
|
||||
|
||||
ScopedStyle& Var(ImGuiStyleVar var, const ImVec2& value) {
|
||||
ImGui::PushStyleVar(var, value);
|
||||
++num_vars;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
ImGui::PopStyleVar(num_vars);
|
||||
ImGui::PopStyleColor(num_colors);
|
||||
num_colors = 0;
|
||||
num_vars = 0;
|
||||
}
|
||||
|
||||
int num_colors = 0;
|
||||
int num_vars = 0;
|
||||
};
|
||||
|
||||
// ImGui file dialog.
|
||||
bool ImGui_FileDialog(char* buf, int len);
|
||||
|
||||
// ImGui Slider that supports both float and double types.
|
||||
bool ImGui_Slider(const char* name, mjtNum* value, mjtNum min, mjtNum max);
|
||||
|
||||
template <typename T>
|
||||
bool ImGui_Checkbox(const char* name, T& value) {
|
||||
static_assert(std::is_integral<T>());
|
||||
bool b = (value != 0);
|
||||
const bool res = ImGui::Checkbox(name, &b);
|
||||
if (res) {
|
||||
value = b ? 1 : 0;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool ImGui_ButtonToggle(const char* label, T* boolean,
|
||||
const ImVec2& size = ImVec2(0, 0)) {
|
||||
static_assert(std::is_integral_v<T>, "Toggle only supports integral types.");
|
||||
|
||||
ScopedStyle style;
|
||||
if (!(*boolean)) {
|
||||
ImColor button = ImGui::GetStyle().Colors[ImGuiCol_Button];
|
||||
button.Value.w = 0.0f;
|
||||
style.Color(ImGuiCol_Button, button);
|
||||
}
|
||||
|
||||
if (ImGui::Button(label, size)) {
|
||||
*boolean = !(*boolean);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool ImGui_SwitchToggle(const char* label, T* boolean,
|
||||
const ImVec2& size = ImVec2(0, 0)) {
|
||||
static_assert(std::is_integral_v<T>, "Toggle only supports integral types.");
|
||||
|
||||
int i = static_cast<int>(*boolean);
|
||||
const ImGuiSliderFlags flags = ImGuiSliderFlags_NoInput;
|
||||
if (size.x > 0) {
|
||||
ImGui::SetNextItemWidth(size.x);
|
||||
}
|
||||
const bool changed = ImGui::SliderInt(label, &i, 0, 1, label, flags);
|
||||
*boolean = (i != 0);
|
||||
return changed;
|
||||
}
|
||||
|
||||
inline bool ImGui_BitToggle(const char* label, int* flags, int flags_value,
|
||||
const ImVec2& size = ImVec2(0, 0)) {
|
||||
bool boolean = (*flags) & flags_value;
|
||||
const bool changed = ImGui_ButtonToggle(label, &boolean, size);
|
||||
if (changed) {
|
||||
*flags = boolean ? ((*flags) | flags_value) : ((*flags) & ~flags_value);
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
// Options for ImGui_InputN (see below).
|
||||
template <typename T>
|
||||
struct ImGuiOpts {
|
||||
std::optional<T> min;
|
||||
std::optional<T> max;
|
||||
std::optional<T> step;
|
||||
std::optional<T> step_fast;
|
||||
std::optional<float> width;
|
||||
const char* format = std::is_floating_point_v<T> ? "%.3g" : "%d";
|
||||
};
|
||||
|
||||
// This is a workaround to fix compilation on gcc <= 12 and clang <= 16
|
||||
template <typename T>
|
||||
struct dependent_false : std::false_type {};
|
||||
|
||||
// A compile-time wrapper around ImGui::InputScalarN. This is useful because
|
||||
// MuJoCo uses an `mjtNum` type which is an alias for float or double.
|
||||
//
|
||||
// Options can be used to specify step sizes, clamp ranges, and formatting.
|
||||
template <typename T>
|
||||
bool ImGui_InputN(const char* name, T* value, int num, ImGuiOpts<T> opts = {}) {
|
||||
bool res = false;
|
||||
if (opts.width) {
|
||||
ImGui::SetNextItemWidth(opts.width.value());
|
||||
}
|
||||
if constexpr (std::is_same_v<T, int>) {
|
||||
const int step = opts.step.value_or(1);
|
||||
const int step_fast = opts.step_fast.value_or(100);
|
||||
const char* format = opts.format;
|
||||
res = ImGui::InputScalarN(name, ImGuiDataType_S32, value, num, &step,
|
||||
&step_fast, format);
|
||||
|
||||
} else if constexpr (std::is_same_v<T, float>) {
|
||||
const float step = opts.step.value_or(0.f);
|
||||
const float step_fast = opts.step_fast.value_or(0.f);
|
||||
const float* pstep = opts.step.has_value() ? &step : nullptr;
|
||||
const float* pstep_fast = opts.step_fast.has_value() ? &step_fast : nullptr;
|
||||
const char* format = opts.format ? opts.format : "%.3f";
|
||||
res = ImGui::InputScalarN(name, ImGuiDataType_Float, value, num, pstep,
|
||||
pstep_fast, format);
|
||||
|
||||
} else if constexpr (std::is_same_v<T, double>) {
|
||||
const double step = opts.step.value_or(0.0);
|
||||
const double step_fast = opts.step_fast.value_or(0.0);
|
||||
const double* pstep = opts.step.has_value() ? &step : nullptr;
|
||||
const double* pstep_fast =
|
||||
opts.step_fast.has_value() ? &step_fast : nullptr;
|
||||
const char* format = opts.format ? opts.format : "%.3f";
|
||||
res = ImGui::InputScalarN(name, ImGuiDataType_Double, value, num, pstep,
|
||||
pstep_fast, format);
|
||||
} else {
|
||||
static_assert(dependent_false<T>::value, "Unsupported type");
|
||||
}
|
||||
|
||||
if (opts.min.has_value()) {
|
||||
if (*value < *opts.min) *value = *opts.min;
|
||||
}
|
||||
if (opts.max.has_value()) {
|
||||
if (*value > *opts.max) *value = *opts.max;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool ImGui_Input(const char* name, T* value, ImGuiOpts<T> opts = {}) {
|
||||
return ImGui_InputN(name, value, 1, opts);
|
||||
}
|
||||
|
||||
// Returns true if the given chord is has _just_ been pressed in this frame.
|
||||
// (This is opposed to "Pressed" which means the chord is active, i.e. the user
|
||||
// is holding down the keys.)
|
||||
inline bool ImGui_IsChordJustPressed(ImGuiKeyChord chord) {
|
||||
return ImGui::IsKeyChordPressed(chord, 0);
|
||||
}
|
||||
|
||||
// Saves the given contents to the clipboard if the clipboard is available.
|
||||
void MaybeSaveToClipboard(const std::string& contents);
|
||||
|
||||
} // namespace mujoco::platform
|
||||
|
||||
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_IMGUI_WIDGETS_H_
|
||||
@@ -0,0 +1,627 @@
|
||||
// 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/platform/interaction.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "engine/engine_util_errmem.h"
|
||||
#include "engine/engine_util_misc.h"
|
||||
#include "engine/engine_vis_visualize.h"
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
static mjtNum CalculateMovementScale(const mjModel* m, const mjvCamera* cam) {
|
||||
float zclip[2] = {0, 0}, zver[2] = {0, 0};
|
||||
mjv_cameraFrustum(zver, nullptr, zclip, m, cam);
|
||||
if (cam->orthographic) {
|
||||
// TODO(b/346130949): multiply by mystery coefficient
|
||||
return (zver[1] + zver[0]) * 0.15;
|
||||
} else if (zclip[0] >= mjMINVAL) {
|
||||
return (zver[1] + zver[0]) / zclip[0];
|
||||
} else {
|
||||
mjERROR("mjvScene frustum_near too small");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static void AlignToCamera(mjtNum res[3], mjtMouse action, mjtNum dx, mjtNum dy,
|
||||
const mjtNum forward[3]) {
|
||||
mjtNum vec[3];
|
||||
switch (action) {
|
||||
case mjMOUSE_ROTATE_V:
|
||||
vec[0] = dy;
|
||||
vec[1] = 0;
|
||||
vec[2] = dx;
|
||||
break;
|
||||
case mjMOUSE_ROTATE_H:
|
||||
vec[0] = dy;
|
||||
vec[1] = dx;
|
||||
vec[2] = 0;
|
||||
break;
|
||||
case mjMOUSE_MOVE_V:
|
||||
case mjMOUSE_MOVE_V_REL:
|
||||
vec[0] = dx;
|
||||
vec[1] = 0;
|
||||
vec[2] = -dy;
|
||||
break;
|
||||
case mjMOUSE_MOVE_H:
|
||||
case mjMOUSE_MOVE_H_REL:
|
||||
vec[0] = dx;
|
||||
vec[1] = -dy;
|
||||
vec[2] = 0;
|
||||
break;
|
||||
case mjMOUSE_ZOOM:
|
||||
break;
|
||||
default:
|
||||
mjERROR("unexpected mouse action %d in AlignToCamera", action);
|
||||
}
|
||||
|
||||
// call 3D converter
|
||||
mjv_alignToCamera(res, vec, forward);
|
||||
}
|
||||
|
||||
void InitPerturb(const mjModel* m, const mjData* d, const mjvCamera* cam,
|
||||
mjvPerturb* pert, mjtPertBit active) {
|
||||
// compute selection point in world coordinates
|
||||
const int sel = pert->select;
|
||||
mjtNum selpos[3];
|
||||
mju_mulMatVec3(selpos, d->xmat + 9 * sel, pert->localpos);
|
||||
mju_addTo3(selpos, d->xpos + 3 * sel);
|
||||
|
||||
// compute average spatial inertia at selection point
|
||||
const int nv = m->nv;
|
||||
std::vector<mjtNum> sqrtInvD(nv);
|
||||
for (int i = 0; i < nv; i++) {
|
||||
sqrtInvD[i] = mju_sqrt(d->qLDiagInv[i]);
|
||||
}
|
||||
|
||||
std::vector<mjtNum> jac(3 * nv);
|
||||
mj_jac(m, d, jac.data(), nullptr, selpos, sel);
|
||||
|
||||
std::vector<mjtNum> jacM2(3 * nv);
|
||||
mj_solveM2(m, const_cast<mjData*>(d), jacM2.data(), jac.data(),
|
||||
sqrtInvD.data(), 3);
|
||||
mjtNum invmass = mju_dot(jacM2.data() + 0 * nv, jacM2.data() + 0 * nv, nv) +
|
||||
mju_dot(jacM2.data() + 1 * nv, jacM2.data() + 1 * nv, nv) +
|
||||
mju_dot(jacM2.data() + 2 * nv, jacM2.data() + 2 * nv, nv);
|
||||
pert->localmass = (invmass == 0) ? 1 : 3 / mju_max(invmass, mjMINVAL);
|
||||
|
||||
// scale localmass with flex average number of edges per vertex
|
||||
if (pert->flexselect >= 0 && !m->flex_rigid[pert->flexselect]) {
|
||||
pert->localmass *= (2.0 * m->flex_edgenum[pert->flexselect]) /
|
||||
(mjtNum)m->flex_vertnum[pert->flexselect];
|
||||
}
|
||||
|
||||
// copy
|
||||
mju_copy3(pert->refpos, d->xipos + 3 * sel);
|
||||
mju_mulQuat(pert->refquat, d->xquat + 4 * sel, m->body_iquat + 4 * sel);
|
||||
mju_copy3(pert->refselpos, selpos);
|
||||
|
||||
// get camera info
|
||||
mjtNum headpos[3], forward[3];
|
||||
mjv_cameraFrame(headpos, forward, nullptr, nullptr, d, cam);
|
||||
|
||||
// compute scaling: rendered pert->refselpos displacement = mouse displacement
|
||||
mjtNum dif[3];
|
||||
mju_sub3(dif, pert->refselpos, headpos);
|
||||
pert->scale = CalculateMovementScale(m, cam) * mju_dot3(dif, forward);
|
||||
pert->active = active;
|
||||
}
|
||||
|
||||
void MovePerturb(const mjModel* m, const mjData* d, const mjvCamera* cam,
|
||||
mjvPerturb* pert, mjtMouse action, mjtNum reldx,
|
||||
mjtNum reldy) {
|
||||
const mjtNum xaxis[3] = {1, 0, 0};
|
||||
const mjtNum yaxis[3] = {0, 1, 0};
|
||||
const mjtNum zaxis[3] = {0, 0, 1};
|
||||
|
||||
int sel = pert->select;
|
||||
const mjtNum* xmat = d->xmat + 9 * sel;
|
||||
mjtNum forward[3], vec[3], scl, q1[4], xiquat[4];
|
||||
|
||||
// get camera info and align
|
||||
mjv_cameraFrame(nullptr, forward, nullptr, nullptr, d, cam);
|
||||
AlignToCamera(vec, action, reldx, reldy, forward);
|
||||
|
||||
// process action
|
||||
switch ((mjtMouse)action) {
|
||||
case mjMOUSE_MOVE_V:
|
||||
case mjMOUSE_MOVE_H:
|
||||
// move along world-space horizontal/vertical planes relative to camera
|
||||
mju_addToScl3(pert->refpos, vec, pert->scale);
|
||||
mju_addToScl3(pert->refselpos, vec, pert->scale);
|
||||
break;
|
||||
|
||||
case mjMOUSE_MOVE_V_REL:
|
||||
case mjMOUSE_MOVE_H_REL:
|
||||
// move along object's local coordinate frame
|
||||
if (action == mjMOUSE_MOVE_H_REL) {
|
||||
mju_mulMatVec3(vec, xmat, xaxis);
|
||||
mju_addToScl3(pert->refpos, vec, pert->scale * reldy);
|
||||
mju_addToScl3(pert->refselpos, vec, pert->scale * reldy);
|
||||
} else {
|
||||
mju_mulMatVec3(vec, xmat, zaxis);
|
||||
mju_addToScl3(pert->refpos, vec, pert->scale * reldy);
|
||||
mju_addToScl3(pert->refselpos, vec, pert->scale * reldy);
|
||||
}
|
||||
|
||||
mju_mulMatVec3(vec, xmat, yaxis);
|
||||
mju_addToScl3(pert->refpos, vec, pert->scale * reldx);
|
||||
mju_addToScl3(pert->refselpos, vec, pert->scale * reldx);
|
||||
break;
|
||||
|
||||
case mjMOUSE_ROTATE_V:
|
||||
case mjMOUSE_ROTATE_H:
|
||||
// normalize vector, get length
|
||||
scl = mju_normalize3(vec);
|
||||
|
||||
// make quaternion and apply
|
||||
mju_axisAngle2Quat(q1, vec, scl * mjPI * 2);
|
||||
mju_mulQuat(pert->refquat, q1, pert->refquat);
|
||||
mju_normalize4(pert->refquat);
|
||||
|
||||
// compute xiquat
|
||||
mju_mulQuat(xiquat, d->xquat + 4 * sel, m->body_iquat + 4 * sel);
|
||||
|
||||
// limit rotation relative to selected body
|
||||
if (sel > 0 && sel < m->nbody) {
|
||||
// q2 = neg(selbody) * refquat
|
||||
mjtNum q2[4];
|
||||
mju_negQuat(q1, xiquat);
|
||||
mju_mulQuat(q2, q1, pert->refquat);
|
||||
|
||||
// convert q2 to axis-angle
|
||||
mjtNum dif[3];
|
||||
mju_quat2Vel(dif, q2, 1);
|
||||
scl = mju_normalize3(dif);
|
||||
|
||||
// check limit: +/- 90 deg allowed
|
||||
if (scl < -mjPI * 0.5 || scl > mjPI * 0.5) {
|
||||
// clamp angle
|
||||
scl = mju_max(-mjPI * 0.5, mju_min(mjPI * 0.5, scl));
|
||||
|
||||
// reconstruct q2
|
||||
mju_axisAngle2Quat(q2, dif, scl);
|
||||
|
||||
// set refquat = selbody * q2_new
|
||||
mju_mulQuat(pert->refquat, xiquat, q2);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case mjMOUSE_ZOOM:
|
||||
break;
|
||||
|
||||
default:
|
||||
mjERROR("unexpected mouse action %d", action);
|
||||
}
|
||||
}
|
||||
|
||||
void MoveCamera(const mjModel* m, const mjData* d, mjvCamera* cam,
|
||||
CameraMotion motion, mjtNum dx, mjtNum dy) {
|
||||
if (cam->type == mjCAMERA_FIXED) {
|
||||
return;
|
||||
}
|
||||
|
||||
mjtNum headpos[3], forward[3], up[3], right[3];
|
||||
mjtNum vec[3], dif[3], scl;
|
||||
|
||||
switch (motion) {
|
||||
case CameraMotion::ZOOM:
|
||||
// Zoom the camera towards the target by adjusting its distance to the
|
||||
// target.
|
||||
cam->distance -= mju_log(1 + cam->distance / m->stat.extent / 3) * dy *
|
||||
9 * m->stat.extent;
|
||||
break;
|
||||
|
||||
case CameraMotion::ORBIT:
|
||||
cam->azimuth -= dx * 180.0;
|
||||
cam->elevation -= dy * 180.0;
|
||||
break;
|
||||
|
||||
case CameraMotion::TRUCK_PEDESTAL:
|
||||
case CameraMotion::TRUCK_DOLLY:
|
||||
if (cam->type == mjCAMERA_TRACKING) {
|
||||
return;
|
||||
}
|
||||
|
||||
mjv_cameraFrame(headpos, forward, up, nullptr, d, cam);
|
||||
mju_cross(right, forward, up);
|
||||
|
||||
// y movement: either dolly (forward/back) or pedestal (up/down)
|
||||
mju_addToScl3(cam->lookat,
|
||||
(motion == CameraMotion::TRUCK_PEDESTAL) ? up : forward, dy);
|
||||
|
||||
// x movement: camera truck (left/right)
|
||||
mju_addToScl3(cam->lookat, right, dx);
|
||||
|
||||
break;
|
||||
|
||||
case CameraMotion::PAN_TILT:
|
||||
if (cam->type == mjCAMERA_TRACKING) {
|
||||
return;
|
||||
}
|
||||
|
||||
mjv_cameraFrame(headpos, forward, nullptr, nullptr, d, cam);
|
||||
cam->azimuth -= dx * 180.0;
|
||||
cam->elevation -= dy * 180.0;
|
||||
mjv_cameraFrame(nullptr, forward, nullptr, nullptr, d, cam);
|
||||
mju_addScl3(cam->lookat, headpos, forward, cam->distance);
|
||||
break;
|
||||
|
||||
case CameraMotion::PLANAR_MOVE_V:
|
||||
case CameraMotion::PLANAR_MOVE_H:
|
||||
// do not move lookat point of tracking camera
|
||||
if (cam->type == mjCAMERA_TRACKING) {
|
||||
return;
|
||||
}
|
||||
|
||||
// get camera info and align
|
||||
mjv_cameraFrame(headpos, forward, nullptr, nullptr, d, cam);
|
||||
AlignToCamera(vec,
|
||||
(motion == CameraMotion::PLANAR_MOVE_V) ? mjMOUSE_MOVE_V
|
||||
: mjMOUSE_MOVE_H,
|
||||
dx, dy, forward);
|
||||
|
||||
// compute scaling: rendered lookat displacement = mouse displacement
|
||||
mju_sub3(dif, cam->lookat, headpos);
|
||||
scl = CalculateMovementScale(m, cam) * mju_dot3(dif, forward);
|
||||
|
||||
// move lookat point in opposite direction
|
||||
mju_addToScl3(cam->lookat, vec, -scl);
|
||||
break;
|
||||
}
|
||||
|
||||
// clamp camera parameters
|
||||
if (cam->azimuth > 180) {
|
||||
cam->azimuth -= 360;
|
||||
}
|
||||
if (cam->azimuth < -180) {
|
||||
cam->azimuth += 360;
|
||||
}
|
||||
if (cam->elevation > 89) {
|
||||
cam->elevation = 89;
|
||||
}
|
||||
if (cam->elevation < -89) {
|
||||
cam->elevation = -89;
|
||||
}
|
||||
if (cam->distance < 0.01 * m->stat.extent) {
|
||||
cam->distance = 0.01 * m->stat.extent;
|
||||
}
|
||||
if (cam->distance > 100 * m->stat.extent) {
|
||||
cam->distance = 100 * m->stat.extent;
|
||||
}
|
||||
}
|
||||
|
||||
static void MakePickRay(mjtNum pos[3], mjtNum ray[3], const mjModel* m,
|
||||
const mjData* d, const mjvCamera* camera, float relx,
|
||||
float rely, float aspect_ratio) {
|
||||
mjtNum forward[3], up[3], right[3];
|
||||
mjv_cameraFrame(pos, forward, up, right, d, camera);
|
||||
|
||||
float zver[2], zhor[2], zclip[2] = {0, 0};
|
||||
mjv_cameraFrustum(zver, zhor, zclip, m, camera);
|
||||
|
||||
// compute frustum halfwidth to match viewport aspect ratio
|
||||
mjtNum half_width = 0.5 * aspect_ratio * (zver[0] + zver[1]);
|
||||
mjtNum frustum_center = (zhor[1] - zhor[0]) / 2;
|
||||
|
||||
// compute up and right offsets from normalized cursor
|
||||
mjtNum d_up = -zver[0] + rely * (zver[0] + zver[1]);
|
||||
mjtNum d_right = frustum_center + (2 * relx - 1) * half_width;
|
||||
|
||||
if (camera->orthographic) {
|
||||
mju_copy3(ray, forward);
|
||||
mju_addToScl3(pos, up, d_up);
|
||||
mju_addToScl3(pos, right, d_right);
|
||||
} else {
|
||||
mju_scl3(ray, forward, zclip[0]);
|
||||
mju_addToScl3(ray, up, d_up);
|
||||
mju_addToScl3(ray, right, d_right);
|
||||
mju_normalize3(ray);
|
||||
}
|
||||
}
|
||||
|
||||
static PickResult PickGeom(const mjModel* m, const mjData* d,
|
||||
const mjtNum ray_pos[3], const mjtNum ray_dir[3],
|
||||
const mjvOption* vis_options) {
|
||||
PickResult result;
|
||||
result.dist = mj_ray(m, d, ray_pos, ray_dir, vis_options->geomgroup,
|
||||
vis_options->flags[mjVIS_STATIC], -1, &result.geom);
|
||||
mju_addScl3(result.point, ray_pos, ray_dir, result.dist);
|
||||
result.body = m->geom_bodyid[result.geom];
|
||||
return result;
|
||||
}
|
||||
|
||||
static PickResult PickFlex(const mjModel* m, const mjData* d,
|
||||
const mjtNum ray_pos[3], const mjtNum ray_dir[3],
|
||||
const mjvOption* vis_options) {
|
||||
const mjtByte flag_vert = vis_options->flags[mjVIS_FLEXVERT];
|
||||
const mjtByte flag_edge = vis_options->flags[mjVIS_FLEXEDGE];
|
||||
const mjtByte flag_face = vis_options->flags[mjVIS_FLEXFACE];
|
||||
const mjtByte flag_skin = vis_options->flags[mjVIS_FLEXSKIN];
|
||||
|
||||
PickResult result;
|
||||
if (!flag_vert && !flag_edge && !flag_face && !flag_skin) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m->nflex; i++) {
|
||||
int vertid;
|
||||
const mjtNum test_dist =
|
||||
mju_rayFlex(m, d, vis_options->flex_layer, flag_vert, flag_edge,
|
||||
flag_face, flag_skin, i, ray_pos, ray_dir, &vertid);
|
||||
|
||||
if (test_dist < 0) {
|
||||
continue;
|
||||
} else if (result.dist >= 0 && test_dist >= result.dist) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.dist = test_dist;
|
||||
if (m->flex_interp[i]) {
|
||||
const mjtNum* coord = m->flex_vert0 + 3 * (m->flex_vertadr[i] + vertid);
|
||||
mjtNum w = 0;
|
||||
int nodeid = -1;
|
||||
int nstart = m->flex_nodeadr[i];
|
||||
int nend = nstart + m->flex_nodenum[i];
|
||||
for (int j = nstart; j < nend; j++) {
|
||||
if (mju_evalBasis(coord, j - nstart, m->flex_interp[i]) > w) {
|
||||
w = mju_evalBasis(coord, j - nstart, m->flex_interp[i]);
|
||||
nodeid = j;
|
||||
}
|
||||
}
|
||||
if (nodeid < 0) {
|
||||
mjERROR("flex %d: node closest to vertex %d not found", i, vertid);
|
||||
}
|
||||
result.body = m->flex_nodebodyid[m->flex_nodeadr[i] + nodeid];
|
||||
|
||||
if (m->flex_centered[i]) {
|
||||
mju_copy3(result.point, d->xpos + 3 * result.body);
|
||||
} else {
|
||||
mju_mulMatVec3(result.point, d->xmat + 9 * result.body,
|
||||
m->flex_node + 3 * nodeid);
|
||||
mju_addTo3(result.point, d->xpos + 3 * result.body);
|
||||
}
|
||||
} else {
|
||||
result.body = m->flex_vertbodyid[m->flex_vertadr[i] + vertid];
|
||||
mju_copy3(result.point,
|
||||
d->flexvert_xpos + 3 * (m->flex_vertadr[i] + vertid));
|
||||
}
|
||||
result.flex = i;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static void MakeSkin(const mjModel* m, const mjData* d, const mjvOption* opt,
|
||||
int i, float* skinnormal, float* skinvert) {
|
||||
int vertadr = m->skin_vertadr[i];
|
||||
int vertnum = m->skin_vertnum[i];
|
||||
int faceadr = m->skin_faceadr[i];
|
||||
int facenum = m->skin_facenum[i];
|
||||
|
||||
// accumulate positions from all bones
|
||||
for (int j = m->skin_boneadr[i]; j < m->skin_boneadr[i] + m->skin_bonenum[i];
|
||||
j++) {
|
||||
// get bind pose
|
||||
mjtNum bindpos[3] = {(mjtNum)m->skin_bonebindpos[3 * j + 0],
|
||||
(mjtNum)m->skin_bonebindpos[3 * j + 1],
|
||||
(mjtNum)m->skin_bonebindpos[3 * j + 2]};
|
||||
mjtNum bindquat[4] = {(mjtNum)m->skin_bonebindquat[4 * j + 0],
|
||||
(mjtNum)m->skin_bonebindquat[4 * j + 1],
|
||||
(mjtNum)m->skin_bonebindquat[4 * j + 2],
|
||||
(mjtNum)m->skin_bonebindquat[4 * j + 3]};
|
||||
|
||||
// compute rotation
|
||||
int bodyid = m->skin_bonebodyid[j];
|
||||
mjtNum quat[4], quatneg[4], rotate[9];
|
||||
mju_negQuat(quatneg, bindquat);
|
||||
mju_mulQuat(quat, d->xquat + 4 * bodyid, quatneg);
|
||||
mju_quat2Mat(rotate, quat);
|
||||
|
||||
// compute translation
|
||||
mjtNum translate[3];
|
||||
mju_mulMatVec3(translate, rotate, bindpos);
|
||||
mju_sub3(translate, d->xpos + 3 * bodyid, translate);
|
||||
|
||||
// process all bone vertices
|
||||
for (int k = m->skin_bonevertadr[j];
|
||||
k < m->skin_bonevertadr[j] + m->skin_bonevertnum[j]; k++) {
|
||||
// vertex id and weight
|
||||
int vid = m->skin_bonevertid[k];
|
||||
float vweight = m->skin_bonevertweight[k];
|
||||
|
||||
// get original position
|
||||
mjtNum pos[3] = {
|
||||
(mjtNum)m->skin_vert[3 * (vertadr + vid)],
|
||||
(mjtNum)m->skin_vert[3 * (vertadr + vid) + 1],
|
||||
(mjtNum)m->skin_vert[3 * (vertadr + vid) + 2],
|
||||
};
|
||||
|
||||
// transform
|
||||
mjtNum pos1[3];
|
||||
mju_mulMatVec3(pos1, rotate, pos);
|
||||
mju_addTo3(pos1, translate);
|
||||
|
||||
// accumulate position
|
||||
skinvert[(3 * vid)] += vweight * (float)pos1[0];
|
||||
skinvert[(3 * vid) + 1] += vweight * (float)pos1[1];
|
||||
skinvert[(3 * vid) + 2] += vweight * (float)pos1[2];
|
||||
}
|
||||
}
|
||||
|
||||
// inflate
|
||||
if (m->skin_inflate[i] && skinnormal != nullptr) {
|
||||
// compute vertex normals from face normals
|
||||
for (int k = faceadr; k < faceadr + facenum; k++) {
|
||||
// get face vertex indices
|
||||
int vid[3] = {m->skin_face[3 * k], m->skin_face[3 * k + 1],
|
||||
m->skin_face[3 * k + 2]};
|
||||
|
||||
// get triangle edges
|
||||
mjtNum vec01[3], vec02[3];
|
||||
for (int r = 0; r < 3; r++) {
|
||||
vec01[r] = skinvert[3 * (vid[1]) + r] - skinvert[3 * (vid[0]) + r];
|
||||
vec02[r] = skinvert[3 * (vid[2]) + r] - skinvert[3 * (vid[0]) + r];
|
||||
}
|
||||
|
||||
// compute face normal
|
||||
mjtNum nrm[3];
|
||||
mju_cross(nrm, vec01, vec02);
|
||||
|
||||
// add normal to each vertex with weight = area
|
||||
for (int r = 0; r < 3; r++) {
|
||||
for (int t = 0; t < 3; t++) {
|
||||
skinnormal[3 * (vid[r]) + t] += nrm[t];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// normalize normals
|
||||
for (int k = 0; k < vertnum; k++) {
|
||||
float s = sqrtf(skinnormal[3 * (k) + 0] * skinnormal[3 * k + 0] +
|
||||
skinnormal[3 * (k) + 1] * skinnormal[3 * k + 1] +
|
||||
skinnormal[3 * (k) + 2] * skinnormal[3 * k + 2]);
|
||||
|
||||
float scl = 1 / mjMAX(mjMINVAL, s);
|
||||
skinnormal[3 * k] *= scl;
|
||||
skinnormal[3 * k + 1] *= scl;
|
||||
skinnormal[3 * k + 2] *= scl;
|
||||
}
|
||||
|
||||
float inflate = m->skin_inflate[i];
|
||||
for (int k = 0; k < vertnum; k++) {
|
||||
skinvert[3 * k] += inflate * skinnormal[3 * k];
|
||||
skinvert[3 * k + 1] += inflate * skinnormal[3 * k + 1];
|
||||
skinvert[3 * k + 2] += inflate * skinnormal[3 * k + 2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static PickResult PickSkin(const mjModel* m, const mjData* d,
|
||||
const mjtNum ray_pos[3], const mjtNum ray_dir[3],
|
||||
const mjvOption* vis_options) {
|
||||
PickResult result;
|
||||
if (!vis_options->flags[mjVIS_SKIN]) {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<float> vertex_buffer;
|
||||
std::vector<float> normal_buffer;
|
||||
|
||||
for (int i = 0; i < m->nskin; i++) {
|
||||
const int skin_group = mjMAX(0, mjMIN(mjNGROUP - 1, m->skin_group[i]));
|
||||
if (!vis_options->skingroup[skin_group]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vertex_buffer.resize(3 * m->skin_vertnum[i]);
|
||||
if (m->skin_inflate[i]) {
|
||||
normal_buffer.resize(3 * m->skin_vertnum[i]);
|
||||
}
|
||||
|
||||
float* skinvert = vertex_buffer.data();
|
||||
float* skinnormal = m->skin_inflate[i] ? normal_buffer.data() : nullptr;
|
||||
MakeSkin(m, d, vis_options, i, skinvert, skinnormal);
|
||||
|
||||
int vertid;
|
||||
mjtNum test_dist = mju_raySkin(m->skin_facenum[i], m->skin_vertnum[i],
|
||||
m->skin_face + 3 * m->skin_faceadr[i],
|
||||
skinvert, ray_pos, ray_dir, &vertid);
|
||||
if (test_dist < 0) {
|
||||
continue;
|
||||
} else if (result.dist >= 0 && test_dist >= result.dist) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.dist = test_dist;
|
||||
|
||||
// find body with largest weight for this vertex
|
||||
float best_weight = -1;
|
||||
for (int j = m->skin_boneadr[i];
|
||||
j < m->skin_boneadr[i] + m->skin_bonenum[i]; j++) {
|
||||
for (int k = m->skin_bonevertadr[j];
|
||||
k < m->skin_bonevertadr[j] + m->skin_bonevertnum[j]; k++) {
|
||||
// get vertex id and weight
|
||||
const int vertex_id = m->skin_bonevertid[k];
|
||||
const float vertex_weight = m->skin_bonevertweight[k];
|
||||
|
||||
// update if matching id and bigger weight
|
||||
if (vertex_id == vertid && vertex_weight > best_weight) {
|
||||
best_weight = vertex_weight;
|
||||
result.body = m->skin_bonebodyid[j];
|
||||
result.skin = i;
|
||||
mju_f2n(result.point, skinvert + 3 * vertid, 3);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
PickResult Pick(const mjModel* m, const mjData* d, const mjvCamera* camera,
|
||||
float x, float y, float aspect_ratio,
|
||||
const mjvOption* vis_options) {
|
||||
mjtNum ray_pos[3];
|
||||
mjtNum ray_dir[3];
|
||||
MakePickRay(ray_pos, ray_dir, m, d, camera, x, 1.0 - y, aspect_ratio);
|
||||
|
||||
PickResult results[3];
|
||||
results[0] = PickGeom(m, d, ray_pos, ray_dir, vis_options);
|
||||
results[1] = PickFlex(m, d, ray_pos, ray_dir, vis_options);
|
||||
results[2] = PickSkin(m, d, ray_pos, ray_dir, vis_options);
|
||||
|
||||
PickResult best_result;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (results[i].dist < 0) {
|
||||
continue;
|
||||
}
|
||||
if (best_result.dist < 0 || results[i].dist < best_result.dist) {
|
||||
best_result = results[i];
|
||||
}
|
||||
}
|
||||
return best_result;
|
||||
}
|
||||
|
||||
int SetCamera(const mjModel* m, mjvCamera* camera, int request_idx) {
|
||||
const int ncam = m ? m->ncam : 0;
|
||||
const int camera_idx = std::clamp(request_idx, kTumbleCameraIdx, ncam - 1);
|
||||
|
||||
if (camera_idx == kTumbleCameraIdx) {
|
||||
camera->type = mjCAMERA_FREE;
|
||||
camera->fixedcamid = -1;
|
||||
} else if (camera_idx == kFreeCameraIdx) {
|
||||
camera->type = mjCAMERA_FREE;
|
||||
camera->distance = 2.0f;
|
||||
camera->fixedcamid = -1;
|
||||
} else if (camera_idx == kTrackingCameraIdx) {
|
||||
if (camera->trackbodyid >= 0) {
|
||||
camera->type = mjCAMERA_TRACKING;
|
||||
} else {
|
||||
camera->type = mjCAMERA_FREE;
|
||||
}
|
||||
camera->fixedcamid = -1;
|
||||
} else {
|
||||
camera->type = mjCAMERA_FIXED;
|
||||
camera->fixedcamid = camera_idx;
|
||||
}
|
||||
|
||||
return camera_idx;
|
||||
}
|
||||
} // namespace mujoco::platform
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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_PLATFORM_INTERACTION_H_
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_INTERACTION_H_
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
// The result of a pick operation.
|
||||
struct PickResult {
|
||||
mjtNum point[3] = {0, 0, 0}; // World coordinates
|
||||
mjtNum dist = -1; // Distance from the camera.
|
||||
int body = -1;
|
||||
int geom = -1;
|
||||
int flex = -1;
|
||||
int skin = -1;
|
||||
};
|
||||
|
||||
// Returns information about the object (if any) under the mouse cursor.
|
||||
PickResult Pick(const mjModel* m, const mjData* d, const mjvCamera* camera,
|
||||
float x, float y, float aspect_ratio,
|
||||
const mjvOption* vis_options);
|
||||
|
||||
// Indices for cameras that are not defined in the model.
|
||||
static constexpr int kTumbleCameraIdx = -3;
|
||||
static constexpr int kFreeCameraIdx = -2;
|
||||
static constexpr int kTrackingCameraIdx = -1;
|
||||
|
||||
// Updates the camera according to the requested index.
|
||||
//
|
||||
// The function returns the new index of the camera which may differ from the
|
||||
// request if the request was invalid (e.g. request was out of range).
|
||||
int SetCamera(const mjModel* m, mjvCamera* camera, int request_idx);
|
||||
|
||||
// Camera motions are either relative to a target or the camera itself.
|
||||
//
|
||||
// We use the following camera nomenclature:
|
||||
// - Truck: moves the camera left/right along a horizontal plane.
|
||||
// - Pedestal: moves the camera up/down along a vertical plane.
|
||||
// - Dolly: moves the camera forward/backward along a horizontal plane.
|
||||
// - Pan: turns the camera left/right.
|
||||
// - Tilt: turns the camera upwards/downwards.
|
||||
// - Zoom: moves the camera closer to or away from the target. This is
|
||||
// different from dolly in that the movement is relative to the
|
||||
// target. (It's also not actually a camera zoom, which is an
|
||||
// action of lens of the camera, rather than the camera itself.)
|
||||
// - Orbit: moves the camera around the target.
|
||||
// - Planer: creates a horizontal or vertical plane based on the cameras
|
||||
// position and orientation, then moves the camera along that plane.
|
||||
enum class CameraMotion {
|
||||
ZOOM,
|
||||
ORBIT,
|
||||
TRUCK_PEDESTAL,
|
||||
TRUCK_DOLLY,
|
||||
PAN_TILT,
|
||||
PLANAR_MOVE_H,
|
||||
PLANAR_MOVE_V,
|
||||
};
|
||||
|
||||
void MoveCamera(const mjModel* m, const mjData* d, mjvCamera* cam,
|
||||
CameraMotion motion, mjtNum dx, mjtNum dy);
|
||||
|
||||
void InitPerturb(const mjModel* m, const mjData* d, const mjvCamera* cam,
|
||||
mjvPerturb* pert, mjtPertBit active);
|
||||
|
||||
void MovePerturb(const mjModel* m, const mjData* d, const mjvCamera* cam,
|
||||
mjvPerturb* pert, mjtMouse action, mjtNum reldx,
|
||||
mjtNum reldy);
|
||||
|
||||
} // namespace mujoco::platform
|
||||
|
||||
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_INTERACTION_H_
|
||||
@@ -0,0 +1,78 @@
|
||||
// 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/platform/renderer.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
#include "experimental/platform/helpers.h"
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
Renderer::Renderer(MakeContextFn make_context_fn)
|
||||
: make_context_fn_(make_context_fn) {
|
||||
}
|
||||
|
||||
Renderer::~Renderer() { Deinit(); }
|
||||
|
||||
void Renderer::Init(const mjModel* model) {
|
||||
Deinit();
|
||||
if (model) {
|
||||
mjr_defaultContext(&render_context_);
|
||||
make_context_fn_(model, &render_context_);
|
||||
mjv_defaultScene(&scene_);
|
||||
mjv_makeScene(model, &scene_, 2000);
|
||||
initialized_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::Deinit() {
|
||||
if (initialized_) {
|
||||
mjv_freeScene(&scene_);
|
||||
mjr_freeContext(&render_context_);
|
||||
initialized_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::Render(const mjModel* model, mjData* data,
|
||||
const mjvPerturb* perturb, mjvCamera* camera,
|
||||
const mjvOption* vis_option, int width, int height) {
|
||||
if (initialized_) {
|
||||
mjv_updateScene(model, data, vis_option, perturb, camera, mjCAT_ALL,
|
||||
&scene_);
|
||||
}
|
||||
|
||||
mjrRect main_viewport = {0, 0, width, height};
|
||||
mjr_render(main_viewport, data ? &scene_ : nullptr, &render_context_);
|
||||
|
||||
auto now = std::chrono::steady_clock::now();
|
||||
auto delta_time = now - last_fps_update_;
|
||||
const double interval = std::chrono::duration<double>(delta_time).count();
|
||||
|
||||
++frames_;
|
||||
if (interval > 0.2) { // only update FPS stat at most 5 times per second
|
||||
last_fps_update_ = now;
|
||||
fps_ = frames_ / interval;
|
||||
frames_ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::SaveScreenshot(const std::string& filename, int width,
|
||||
int height) {
|
||||
SaveScreenshotToWebp(width, height, &render_context_, filename);
|
||||
}
|
||||
|
||||
} // namespace mujoco::platform
|
||||
@@ -0,0 +1,81 @@
|
||||
// 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_PLATFORM_RENDERER_H_
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_RENDERER_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
// Renders the mujoco simulation and the imgui state into the active window
|
||||
// using the filament rendering backend.
|
||||
class Renderer {
|
||||
public:
|
||||
// Function that creates a mjrContext for the given model. We use a function
|
||||
// to allow different mjrContext implementations to be created without
|
||||
// requiring a direct dependency on them.
|
||||
using MakeContextFn = std::function<void(const mjModel* m, mjrContext* con)>;
|
||||
|
||||
explicit Renderer(MakeContextFn make_context_fn);
|
||||
~Renderer();
|
||||
|
||||
Renderer(const Renderer&) = delete;
|
||||
Renderer& operator=(const Renderer&) = delete;
|
||||
|
||||
// Initializes the renderer with the given mjModel.
|
||||
void Init(const mjModel* model);
|
||||
|
||||
// Renders the simulation state into the active window. Also renders the imgui
|
||||
// state, but that is obtained directly from the ImGui library.
|
||||
void Render(const mjModel* model, mjData* data, const mjvPerturb* perturb,
|
||||
mjvCamera* camera, const mjvOption* vis_option, int width,
|
||||
int height);
|
||||
|
||||
// Saves a screenshot of the simulation state into the given file.
|
||||
void SaveScreenshot(const std::string& filename, int width, int height);
|
||||
|
||||
// Rendering flags.
|
||||
mjtByte* GetRenderFlags() { return scene_.flags; }
|
||||
|
||||
// Returns the current, average frame rate.
|
||||
double GetFrameRate() const { return fps_; }
|
||||
|
||||
// Returns the render context.
|
||||
const mjrContext& GetContext() const { return render_context_; }
|
||||
|
||||
private:
|
||||
using TimePoint = std::chrono::time_point<std::chrono::steady_clock>;
|
||||
|
||||
// Resets the renderer; no rendering will occur until Init() is called again.
|
||||
void Deinit();
|
||||
|
||||
MakeContextFn make_context_fn_;
|
||||
mjrContext render_context_;
|
||||
mjvScene scene_;
|
||||
bool initialized_ = false;
|
||||
|
||||
int frames_ = 0;
|
||||
TimePoint last_fps_update_;
|
||||
double fps_ = 0;
|
||||
};
|
||||
|
||||
} // namespace mujoco::platform
|
||||
|
||||
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_RENDERER_H_
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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/platform/sim_history.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <span>
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
void SimHistory::Init(int state_size, int max_history, int max_bytes) {
|
||||
// History buffer will be smaller of number of states and total memory.
|
||||
const int state_bytes = state_size * sizeof(mjtNum);
|
||||
const int history_length = std::min(INT_MAX / state_bytes, max_history);
|
||||
const int history_bytes = std::min(state_bytes * history_length, max_bytes);
|
||||
|
||||
const int max_states = std::max(1, history_bytes / state_bytes);
|
||||
history_.resize(max_states);
|
||||
|
||||
for (State& state : history_) {
|
||||
state.resize(state_size, 0);
|
||||
}
|
||||
cursor_ = 0;
|
||||
offset_ = 0;
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
std::span<mjtNum> SimHistory::AddToHistory() {
|
||||
const int max_size = history_.size();
|
||||
if (offset_ != 0) {
|
||||
// offset will be a negative number between 1 - history_.size() and 0.
|
||||
size_ += offset_;
|
||||
cursor_ += offset_;
|
||||
if (cursor_ < 0) {
|
||||
cursor_ += max_size;
|
||||
}
|
||||
offset_ = 0;
|
||||
}
|
||||
|
||||
std::span<mjtNum> state;
|
||||
if (max_size > 0 && cursor_ < max_size) {
|
||||
state = history_[cursor_];
|
||||
cursor_ = (cursor_ + 1) % max_size;
|
||||
size_ = std::min(size_ + 1, max_size);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
std::span<mjtNum> SimHistory::SetIndex(int offset) {
|
||||
const int size = history_.size();
|
||||
if (size > 0) {
|
||||
offset_ = std::clamp<int>(offset, 1 - size, 0);
|
||||
}
|
||||
|
||||
if (history_.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const int actual_index =
|
||||
(cursor_ - 1 + offset_ + history_.size()) % history_.size();
|
||||
return history_[actual_index];
|
||||
}
|
||||
|
||||
} // namespace mujoco::platform
|
||||
@@ -0,0 +1,94 @@
|
||||
// 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_PLATFORM_SIM_HISTORY_H_
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_SIM_HISTORY_H_
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
// A historical buffer of simulation data.
|
||||
class SimHistory {
|
||||
public:
|
||||
SimHistory() = default;
|
||||
|
||||
// Simulation data is stored a an array of mjtNum; see mj_getState().
|
||||
using State = std::vector<mjtNum>;
|
||||
|
||||
// Clears and initializes the history buffer to store state.
|
||||
void Init(int state_size, int max_history = 2000, int max_bytes = 1e8);
|
||||
|
||||
// Adds an uninitialized state to the history and returns a reference to it
|
||||
// so that the caller can populate the data. Also resets the current index to
|
||||
// 0; see SetIndex() for details.
|
||||
std::span<mjtNum> AddToHistory();
|
||||
|
||||
// Returns the history at the given index (i.e. the number of steps) in the
|
||||
// past. The `offset` will be clamped internally to the range [0, Size() - 1].
|
||||
// This function returns the valid, clamped value.
|
||||
//
|
||||
// For example, calling SetIndex(0) will return the most recently recorded
|
||||
// state. Calling SetIndex(-N) will return the state from N steps ago.
|
||||
//
|
||||
// Note that future calls to `AddToHistory` will begin recording from the
|
||||
// newly set index, effectively creating a new "branch" of the history buffer.
|
||||
// If you do not want to lose any states, you must call `SetIndex(0)` before
|
||||
// before resuming playback.
|
||||
//
|
||||
// For example, consider you have recorded 6 states:
|
||||
// N(-5) N(-4) N(-3) N(-2) N(-1) N(0)
|
||||
// -----------------------------------------^
|
||||
//
|
||||
// You then call `SetIndex(-3)`:
|
||||
// N(-5) N(-4) N(-3) N(-2) N(-1) N(0)
|
||||
// ------------------^
|
||||
//
|
||||
// And then call `AddToHistory()
|
||||
// N(-5) N(-4) N(-3) N(-2) N(-1) N(0)
|
||||
// | | | x x x
|
||||
// v v v
|
||||
// N'(-3) N'(-2) N'(-1) N'(0)
|
||||
// -------------------------^
|
||||
//
|
||||
// In this case, we effectively erase states 0 to -3 of the previous history,
|
||||
// "copy" the older states into the new branch, and add the most recent state
|
||||
// at the "head" of the history buffer.
|
||||
std::span<mjtNum> SetIndex(int offset);
|
||||
|
||||
// Returns the currently set index.
|
||||
int GetIndex() const { return offset_; }
|
||||
|
||||
// Returns the number of states in the history buffer.
|
||||
int Size() const { return size_; }
|
||||
|
||||
private:
|
||||
// The history of states.
|
||||
std::vector<State> history_;
|
||||
|
||||
// The index at which the next AddToHistory() call will write.
|
||||
int cursor_ = 0;
|
||||
|
||||
// The most recently requested offset from SetIndex().
|
||||
int offset_ = 0;
|
||||
|
||||
// The total number of states available in the history buffer.
|
||||
int size_ = 0;
|
||||
};
|
||||
|
||||
} // namespace mujoco::platform
|
||||
|
||||
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_SIM_HISTORY_H_
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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/platform/sim_profiler.h"
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
#include <imgui.h>
|
||||
#include <implot.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
SimProfiler::SimProfiler() {
|
||||
Clear();
|
||||
}
|
||||
|
||||
void SimProfiler::Clear() {
|
||||
constexpr int kProfilerMaxFrames = 200;
|
||||
cpu_total_.clear();
|
||||
cpu_collision_.clear();
|
||||
cpu_prepare_.clear();
|
||||
cpu_solve_.clear();
|
||||
cpu_other_.clear();
|
||||
dim_dof_.clear();
|
||||
dim_body_.clear();
|
||||
dim_constraint_.clear();
|
||||
dim_sqrt_nnz_.clear();
|
||||
dim_contact_.clear();
|
||||
dim_iteration_.clear();
|
||||
|
||||
cpu_total_.resize(kProfilerMaxFrames, 0);
|
||||
cpu_collision_.resize(kProfilerMaxFrames, 0);
|
||||
cpu_prepare_.resize(kProfilerMaxFrames, 0);
|
||||
cpu_solve_.resize(kProfilerMaxFrames, 0);
|
||||
cpu_other_.resize(kProfilerMaxFrames, 0);
|
||||
dim_dof_.resize(kProfilerMaxFrames, 0);
|
||||
dim_body_.resize(kProfilerMaxFrames, 0);
|
||||
dim_constraint_.resize(kProfilerMaxFrames, 0);
|
||||
dim_sqrt_nnz_.resize(kProfilerMaxFrames, 0);
|
||||
dim_contact_.resize(kProfilerMaxFrames, 0);
|
||||
dim_iteration_.resize(kProfilerMaxFrames, 0);
|
||||
}
|
||||
|
||||
void SimProfiler::Update(const mjModel* model, const mjData* data) {
|
||||
// CPU timers.
|
||||
mjtNum total = data->timer[mjTIMER_STEP].duration;
|
||||
mjtNum number = static_cast<mjtNum>(data->timer[mjTIMER_STEP].number);
|
||||
if (number == 0.0) {
|
||||
total = data->timer[mjTIMER_FORWARD].duration;
|
||||
number = static_cast<mjtNum>(data->timer[mjTIMER_FORWARD].number);
|
||||
}
|
||||
if (number == 0.0) {
|
||||
// This can happen if the simulation is paused.
|
||||
return;
|
||||
}
|
||||
|
||||
cpu_total_.erase(cpu_total_.begin());
|
||||
cpu_total_.push_back(total / number);
|
||||
|
||||
mjtNum collision = data->timer[mjTIMER_POS_COLLISION].duration / number;
|
||||
cpu_collision_.erase(cpu_collision_.begin());
|
||||
cpu_collision_.push_back(collision);
|
||||
|
||||
mjtNum prepare = (data->timer[mjTIMER_POS_MAKE].duration / number) +
|
||||
(data->timer[mjTIMER_POS_PROJECT].duration / number);
|
||||
cpu_prepare_.erase(cpu_prepare_.begin());
|
||||
cpu_prepare_.push_back(prepare);
|
||||
|
||||
mjtNum solve = data->timer[mjTIMER_CONSTRAINT].duration / number;
|
||||
cpu_solve_.erase(cpu_solve_.begin());
|
||||
cpu_solve_.push_back(solve);
|
||||
|
||||
mjtNum other = total - collision - prepare - solve;
|
||||
cpu_other_.erase(cpu_other_.begin());
|
||||
cpu_other_.push_back(other);
|
||||
|
||||
// Solver diagnostics.
|
||||
mjtNum sqrt_nnz = 0;
|
||||
int solver_niter = 0;
|
||||
const int nisland = data->nefc ? mjMAX(1, mjMIN(data->nisland, mjNISLAND)) : 0;
|
||||
for (int island=0; island < nisland; island++) {
|
||||
sqrt_nnz += data->solver_nnz[island];
|
||||
solver_niter += data->solver_niter[island];
|
||||
}
|
||||
sqrt_nnz = mju_sqrt(sqrt_nnz);
|
||||
|
||||
dim_dof_.erase(dim_dof_.begin());
|
||||
int nv = (model->opt.enableflags & mjENBL_SLEEP) ? data->nv_awake
|
||||
: model->nv;
|
||||
dim_dof_.push_back(nv);
|
||||
|
||||
dim_body_.erase(dim_body_.begin());
|
||||
int nbody = (model->opt.enableflags & mjENBL_SLEEP) ? data->nbody_awake
|
||||
: model->nbody;
|
||||
dim_body_.push_back(nbody);
|
||||
|
||||
dim_constraint_.erase(dim_constraint_.begin());
|
||||
dim_constraint_.push_back(data->nefc);
|
||||
|
||||
dim_sqrt_nnz_.erase(dim_sqrt_nnz_.begin());
|
||||
dim_sqrt_nnz_.push_back(sqrt_nnz);
|
||||
|
||||
dim_contact_.erase(dim_contact_.begin());
|
||||
dim_contact_.push_back(data->ncon);
|
||||
|
||||
dim_iteration_.erase(dim_iteration_.begin());
|
||||
dim_iteration_.push_back(static_cast<float>(solver_niter) / nisland);
|
||||
}
|
||||
|
||||
|
||||
void SimProfiler::CpuTimeGraph() {
|
||||
if (ImPlot::BeginPlot("CPU Time", ImVec2(-1, 0))) {
|
||||
ImPlot::SetupAxis(ImAxis_X1, "frame", ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxis(ImAxis_Y1, "msec", ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.2f");
|
||||
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
|
||||
ImPlot::SetupFinish();
|
||||
|
||||
ImPlot::PlotLine("total", cpu_total_.data(), cpu_total_.size(), 1,
|
||||
-(int)cpu_total_.size());
|
||||
ImPlot::PlotLine("prepare", cpu_prepare_.data(), cpu_prepare_.size(), 1,
|
||||
-(int)cpu_prepare_.size());
|
||||
ImPlot::PlotLine("solve", cpu_solve_.data(), cpu_solve_.size(), 1,
|
||||
-(int)cpu_solve_.size());
|
||||
ImPlot::PlotLine("collision", cpu_collision_.data(), cpu_collision_.size(),
|
||||
1, -(int)cpu_collision_.size());
|
||||
ImPlot::PlotLine("other", cpu_other_.data(), cpu_other_.size(), 1,
|
||||
-(int)cpu_other_.size());
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
}
|
||||
|
||||
void SimProfiler::DimensionsGraph() {
|
||||
if (ImPlot::BeginPlot("Dimensions", ImVec2(-1, 0))) {
|
||||
ImPlot::SetupAxis(ImAxis_X1, "frame", ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxis(ImAxis_Y1, "count", ImPlotAxisFlags_AutoFit);
|
||||
ImPlot::SetupAxisFormat(ImAxis_Y1, "%.0f");
|
||||
ImPlot::SetupLegend(ImPlotLocation_NorthEast);
|
||||
ImPlot::SetupFinish();
|
||||
|
||||
ImPlot::PlotLine("dof", dim_dof_.data(), dim_dof_.size(), 1,
|
||||
-(int)dim_dof_.size());
|
||||
ImPlot::PlotLine("body", dim_body_.data(), dim_body_.size(), 1,
|
||||
-(int)dim_body_.size());
|
||||
ImPlot::PlotLine("constraint", dim_constraint_.data(),
|
||||
dim_constraint_.size(), 1, -(int)dim_constraint_.size());
|
||||
ImPlot::PlotLine("sqrt(nnz)", dim_sqrt_nnz_.data(), dim_sqrt_nnz_.size(), 1,
|
||||
-(int)dim_sqrt_nnz_.size());
|
||||
ImPlot::PlotLine("contact", dim_contact_.data(), dim_contact_.size(), 1,
|
||||
-(int)dim_contact_.size());
|
||||
ImPlot::PlotLine("iteration", dim_iteration_.data(), dim_iteration_.size(),
|
||||
1, -(int)dim_iteration_.size());
|
||||
ImPlot::EndPlot();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mujoco::platform
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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_PLATFORM_SIM_PROFILER_H_
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_SIM_PROFILER_H_
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
// Collects and displays profiling data for MuJoCo simulations.
|
||||
class SimProfiler {
|
||||
public:
|
||||
SimProfiler();
|
||||
|
||||
// Clears all captured profiling data.
|
||||
void Clear();
|
||||
|
||||
// Updates the profiling data with the latest simulation data.
|
||||
void Update(const mjModel* model, const mjData* data);
|
||||
|
||||
// Displays the profiling data using ImPlot.
|
||||
void CpuTimeGraph();
|
||||
void DimensionsGraph();
|
||||
|
||||
private:
|
||||
std::vector<float> cpu_total_;
|
||||
std::vector<float> cpu_collision_;
|
||||
std::vector<float> cpu_prepare_;
|
||||
std::vector<float> cpu_solve_;
|
||||
std::vector<float> cpu_other_;
|
||||
std::vector<float> dim_dof_;
|
||||
std::vector<float> dim_body_;
|
||||
std::vector<float> dim_constraint_;
|
||||
std::vector<float> dim_sqrt_nnz_;
|
||||
std::vector<float> dim_contact_;
|
||||
std::vector<float> dim_iteration_;
|
||||
};
|
||||
|
||||
} // namespace mujoco::platform
|
||||
|
||||
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_SIM_PROFILER_H_
|
||||
@@ -0,0 +1,209 @@
|
||||
// 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/platform/step_control.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <ratio>
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
static mjtNum Timer() {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Milliseconds = std::chrono::duration<double, std::milli>;
|
||||
static Clock::time_point start = Clock::now();
|
||||
return Milliseconds(Clock::now() - start).count();
|
||||
}
|
||||
|
||||
StepControl::StepControl() {
|
||||
mjcb_time = Timer;
|
||||
}
|
||||
|
||||
float StepControl::GetSpeedMeasured() const {
|
||||
return speed_measured_;
|
||||
}
|
||||
|
||||
float StepControl::GetSpeed() const {
|
||||
return speed_;
|
||||
}
|
||||
|
||||
void StepControl::SetSpeed(float speed_percent_real_time) {
|
||||
speed_ = std::clamp(speed_percent_real_time, .1f, 100.f);
|
||||
ForceSync();
|
||||
}
|
||||
|
||||
void StepControl::ForceSync() { force_sync_ = true; }
|
||||
|
||||
void StepControl::GetNoiseParameters(float& ctrl_noise_scale,
|
||||
float& ctrl_noise_rate) const {
|
||||
ctrl_noise_scale = ctrl_noise_std_;
|
||||
ctrl_noise_rate = ctrl_noise_rate_;
|
||||
}
|
||||
|
||||
void StepControl::SetNoiseParameters(float ctrl_noise_scale,
|
||||
float ctrl_noise_rate) {
|
||||
ctrl_noise_std_ = ctrl_noise_scale;
|
||||
ctrl_noise_rate_ = ctrl_noise_rate;
|
||||
}
|
||||
|
||||
StepControl::Status StepControl::Advance(const mjModel* m, mjData* d) {
|
||||
if (!m) {
|
||||
return Status::kOk;
|
||||
}
|
||||
|
||||
if (paused_) {
|
||||
// When we eventually unpause, we need to make sure we sync to immediately
|
||||
// and step once. Without this we could step many times before rendering
|
||||
// resulting in a noticeable delay before the simulation restarts
|
||||
// (especially for large slowdowns).
|
||||
force_sync_ = true;
|
||||
|
||||
if (!single_step_) {
|
||||
// Run mj_forward to update rendering and joint sliders.
|
||||
mj_forward(m, d);
|
||||
if (pause_update_) {
|
||||
mju_copy(d->qacc_warmstart, d->qacc, m->nv);
|
||||
}
|
||||
return Status::kPaused;
|
||||
}
|
||||
single_step_ = false;
|
||||
}
|
||||
|
||||
const Clock::time_point start_cpu = Clock::now();
|
||||
const double slowdown = 100. / std::clamp<double>(speed_, 0.001, 100.);
|
||||
double elapsed_cpu = Seconds(start_cpu - sync_cpu_).count();
|
||||
double elapsed_sim = d->time - sync_sim_;
|
||||
|
||||
bool resync = false;
|
||||
|
||||
// Resync if we're forced to.
|
||||
if (force_sync_) {
|
||||
force_sync_ = false;
|
||||
resync = true;
|
||||
}
|
||||
|
||||
// Resync if we've never synced.
|
||||
if (sync_cpu_.time_since_epoch().count() == 0) {
|
||||
resync = true;
|
||||
}
|
||||
|
||||
// Resync if any elapsed time is negative.
|
||||
if (elapsed_cpu < 0 || elapsed_sim < 0) {
|
||||
resync = true;
|
||||
}
|
||||
|
||||
// Resync if the distance from the target simulation time is bigger than
|
||||
// sync_misalign_ (misalignment condition).
|
||||
if (std::abs(elapsed_cpu / slowdown - elapsed_sim) > sync_misalign_) {
|
||||
resync = true;
|
||||
}
|
||||
|
||||
if (resync) {
|
||||
// Reset sync times.
|
||||
sync_cpu_ = start_cpu;
|
||||
sync_sim_ = d->time;
|
||||
}
|
||||
|
||||
// Stepping loop.
|
||||
while (true) {
|
||||
const Clock::time_point now_cpu = Clock::now();
|
||||
elapsed_cpu = Seconds(now_cpu - sync_cpu_).count();
|
||||
elapsed_sim = d->time - sync_sim_;
|
||||
|
||||
// Stop stepping if simulation no longer lags cpu.
|
||||
if (elapsed_sim * slowdown >= elapsed_cpu) {
|
||||
return Status::kOk;
|
||||
}
|
||||
|
||||
// Stop stepping if simulation is taking too long to catch up.
|
||||
// Note: 12ms == 70% of 1/60 seconds/frame.
|
||||
constexpr Clock::duration kMaxCpuTimeForSim = std::chrono::milliseconds(12);
|
||||
if (now_cpu - start_cpu >= kMaxCpuTimeForSim) {
|
||||
// Note: GetSpeed() and GetSpeedMeasured() will be different in this case.
|
||||
return Status::kOk;
|
||||
}
|
||||
|
||||
// Measure slowdown here in first viable in-sync step. This update location
|
||||
// is chosen to minimize visual noise caused by changing measurements.
|
||||
if (elapsed_sim > 0) {
|
||||
double measured_slowdown = elapsed_cpu / elapsed_sim;
|
||||
speed_measured_ = 100. / measured_slowdown;
|
||||
}
|
||||
|
||||
mjtNum prev_time = d->time;
|
||||
InjectNoise(m, d);
|
||||
mj_step(m, d);
|
||||
|
||||
if (mjDISABLED(mjDSBL_AUTORESET)) {
|
||||
for (mjtWarning w : kDivergedWarnings) {
|
||||
if (d->warning[w].number > 0) {
|
||||
// Stop stepping if the simulation diverged.
|
||||
paused_ = true;
|
||||
return Status::kDiverged;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Stop stepping if we auto reset.
|
||||
if (d->time < prev_time) {
|
||||
return Status::kAutoReset;
|
||||
}
|
||||
}
|
||||
|
||||
// Stop after one step if we resynced; next iteration will deal with timing.
|
||||
if (resync) {
|
||||
return Status::kOk;
|
||||
}
|
||||
}
|
||||
|
||||
return Status::kDiverged; // Unreachable
|
||||
}
|
||||
|
||||
void StepControl::InjectNoise(const mjModel* m, mjData* d) {
|
||||
// no noise, return
|
||||
if (ctrl_noise_std_ <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// convert rate and scale to discrete time (Ornstein–Uhlenbeck)
|
||||
mjtNum rate = mju_exp(-m->opt.timestep / ctrl_noise_rate_);
|
||||
mjtNum scale = ctrl_noise_std_ * mju_sqrt(1-rate*rate);
|
||||
|
||||
for (int i = 0; i < m->nu; i++) {
|
||||
mjtNum bottom = 0;
|
||||
mjtNum top = 0;
|
||||
mjtNum midpoint = 0;
|
||||
mjtNum halfrange = 1;
|
||||
if (m->actuator_ctrllimited[i]) {
|
||||
bottom = m->actuator_ctrlrange[2*i];
|
||||
top = m->actuator_ctrlrange[2*i+1];
|
||||
midpoint = 0.5 * (top + bottom); // target of exponential decay
|
||||
halfrange = 0.5 * (top - bottom); // scales noise
|
||||
}
|
||||
|
||||
// exponential convergence to midpoint at ctrl_noise_rate
|
||||
d->ctrl[i] = rate * d->ctrl[i] + (1-rate) * midpoint;
|
||||
|
||||
// add noise
|
||||
d->ctrl[i] += scale * halfrange * mju_standardNormal(nullptr);
|
||||
|
||||
// clip to range if limited
|
||||
if (m->actuator_ctrllimited[i]) {
|
||||
d->ctrl[i] = mju_clip(d->ctrl[i], bottom, top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mujoco::platform
|
||||
@@ -0,0 +1,122 @@
|
||||
// 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_PLATFORM_STEP_CONTROL_H_
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_STEP_CONTROL_H_
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
using Seconds = std::chrono::duration<double>;
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
// State and logic for physics synchronization and stepping.
|
||||
class StepControl {
|
||||
public:
|
||||
StepControl();
|
||||
|
||||
enum class Status {
|
||||
kOk,
|
||||
|
||||
// Simulation was not stepped because it is paused.
|
||||
kPaused,
|
||||
|
||||
// Simulation diverged with autoreset enabled.
|
||||
kAutoReset,
|
||||
|
||||
// Simulation diverged with autoreset disabled.
|
||||
// Note: Consider reporting mjData warning diagnostics in kDivergedWarnings.
|
||||
kDiverged,
|
||||
};
|
||||
|
||||
// List of warnings that are checked to determine simulation divergence.
|
||||
static constexpr mjtWarning kDivergedWarnings[] = {
|
||||
mjWARN_BADQACC, mjWARN_BADQVEL, mjWARN_BADQPOS};
|
||||
|
||||
// Steps physics forward, respecting speed settings and refresh budget.
|
||||
Status Advance(const mjModel* m, mjData* d);
|
||||
|
||||
// Ensures the next call to Advance() will synchronize time and step once.
|
||||
void ForceSync();
|
||||
|
||||
// Gets/sets the desired simulation speed as a percentage of real time.
|
||||
float GetSpeed() const;
|
||||
float GetSpeedMeasured() const;
|
||||
void SetSpeed(float speed); // speed is clamped to [0.1%, 100%]
|
||||
|
||||
// Gets/sets the control noise parameters applied before stepping.
|
||||
void GetNoiseParameters(float& noise_scale, float& noise_rate) const;
|
||||
void SetNoiseParameters(float noise_scale, float noise_rate);
|
||||
|
||||
// Returns true if the simulation is paused.
|
||||
bool IsPaused() { return paused_; }
|
||||
|
||||
// Pauses/unpauses the simulation.
|
||||
void Pause() { paused_ = true; }
|
||||
void Unpause() { paused_ = false; }
|
||||
void TogglePause() { paused_ = !paused_; }
|
||||
|
||||
// If the simulation is paused, will perform a single step on the next
|
||||
// Advance() call.
|
||||
void RequestSingleStep() { single_step_ = true; }
|
||||
|
||||
private:
|
||||
std::string AdvanceOneStep(const mjModel* m, mjData* d);
|
||||
|
||||
void InjectNoise(const mjModel* m, mjData* d);
|
||||
|
||||
// Control noise standard deviation
|
||||
double ctrl_noise_std_ = 0;
|
||||
|
||||
// Control noise correlation rate
|
||||
double ctrl_noise_rate_ = 0;
|
||||
|
||||
// Desired simulation speed as a percentage of real time
|
||||
float speed_ = 100;
|
||||
|
||||
// Measured simulation speed as a percentage of real time
|
||||
float speed_measured_ = -1;
|
||||
|
||||
// If true, the next call to Advance() will synchronize time step once.
|
||||
bool force_sync_ = true;
|
||||
|
||||
// CPU time (aka wall time) of the last synchronization event
|
||||
std::chrono::time_point<Clock> sync_cpu_;
|
||||
|
||||
// Simulation time of the last synchronization event
|
||||
mjtNum sync_sim_ = 0;
|
||||
|
||||
// Maximum mis-alignment before re-sync (simulation seconds)
|
||||
double sync_misalign_ = .1;
|
||||
|
||||
// Whether or not the simulation is paused.
|
||||
bool paused_ = false;
|
||||
|
||||
// Perform only a single step on the next call to Advance() if the simulation
|
||||
// is paused.
|
||||
bool single_step_ = false;
|
||||
|
||||
// If true and paused, d->qacc_warmstart is set to d->qacc after mj_forward
|
||||
// which has the effect of making the constraint solver eventually converge
|
||||
// while the simulation is paused.
|
||||
bool pause_update_ = false;
|
||||
};
|
||||
|
||||
} // namespace mujoco::platform
|
||||
|
||||
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_STEP_CONTROL_H_
|
||||
@@ -0,0 +1,188 @@
|
||||
// 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/platform/window.h"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL.h>
|
||||
#include <SDL_error.h>
|
||||
#include <SDL_events.h>
|
||||
#include <SDL_hints.h>
|
||||
#include <SDL_syswm.h>
|
||||
#include <SDL_version.h>
|
||||
#include <SDL_video.h>
|
||||
#include <backends/imgui_impl_sdl2.h>
|
||||
#include <imgui.h>
|
||||
#include "experimental/platform/helpers.h"
|
||||
#include <mujoco/mujoco.h>
|
||||
|
||||
// Because X11/Xlib.h defines Status.
|
||||
#ifdef Status
|
||||
#undef Status
|
||||
#endif
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
static void InitImGui(SDL_Window* window, const LoadAssetFn& load_asset_fn,
|
||||
bool build_fonts) {
|
||||
ImGui::CreateContext();
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
io.BackendFlags |= ImGuiBackendFlags_RendererHasTextures;
|
||||
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
|
||||
io.IniFilename = nullptr;
|
||||
ImGui::StyleColorsDark();
|
||||
ImGui_ImplSDL2_InitForOther(window);
|
||||
|
||||
#ifndef __EMSCRIPTEN__
|
||||
// TODO: Get font loading working for wasm.
|
||||
// Note: fonts are stored statically because they aren't actually loaded
|
||||
// until the fonts are built.
|
||||
ImFontConfig main_cfg;
|
||||
static auto main_font = load_asset_fn("OpenSans-Regular.ttf");
|
||||
io.Fonts->AddFontFromMemoryTTF(main_font.data(), main_font.size(), 20.f,
|
||||
&main_cfg);
|
||||
|
||||
ImFontConfig icon_cfg;
|
||||
icon_cfg.MergeMode = true;
|
||||
static auto icon_font = load_asset_fn("fontawesome-webfont.ttf");
|
||||
constexpr ImWchar icon_ranges[] = {0xf000, 0xf3ff, 0x000};
|
||||
io.Fonts->AddFontFromMemoryTTF(icon_font.data(), icon_font.size(), 14.f,
|
||||
&icon_cfg, icon_ranges);
|
||||
#endif
|
||||
|
||||
if (build_fonts) {
|
||||
io.Fonts->Build();
|
||||
}
|
||||
}
|
||||
|
||||
Window::Window(std::string_view title, int width, int height, Config config,
|
||||
const LoadAssetFn& load_asset_fn)
|
||||
: width_(width), height_(height), config_(config) {
|
||||
SDL_SetHint(SDL_HINT_FRAMEBUFFER_ACCELERATION, "1");
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
|
||||
SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 16);
|
||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) != 0) {
|
||||
mju_error("Error initializing SDL: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
int window_flags = SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||
|
||||
if (!config_.enable_keyboard) {
|
||||
SDL_EventState(SDL_TEXTINPUT, SDL_DISABLE);
|
||||
SDL_EventState(SDL_KEYDOWN, SDL_DISABLE);
|
||||
SDL_EventState(SDL_KEYUP, SDL_DISABLE);
|
||||
}
|
||||
|
||||
RenderConfig render_config = config_.render_config;
|
||||
if (render_config == kFilamentVulkan) {
|
||||
window_flags |= SDL_WINDOW_VULKAN;
|
||||
} else if (render_config == kFilamentWebGL) {
|
||||
window_flags |= SDL_WINDOW_OPENGL;
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
|
||||
} else if (render_config == kClassicOpenGL || render_config == kFilamentOpenGL) {
|
||||
window_flags |= SDL_WINDOW_OPENGL;
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 0);
|
||||
} else {
|
||||
mju_error("Unsupported window config: %d", render_config);
|
||||
}
|
||||
|
||||
sdl_window_ =
|
||||
SDL_CreateWindow(title.data(), SDL_WINDOWPOS_UNDEFINED,
|
||||
SDL_WINDOWPOS_UNDEFINED, width, height, window_flags);
|
||||
if (!sdl_window_) {
|
||||
mju_error("Error creating window: %s", SDL_GetError());
|
||||
}
|
||||
|
||||
InitImGui(sdl_window_, load_asset_fn, (render_config != kClassicOpenGL));
|
||||
|
||||
if (render_config == kFilamentWebGL || render_config == kClassicOpenGL) {
|
||||
SDL_GLContext gl_context = SDL_GL_CreateContext(sdl_window_);
|
||||
SDL_GL_MakeCurrent(sdl_window_, gl_context);
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
SDL_SysWMinfo wmi;
|
||||
SDL_VERSION(&wmi.version);
|
||||
SDL_GetWindowWMInfo(sdl_window_, &wmi);
|
||||
native_window_ = reinterpret_cast<void*>(wmi.info.x11.window);
|
||||
#endif
|
||||
}
|
||||
|
||||
Window::~Window() {
|
||||
SDL_DestroyWindow(sdl_window_);
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
void Window::SetTitle(std::string_view title) {
|
||||
SDL_SetWindowTitle(sdl_window_, title.data());
|
||||
}
|
||||
|
||||
std::string Window::GetDropFile() {
|
||||
std::string tmp;
|
||||
std::swap(tmp, drop_file_);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
Window::Status Window::NewFrame() {
|
||||
SDL_Event event;
|
||||
while (SDL_PollEvent(&event)) {
|
||||
ImGui_ImplSDL2_ProcessEvent(&event);
|
||||
|
||||
if (event.type == SDL_QUIT) {
|
||||
should_exit_ = true;
|
||||
} else if (event.type == SDL_APP_WILLENTERBACKGROUND) {
|
||||
should_exit_ = true;
|
||||
} else if (event.type == SDL_WINDOWEVENT) {
|
||||
if (event.window.event == SDL_WINDOWEVENT_RESIZED) {
|
||||
SDL_GetWindowSize(sdl_window_, &width_, &height_);
|
||||
int drawable_width = width_;
|
||||
int drawable_height = height_;
|
||||
SDL_GL_GetDrawableSize(sdl_window_, &drawable_width, &drawable_height);
|
||||
scale_ = (float)drawable_width / (float)width_;
|
||||
}
|
||||
} else if (event.type == SDL_DROPFILE) {
|
||||
drop_file_ = event.drop.file;
|
||||
}
|
||||
}
|
||||
|
||||
ImGui_ImplSDL2_NewFrame();
|
||||
ImGui::NewFrame();
|
||||
|
||||
return should_exit_ ? kQuitting : kRunning;
|
||||
}
|
||||
|
||||
void Window::EndFrame() {
|
||||
// We use ImGui for input management in addition to GUI rendering so its
|
||||
// important to call ImGui::EndFrame even if we don't call ImGui::Render.
|
||||
// Note ImGui::Render internally calls ImGui::EndFrame, but so long as
|
||||
// ImGui::NewFrame has been called, ImGui::EndFrame may be called multiple
|
||||
// times; it will be a no-op.
|
||||
ImGui::EndFrame();
|
||||
}
|
||||
|
||||
void Window::Present() {
|
||||
// Filament (with the exception of WebGL) handles the swapchain internally.
|
||||
if (config_.render_config != kFilamentVulkan
|
||||
&& config_.render_config != kFilamentOpenGL) {
|
||||
SDL_GL_SwapWindow(sdl_window_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace mujoco::platform
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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_PLATFORM_WINDOW_H_
|
||||
#define MUJOCO_SRC_EXPERIMENTAL_PLATFORM_WINDOW_H_
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "experimental/platform/helpers.h"
|
||||
#include <SDL_video.h>
|
||||
|
||||
namespace mujoco::platform {
|
||||
|
||||
// Platform-independent window abstraction using SDL2.
|
||||
//
|
||||
// Initializes the SDL2 and ImGui libraries, creates/owns the native window, and
|
||||
// handles events from the window.
|
||||
class Window {
|
||||
public:
|
||||
// Configures the window for the specified rendering backend.
|
||||
enum RenderConfig {
|
||||
kClassicOpenGL,
|
||||
kFilamentVulkan,
|
||||
kFilamentOpenGL,
|
||||
kFilamentWebGL,
|
||||
};
|
||||
|
||||
struct Config {
|
||||
RenderConfig render_config = kClassicOpenGL;
|
||||
bool enable_keyboard = true;
|
||||
};
|
||||
|
||||
Window(std::string_view title, int width, int height, Config config,
|
||||
const LoadAssetFn& load_asset_fn);
|
||||
~Window();
|
||||
|
||||
Window(const Window&) = delete;
|
||||
Window& operator=(const Window&) = delete;
|
||||
|
||||
// The status of the window.
|
||||
enum Status {
|
||||
kRunning,
|
||||
kQuitting,
|
||||
};
|
||||
|
||||
// Processes all pendings window events and prepares ImGui for input handling
|
||||
// and GUI rendering. Returns the status of the window.
|
||||
Status NewFrame();
|
||||
|
||||
// Finalizes ImGui input handling. Must call NewFrame first.
|
||||
void EndFrame();
|
||||
|
||||
// Swaps and presents the window buffer.
|
||||
void Present();
|
||||
|
||||
// Sets the title of the window.
|
||||
void SetTitle(std::string_view title);
|
||||
|
||||
// Returns information related to the current size of the window.
|
||||
int GetWidth() const { return width_; }
|
||||
int GetHeight() const { return height_; }
|
||||
float GetScale() const { return scale_; }
|
||||
float GetAspectRatio() const {
|
||||
return height_ > 0
|
||||
? static_cast<float>(width_) / static_cast<float>(height_)
|
||||
: 1.0f;
|
||||
}
|
||||
|
||||
// Returns the path to a file that was dropped on the window. Once called,
|
||||
// the value will be cleared until the next time a file is dropped.
|
||||
std::string GetDropFile();
|
||||
|
||||
// Returns the handle to the underlying native window.
|
||||
void* GetNativeWindowHandle() { return native_window_; }
|
||||
|
||||
private:
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
float scale_ = 1.0f;
|
||||
Config config_;
|
||||
void* native_window_ = nullptr;
|
||||
SDL_Window* sdl_window_ = nullptr;
|
||||
bool should_exit_ = false;
|
||||
std::string drop_file_;
|
||||
};
|
||||
|
||||
} // namespace mujoco::platform
|
||||
|
||||
#endif // MUJOCO_SRC_EXPERIMENTAL_PLATFORM_WINDOW_H_
|
||||
Reference in New Issue
Block a user