mujoco_rs/
viewer.rs

1use glfw::{Action, Context, Glfw, GlfwReceiver, Key, Modifiers, MouseButton, PWindow, WindowEvent};
2use bitflags::bitflags;
3
4use std::time::Instant;
5
6use crate::{get_mujoco_version, mujoco_c::*};
7
8#[cfg(feature = "cpp-viewer")]
9use std::ffi::CString;
10
11use crate::prelude::{MjrContext, MjrRectangle, MjtFont, MjtGridPos};
12use crate::wrappers::mj_visualization::*;
13use crate::wrappers::mj_model::MjModel;
14use crate::wrappers::mj_data::MjData;
15
16/****************************************** */
17// Rust native viewer
18/****************************************** */
19const MJ_VIEWER_DEFAULT_SIZE_PX: (u32, u32) = (1280, 720);
20/// How much extra room to create in the [`MjvScene`]. Useful for drawing labels, etc.
21const MJ_VIEWER_EXTRA_SCENE_GEOM_SPACE: usize = 100;
22const DOUBLE_CLICK_WINDOW_MS: u128 = 250;
23
24const HELP_MENU_TITLES: &str = concat!(
25    "Toggle help\n",
26    "Free camera\n",
27    "Track camera\n",
28    "Camera orbit\n",
29    "Camera pan\n",
30    "Camera look at\n",
31    "Zoom\n",
32    "Object select\n",
33    "Selection rotate\n",
34    "Selection translate\n",
35    "Exit"
36);
37
38const HELP_MENU_VALUES: &str = concat!(
39    "F1\n",
40    "Escape\n",
41    "Control + Alt + double-left click\n",
42    "Left drag\n",
43    "Right [+Shift] drag\n",
44    "Alt + double-left click\n",
45    "Zoom, middle drag\n",
46    "Double-left click\n",
47    "Control + [Shift] + drag\n",
48    "Control + Alt + [Shift] + drag\n",
49    "Control + Q"
50);
51
52#[derive(Debug)]
53pub enum MjViewerError {
54    GlfwInitError (glfw::InitError),
55    WindowCreationError
56}
57
58/// A Rust-native implementation of the MuJoCo viewer. To confirm to rust safety rules,
59/// the viewer doesn't store a mutable reference to the [`MjData`] struct, but it instead
60/// accepts it as a parameter at its methods.
61/// 
62/// Currently supported (to be expanded in the future):
63/// - Visualization of the 3D scene,
64/// - Close via Ctrl + Q or by closing the window,
65/// - Body tracking via Ctrl + Alt + double left click,
66/// - Camera look at object via Alt + double left click,
67/// - Escape from tracked camera via Esc.
68/// - Perturbations:
69///     - Select the object of interested by double clicking on it,
70///     - Hold down Control and start dragging for **rotational** perturbations,
71///     - Hold down Control+Alt and start dragging for **translational** perturbations,
72///     - To move in the XY plane instead of the default XZ plane, hold Shift.
73/// 
74/// The [`MjViewer::sync`] method must be called to sync the state of [`MjViewer`] and [`MjData`].
75/// 
76/// # Safety
77/// Due to the nature of OpenGL, this should only be run in the **main thread**.
78#[derive(Debug)]
79pub struct MjViewer<'m> {
80    /* MuJoCo rendering */
81    scene: MjvScene<'m>,
82    context: MjrContext,
83    camera: MjvCamera,
84
85    /* Other MuJoCo related */
86    model: &'m MjModel,
87    pert: MjvPerturb,
88
89    /* Internal state */
90    last_x: mjtNum,
91    last_y: mjtNum,
92    last_bnt_press_time: Instant,
93    rect_view: MjrRectangle,
94    rect_full: MjrRectangle,
95    status_flags: ViewerStatusBits,  // Status flag indicating the state of the menu
96
97    /* OpenGL */
98    glfw: Glfw,
99    window: PWindow,
100    events: GlfwReceiver<(f64, WindowEvent)>,
101
102    /* External interaction */
103    user_scn: MjvScene<'m>
104}
105
106impl<'m> MjViewer<'m> {
107    /// Launches the MuJoCo viewer. A [`Result`] struct is returned that either contains
108    /// [`MjViewer`] or a [`MjViewerError`]. The `scene_max_geom` parameter
109    /// defines how much space will be allocated for additional, user-defined visual-only geoms.
110    /// It can thus be set to 0 if no additional geoms will be drawn by the user.
111    pub fn launch_passive(model: &'m MjModel, scene_max_geom: usize) -> Result<Self, MjViewerError> {
112        let mut glfw = glfw::init_no_callbacks()
113            .map_err(|err| MjViewerError::GlfwInitError(err))?;
114
115        let (w, h) = MJ_VIEWER_DEFAULT_SIZE_PX;
116        let title = format!("MuJoCo Rust Viewer (MuJoCo {})", get_mujoco_version());
117
118        let (mut window, events) = match glfw.create_window(
119            w, h, &title,
120            glfw::WindowMode::Windowed
121        ) {
122            Some(x) => Ok(x),
123            None => Err(MjViewerError::WindowCreationError)
124        }?;
125
126        /* Initialize the OpenGL related things */
127        window.make_current();
128        window.set_all_polling(true);
129        glfw.set_swap_interval(glfw::SwapInterval::None);
130
131        let scene = MjvScene::new(model, model.ffi().ngeom as usize + scene_max_geom + MJ_VIEWER_EXTRA_SCENE_GEOM_SPACE);
132        let user_scn = MjvScene::new(model, scene_max_geom);
133        let context= MjrContext::new(model);
134        let camera = MjvCamera::new(0, MjtCamera::mjCAMERA_FREE, model);
135        let pert = MjvPerturb::default();
136        Ok(Self {
137            scene,
138            context,
139            camera,
140            model,
141            pert,
142            glfw,
143            window,
144            events,
145            user_scn,
146            last_x: 0.0,
147            last_y: 0.0,
148            status_flags: ViewerStatusBits::HELP_MENU,
149            last_bnt_press_time: Instant::now(),
150            rect_view: MjrRectangle::default(),
151            rect_full: MjrRectangle::default(),
152        })
153    }
154
155    /// Checks whether the window is still open.
156    pub fn running(&self) -> bool {
157        !self.window.should_close()
158    }
159
160    /// Returns an immutable reference to a user scene for drawing custom visual-only geoms.
161    pub fn user_scn(&self) -> &MjvScene<'m>{
162        &self.user_scn
163    }
164
165    /// Returns a mutable reference to a user scene for drawing custom visual-only geoms.
166    pub fn user_scn_mut(&mut self) -> &mut MjvScene<'m>{
167        &mut self.user_scn
168    }
169
170    /// Syncs the state of `data` with the viewer as well as perform
171    /// rendering on the viewer.
172    pub fn sync(&mut self, data: &mut MjData) {
173        /* Process mouse and keyboard events */
174        self.process_events(data);
175
176        /* Update the scene from data and render */
177        self.update_scene(data);
178
179        /* Update the user menu state and overlays */
180        self.update_menus();
181
182        /* Display the drawn content */
183        self.window.swap_buffers();
184
185        /* Apply perturbations */
186        self.pert.apply(self.model, data);
187    }
188
189    /// Updates the scene and draws it to the display.
190    fn update_scene(&mut self, data: &mut MjData) {
191        /* Read the screen size */
192        self.update_rectangles(self.window.get_framebuffer_size());
193
194        /* Update the scene from the MjData state */
195        let opt = MjvOption::default();
196        self.scene.update(data, &opt, &self.pert, &mut self.camera);
197
198        /* Draw user scene geoms */
199        let ffi = unsafe { self.scene.ffi_mut() };
200        assert!(
201            ffi.ngeom + self.user_scn.ffi().ngeom <= ffi.maxgeom,
202            "not enough space available in the internal scene; this is a bug, please report it."
203        );
204
205        /* Fast copy */
206        unsafe { std::ptr::copy_nonoverlapping(
207            self.user_scn.ffi_mut().geoms,
208            ffi.geoms.add(ffi.ngeom as usize),
209            self.user_scn.ffi().ngeom as usize
210        ) };
211        ffi.ngeom += self.user_scn.ffi().ngeom;
212        self.scene.render(&self.rect_full, &self.context);
213    }
214
215    /// Draws the user menu
216    fn update_menus(&mut self) {
217        let mut rectangle = self.rect_view;
218        rectangle.width = rectangle.width - rectangle.width / 4;
219
220        /* Overlay section */
221
222        if self.status_flags.contains(ViewerStatusBits::HELP_MENU) {  // Help
223            self.context.overlay(
224                MjtFont::mjFONT_NORMAL, MjtGridPos::mjGRID_TOPLEFT,
225                rectangle,
226                HELP_MENU_TITLES,
227                Some(HELP_MENU_VALUES)
228            );
229        }
230    }
231
232    /// Updates the dimensions of the rectangles defining the dimensions of
233    /// the user interface, as well as the actual scene viewer.
234    fn update_rectangles(&mut self, viewport_size: (i32, i32)) {
235        // The scene (middle) rectangle
236        self.rect_view.width = viewport_size.0;
237        self.rect_view.height = viewport_size.1;
238
239        self.rect_full.width = viewport_size.0;
240        self.rect_full.height = viewport_size.1;
241    }
242
243    /// Processes user input events.
244    fn process_events(&mut self, data: &mut MjData) {
245        self.glfw.poll_events();
246        while let Some((_, event)) = self.events.receive() {
247            match event {
248                WindowEvent::Key(Key::Q, _, Action::Press, Modifiers::Control) => {
249                    self.window.set_should_close(true);
250                    break;  // no use in polling other events
251                },
252                WindowEvent::Key(Key::Escape, _, Action::Press, _) => {
253                    self.camera.free();
254                },
255
256                WindowEvent::Key(Key::F1, _, Action::Press, _) => {
257                    self.status_flags.toggle(ViewerStatusBits::HELP_MENU);
258                }
259
260                WindowEvent::Scroll(_, change) => {
261                    self.process_scroll(change);
262                }
263                WindowEvent::CursorPos(x, y) => {
264                    self.process_cursor_pos(x, y, data);
265                },
266
267                // Match left button presses
268                WindowEvent::MouseButton(MouseButton::Left, action, modifiers) => {
269                    self.process_left_click(data, &action, &modifiers);
270                }
271                _ => {}  // ignore other events
272            }
273        }
274    }
275
276    /// Processes scrolling events.
277    fn process_scroll(&mut self, change: f64) {
278        self.camera.move_(MjtMouse::mjMOUSE_ZOOM, self.model, 0.0, -0.05 * change, &self.scene);
279    }
280
281    /// Processes camera and perturbation movements.
282    fn process_cursor_pos(&mut self, x: f64, y: f64, data: &mut MjData) {
283        /* Calculate the change in mouse position since last call */
284        let dx = x - self.last_x;
285        let dy = y - self.last_y;
286        self.last_x = x;
287        self.last_y = y;
288
289        /* Check mouse presses and move the camera if any of them is pressed */
290        let action;
291        let height = self.window.get_size().1 as mjtNum;
292
293        let shift = self.window.get_key(Key::LeftShift) == Action::Press;
294
295        if self.status_flags.contains(ViewerStatusBits::LEFT_CLICK) {
296            if self.pert.active == MjtPertBit::mjPERT_TRANSLATE as i32 {
297                action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
298            }
299            else {
300                action = if shift {MjtMouse::mjMOUSE_ROTATE_H} else {MjtMouse::mjMOUSE_ROTATE_V};
301            }
302        }
303        else if self.window.get_mouse_button(MouseButton::Right) == Action::Press {
304            action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
305        }
306        else if self.window.get_mouse_button(MouseButton::Middle) == Action::Press {
307            action = MjtMouse::mjMOUSE_ZOOM;
308        }
309        else {
310            return;  // If buttons aren't pressed, ignore.
311        }
312
313        /* When the perturbation isn't active, move the camera */
314        if self.pert.active == 0 {
315            self.camera.move_(action, self.model, dx / height, dy / height, &self.scene);
316        }
317        else {  // When the perturbation is active, move apply the perturbation.
318            self.pert.move_(self.model, data, action, dx / height, dy / height, &self.scene);
319        }
320    }
321
322    /// Processes left clicks and double left clicks.
323    fn process_left_click(&mut self, data: &mut MjData, action: &Action, modifiers: &Modifiers) {
324        match action {
325            Action::Press => {
326                /* Clicking and holding applies perturbation */
327                if self.pert.select > 0 && modifiers.contains(Modifiers::Control) {
328                    let type_ = if modifiers.contains(Modifiers::Alt) {
329                        MjtPertBit::mjPERT_TRANSLATE
330                    } else {
331                        MjtPertBit::mjPERT_ROTATE
332                    };
333                    self.pert.start(type_, &self.model, data, &self.scene);
334                }
335
336                /* Double click detection */
337                if !self.status_flags.contains(ViewerStatusBits::LEFT_CLICK) && self.last_bnt_press_time.elapsed().as_millis() < DOUBLE_CLICK_WINDOW_MS {
338                    let (mut x, mut y) = self.window.get_cursor_pos();
339
340                    /* Fix the coordinates */
341                    let buffer_ratio = self.window.get_framebuffer_size().0 as mjtNum / self.window.get_size().0 as mjtNum;
342                    x *= buffer_ratio;
343                    y *= buffer_ratio;
344                    y = self.rect_full.height as mjtNum - y;  // match OpenGL's coordinate system.
345
346                    /* Obtain the selection */ 
347                    let rect: &mjrRect_ = &self.rect_view;
348                    let (body_id, _, flex_id, skin_id, xyz) = self.scene.find_selection(
349                        data, &MjvOption::default(),
350                        rect.width as mjtNum / rect.height as mjtNum,
351                        (x - rect.left as mjtNum) / rect.width as mjtNum,
352                        (y - rect.bottom as mjtNum) / rect.height as mjtNum
353                    );
354
355                    /* Set tracking camera */
356                    if modifiers.contains(Modifiers::Alt) {
357                        if body_id >= 0 {
358                            self.camera.lookat = xyz;
359                            if modifiers.contains(Modifiers::Control) {
360                                self.camera.track(body_id as u32);
361                            }
362                        }
363                    }
364                    else {
365                        /* Mark selection */
366                        if body_id >= 0 {
367                            self.pert.select = body_id;
368                            self.pert.flexselect = flex_id;
369                            self.pert.skinselect = skin_id;
370                            self.pert.active = 0;
371                            self.pert.update_local_pos(xyz, data);
372                        }
373                        else {
374                            self.pert.select = 0;
375                            self.pert.flexselect = -1;
376                            self.pert.skinselect = -1;
377                        }
378                    }
379                }
380                self.last_bnt_press_time = Instant::now();
381                self.status_flags.set(ViewerStatusBits::LEFT_CLICK, true);
382            },
383            Action::Release => {
384                // Clear perturbation when left click is released.
385                self.pert.active = 0;
386                self.status_flags.remove(ViewerStatusBits::LEFT_CLICK);
387            },
388            Action::Repeat => {}
389        };
390    }
391}
392
393
394
395bitflags! {
396    /// Boolean flags that define some of
397    /// the Viewer's internal state.
398    #[derive(Debug)]
399    struct ViewerStatusBits: u8 {
400        const LEFT_CLICK = 1 << 0;
401        const HELP_MENU  = 1 << 1;
402    }
403}
404
405/****************************************** */
406// C++ viewer wrapper
407/****************************************** */
408/// Wrapper around the C++ implementation of MujoCo viewer
409#[cfg(feature = "cpp-viewer")]
410pub struct MjViewerCpp<'m> {
411    sim: *mut mujoco_Simulate,
412    running: bool,
413
414    // Store these here since the C++ bindings save references to them.
415    // We don't actually need them ourselves, at least not here.
416    _cam: Box<MjvCamera>,
417    _opt: Box<MjvOption>,
418    _pert: Box<MjvPerturb>,
419    _user_scn: Box<MjvScene<'m>>,
420    _glfw: glfw::Glfw
421}
422
423#[cfg(feature = "cpp-viewer")]
424impl<'m> MjViewerCpp<'m> {
425    #[inline]
426    pub fn running(&self) -> bool {
427        self.running
428    }
429
430    #[inline]
431    pub fn user_scn_mut(&mut self) -> &mut MjvScene<'m> {
432        &mut self._user_scn
433    }
434
435    pub fn launch_passive(model: &'m MjModel, data: &MjData, scene_max_ngeom: usize) -> Self {
436        let mut _glfw = glfw::init(glfw::fail_on_errors).unwrap();
437
438        // Allocate on the heap as the data must not be moved due to C++ bindings
439        let mut _cam = Box::new(MjvCamera::default());
440        let mut _opt: Box<MjvOption> = Box::new(MjvOption::default());
441        let mut _pert = Box::new(MjvPerturb::default());
442        let mut _user_scn = Box::new(MjvScene::new(&model, scene_max_ngeom));
443        let sim;
444        let c_filename = CString::new("file.xml").unwrap();
445        unsafe {
446            sim = new_simulate(&mut *_cam, &mut *_opt, &mut *_pert, _user_scn.ffi_mut(), true);
447            (*sim).RenderInit();
448            (*sim).Load(model.__raw(), data.__raw(), c_filename.as_ptr());
449            (*sim).RenderStep(true);
450        }
451
452        Self {sim, running: true, _cam, _opt, _pert, _glfw, _user_scn}
453    }
454
455    /// Returns the underlying C++ binding object of the viewer.
456    pub fn __raw(&self) -> *mut mujoco_Simulate {
457        self.sim
458    }
459
460    /// Renders the simulation.
461    /// `update_timer` flag species whether the time should be updated
462    /// inside the viewer (for vsync purposes).
463    /// # SAFETY
464    /// This needs to be called periodically from the MAIN thread, otherwise
465    /// GLFW stops working.
466    pub fn render(&mut self, update_timer: bool) {
467        unsafe {
468            assert!(self.running, "render called after viewer has been closed!");
469            self.running = (*self.sim).RenderStep(update_timer);
470        }
471    }
472
473    pub fn sync(&mut self) {
474        unsafe {
475            (*self.sim).Sync(false);
476        }
477    }
478}
479
480#[cfg(feature = "cpp-viewer")]
481impl Drop for MjViewerCpp<'_> {
482    fn drop(&mut self) {
483        unsafe {
484            (*self.sim).RenderCleanup();
485            free_simulate(self.sim);
486        }
487    }
488}