Add support for user-defined figures in the MuJoCo viewer.

PiperOrigin-RevId: 726983516
Change-Id: Ifb821f3cafb163d4f3077d76b0668037b4bc5a9c
This commit is contained in:
Google DeepMind
2025-02-14 10:44:50 -08:00
committed by Copybara-Service
parent 6f0bd0a5f8
commit 37d7591ce0
5 changed files with 72 additions and 19 deletions
+2
View File
@@ -25,6 +25,8 @@ Feature promotion
General General
^^^^^^^ ^^^^^^^
- Add support for custom plots in the MuJoCo viewer by exposing a ``viewport`` property, a ``set_figures`` method,
and a ``clear_figures`` method.
- Separate collision and deformation meshes for :ref:`flex<deformable-flex>`. This enables a fixed cost for the soft - Separate collision and deformation meshes for :ref:`flex<deformable-flex>`. This enables a fixed cost for the soft
body computations, while preserving the fidelity of high-resolution collisions. body computations, while preserving the fidelity of high-resolution collisions.
- Added :ref:`mjs_setDeepCopy` API function. When the deep copy flag is 0, attaching a model will not copy it to the - Added :ref:`mjs_setDeepCopy` API function. When the deep copy flag is 0, attaching a model will not copy it to the
+43 -19
View File
@@ -28,6 +28,7 @@
#include <pybind11/gil.h> #include <pybind11/gil.h>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
#include <pybind11/pytypes.h> #include <pybind11/pytypes.h>
#include <pybind11/stl.h>
namespace mujoco::python { namespace mujoco::python {
namespace { namespace {
@@ -69,11 +70,10 @@ class UIAdapterWithPyCallback : public Adapter {
class SimulateWrapper { class SimulateWrapper {
public: public:
SimulateWrapper(std::unique_ptr<PlatformUIAdapter> platform_ui_adapter, SimulateWrapper(std::unique_ptr<PlatformUIAdapter> platform_ui_adapter,
py::object cam, py::object opt, py::object cam, py::object opt, py::object pert,
py::object pert, py::object user_scn, bool is_passive) py::object user_scn, bool is_passive)
: simulate_(new mujoco::Simulate( : simulate_(new mujoco::Simulate(
std::move(platform_ui_adapter), std::move(platform_ui_adapter), cam.cast<MjvCameraWrapper&>().get(),
cam.cast<MjvCameraWrapper&>().get(),
opt.cast<MjvOptionWrapper&>().get(), opt.cast<MjvOptionWrapper&>().get(),
pert.cast<MjvPerturbWrapper&>().get(), is_passive)), pert.cast<MjvPerturbWrapper&>().get(), is_passive)),
m_(py::none()), m_(py::none()),
@@ -126,6 +126,28 @@ class SimulateWrapper {
py::object GetModel() const { return m_; } py::object GetModel() const { return m_; }
py::object GetData() const { return d_; } py::object GetData() const { return d_; }
mjrRect GetViewport() const {
// Return the viewport corresponding to the 3D view, i.e. the viewer window
// without the UI elements.
return simulate_->uistate.rect[3];
}
void SetFigures(
const std::vector<std::pair<mjrRect, py::object>>& viewports_figures) {
// Pairs of [viewport, figure], where viewport corresponds to the location
// of the figure on the viewer window.
std::vector<std::pair<mjrRect, mjvFigure>> user_figures;
for (const auto& [viewport, figure] : viewports_figures) {
mjvFigure casted_figure = *figure.cast<MjvFigureWrapper&>().get();
user_figures.push_back(std::make_pair(viewport, casted_figure));
}
// Set them all at once to prevent figure flickering.
simulate_->user_figures_ = user_figures;
}
void ClearFigures() { simulate_->user_figures_.clear(); }
private: private:
mujoco::Simulate* simulate_; mujoco::Simulate* simulate_;
std::atomic_int destroyed_ = 0; std::atomic_int destroyed_ = 0;
@@ -173,14 +195,14 @@ inline auto CallIfNotNull(void (mujoco::Simulate::*func)(Args...)) {
} }
template <typename T> template <typename T>
inline auto GetIfNotNull(T mujoco::Simulate::*member) { inline auto GetIfNotNull(T mujoco::Simulate::* member) {
return [member](SimulateWrapper& wrapper) -> T& { return [member](SimulateWrapper& wrapper) -> T& {
return SimulateRefOrThrow(wrapper).*member; return SimulateRefOrThrow(wrapper).*member;
}; };
} }
template <typename T, typename... Args> template <typename T, typename... Args>
inline auto SetIfNotNull(T mujoco::Simulate::*member) { inline auto SetIfNotNull(T mujoco::Simulate::* member) {
return [member](SimulateWrapper& wrapper, const T& value) -> void { return [member](SimulateWrapper& wrapper, const T& value) -> void {
SimulateRefOrThrow(wrapper).*member = value; SimulateRefOrThrow(wrapper).*member = value;
}; };
@@ -205,8 +227,7 @@ PYBIND11_MODULE(_simulate, pymodule) {
py::object key_callback) { py::object key_callback) {
bool is_passive = !run_physics_thread; bool is_passive = !run_physics_thread;
return std::make_unique<SimulateWrapper>( return std::make_unique<SimulateWrapper>(
std::make_unique<UIAdapterWithPyCallback<UIAdapter>>( std::make_unique<UIAdapterWithPyCallback<UIAdapter>>(key_callback),
key_callback),
scn, cam, opt, pert, is_passive); scn, cam, opt, pert, is_passive);
})) }))
.def("destroy", &SimulateWrapper::Destroy) .def("destroy", &SimulateWrapper::Destroy)
@@ -225,8 +246,12 @@ PYBIND11_MODULE(_simulate, pymodule) {
.def("lock", GetIfNotNull(&mujoco::Simulate::mtx), .def("lock", GetIfNotNull(&mujoco::Simulate::mtx),
py::call_guard<py::gil_scoped_release>(), py::call_guard<py::gil_scoped_release>(),
py::return_value_policy::reference_internal) py::return_value_policy::reference_internal)
.def("set_figures", &SimulateWrapper::SetFigures,
py::arg("viewports_figures"))
.def("clear_figures", &SimulateWrapper::ClearFigures)
.def_property_readonly("m", &SimulateWrapper::GetModel) .def_property_readonly("m", &SimulateWrapper::GetModel)
.def_property_readonly("d", &SimulateWrapper::GetData) .def_property_readonly("d", &SimulateWrapper::GetData)
.def_property_readonly("viewport", &SimulateWrapper::GetViewport)
.def_property_readonly("ctrl_noise_std", .def_property_readonly("ctrl_noise_std",
GetIfNotNull(&mujoco::Simulate::ctrl_noise_std), GetIfNotNull(&mujoco::Simulate::ctrl_noise_std),
py::call_guard<py::gil_scoped_release>()) py::call_guard<py::gil_scoped_release>())
@@ -260,18 +285,17 @@ PYBIND11_MODULE(_simulate, pymodule) {
return sim.exitrequest.load(); return sim.exitrequest.load();
}), }),
py::call_guard<py::gil_scoped_release>()) py::call_guard<py::gil_scoped_release>())
.def( .def("exit",
"exit", [](SimulateWrapper& wrapper) {
[](SimulateWrapper& wrapper) { mujoco::Simulate* sim = wrapper.simulate();
mujoco::Simulate* sim = wrapper.simulate(); if (!sim) {
if (!sim) { return;
return; }
}
int value = 0; int value = 0;
sim->exitrequest.compare_exchange_strong(value, 1); sim->exitrequest.compare_exchange_strong(value, 1);
wrapper.WaitUntilExit(); wrapper.WaitUntilExit();
}) })
.def_property_readonly("uiloadrequest", .def_property_readonly("uiloadrequest",
CallIfNotNull(+[](mujoco::Simulate& sim) { CallIfNotNull(+[](mujoco::Simulate& sim) {
+17
View File
@@ -108,6 +108,23 @@ class Handle:
return sim.d return sim.d
return None return None
@property
def viewport(self):
sim = self._sim()
if sim is not None:
return sim.viewport
return None
def set_figures(self, viewports_figures):
sim = self._sim()
if sim is not None:
sim.set_figures(viewports_figures)
def clear_figures(self):
sim = self._sim()
if sim is not None:
sim.clear_figures()
def close(self): def close(self):
sim = self._sim() sim = self._sim()
if sim is not None: if sim is not None:
+9
View File
@@ -542,6 +542,10 @@ void ShowSensor(mj::Simulate* sim, mjrRect rect) {
mjr_figure(viewport, &sim->figsensor, &sim->platform_ui->mjr_context()); mjr_figure(viewport, &sim->figsensor, &sim->platform_ui->mjr_context());
} }
void ShowFigure(mj::Simulate* sim, mjrRect viewport, mjvFigure* fig){
mjr_figure(viewport, fig, &sim->platform_ui->mjr_context());
}
// load state from history buffer // load state from history buffer
static void LoadScrubState(mj::Simulate* sim) { static void LoadScrubState(mj::Simulate* sim) {
// get index into circular buffer // get index into circular buffer
@@ -2588,6 +2592,11 @@ void Simulate::Render() {
} }
} }
// user figures
for (auto& [viewport, figure] : this->user_figures_) {
ShowFigure(this, viewport, &figure);
}
// finalize // finalize
this->platform_ui->SwapBuffers(); this->platform_ui->SwapBuffers();
} }
+1
View File
@@ -252,6 +252,7 @@ class Simulate {
// additional user-defined visualization geoms (used in passive mode) // additional user-defined visualization geoms (used in passive mode)
mjvScene* user_scn = nullptr; mjvScene* user_scn = nullptr;
mjtByte user_scn_flags_prev_[mjNRNDFLAG]; mjtByte user_scn_flags_prev_[mjNRNDFLAG];
std::vector<std::pair<mjrRect, mjvFigure>> user_figures_;
// OpenGL rendering and UI // OpenGL rendering and UI
int refresh_rate = 60; int refresh_rate = 60;