Rename toolbox to platform.

PiperOrigin-RevId: 838701951
Change-Id: I339eafd35d919710cb47bb04e9a8e29f92739513
This commit is contained in:
Haroon Qureshi
2025-12-01 03:57:37 -08:00
committed by Copybara-Service
parent c6b587b515
commit 139a5b6494
24 changed files with 194 additions and 195 deletions
+60
View File
@@ -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})
+807
View File
@@ -0,0 +1,807 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "experimental/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
+88
View File
@@ -0,0 +1,88 @@
// Copyright 2025 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef MUJOCO_SRC_EXPERIMENTAL_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_
+222
View File
@@ -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
+66
View File
@@ -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
+228
View File
@@ -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_
+627
View File
@@ -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
+85
View File
@@ -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_
+78
View File
@@ -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
+81
View File
@@ -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_
+77
View File
@@ -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
+94
View File
@@ -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_
+167
View File
@@ -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
+55
View File
@@ -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_
+209
View File
@@ -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
+122
View File
@@ -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_
+188
View File
@@ -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
+101
View File
@@ -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_