mujoco_rs/
viewer.rs

1use glfw::{Action, Context, Glfw, GlfwReceiver, Key, Modifiers, MouseButton, PWindow, WindowEvent};
2
3use std::time::Instant;
4
5use crate::mujoco_c::*;
6
7#[cfg(feature = "cpp-viewer")]
8use std::ffi::CString;
9
10use crate::prelude::{MjrContext, MjrRectangle};
11use crate::wrappers::mj_visualization::*;
12use crate::wrappers::mj_model::MjModel;
13use crate::wrappers::mj_data::MjData;
14
15/****************************************** */
16// Rust native viewer
17/****************************************** */
18const MJ_VIEWER_DEFAULT_SIZE_PX: (u32, u32) = (1280, 720);
19const MJ_VIEWER_DEFAULT_TITLE: &str = "MuJoCo Viewer (Rust)";
20const DOUBLE_CLICK_WINDOW_MS: u128 = 250;
21
22
23#[derive(Debug)]
24pub enum MjViewerError {
25    GlfwInitError (glfw::InitError),
26    WindowCreationError
27}
28
29/// A Rust-native implementation of the MuJoCo viewer. To confirm to rust safety rules,
30/// the viewer doesn't store a mutable reference to the [`MjData`] struct, but it instead
31/// accepts it as a parameter at its methods.
32/// 
33/// Currently supported (to be expanded in the future):
34/// - Visualization of the 3D scene,
35/// - Close via Ctrl + Q or by closing the window,
36/// - Body tracking via Ctrl + double left-click,
37/// - Escape from tracked camera via Esc.
38/// 
39/// Only passive mode is available, which means the user must call [`MjViewer::sync`]
40/// to update the state inside the viewer.
41#[derive(Debug)]
42pub struct MjViewer<'m> {
43    /* MuJoCo rendering */
44    scene: MjvScene<'m>,
45    context: MjrContext,
46    camera: MjvCamera,
47
48    /* Other MuJoCo related */
49    model: &'m MjModel,
50    pert: MjvPerturb,
51
52    /* Internal state */
53    last_x: mjtNum,
54    last_y: mjtNum,
55    left_click: bool,
56    last_bnt_press_time: Instant,
57    rect_view: MjrRectangle,
58    rect_full: MjrRectangle,
59
60    /* OpenGL */
61    glfw: Glfw,
62    window: PWindow,
63    events: GlfwReceiver<(f64, WindowEvent)>
64}
65
66impl<'m> MjViewer<'m> {
67    /// Launches the MuJoCo viewer. A [`Result`] struct is returned that either contains
68    /// [`MjViewer`] or a [`MjViewerError`].
69    pub fn launch_passive(model: &'m MjModel, scene_max_ngeom: usize) -> Result<Self, MjViewerError> {
70        let mut glfw = glfw::init_no_callbacks()
71            .map_err(|err| MjViewerError::GlfwInitError(err))?;
72        let (w, h) = MJ_VIEWER_DEFAULT_SIZE_PX;
73        let (mut window, events) = match glfw.create_window(
74            w, h, MJ_VIEWER_DEFAULT_TITLE, glfw::WindowMode::Windowed
75        ) {
76            Some(x) => Ok(x),
77            None => Err(MjViewerError::WindowCreationError)
78        }?;
79
80        /* Initialize the OpenGL related things */
81        window.make_current();
82        window.set_all_polling(true);
83        glfw.set_swap_interval(glfw::SwapInterval::None);
84
85        let scene = MjvScene::new(model, scene_max_ngeom);
86        let context= MjrContext::new(model);
87        let camera = MjvCamera::new(0, MjtCamera::mjCAMERA_FREE, model);
88        let pert = MjvPerturb::default();
89        Ok(Self {
90            scene,
91            context,
92            camera,
93            model,
94            pert,
95            glfw,
96            window,
97            events,
98            last_x: 0.0,
99            last_y: 0.0,
100            left_click: false,
101            last_bnt_press_time: Instant::now(),
102            rect_view: MjrRectangle::default(),
103            rect_full: MjrRectangle::default(),
104        })
105    }
106
107    /// Checks whether the window is still open.
108    pub fn running(&self) -> bool {
109        !self.window.should_close()
110    }
111
112    pub fn sync(&mut self, data: &mut MjData) {
113        self.process_events(data);
114        self.update(data);
115    }
116
117
118    /// Updates the screen state
119    fn update(&mut self, data: &mut MjData) {
120        /* Read the screen size */
121        let mut viewport = MjrRectangle::default();
122        let (width, height) = self.window.get_framebuffer_size();
123        viewport.width = width;
124        viewport.height = height;
125
126        self.update_rectangles((width, height));
127
128        /* Update the scene from the MjData state */
129        let opt = MjvOption::default();
130        self.scene.update(data, &opt, &self.pert, &mut self.camera);
131        self.scene.render(&viewport, &self.context);
132
133        /* Display the changes */
134        self.window.swap_buffers();
135    }
136
137    /// Updates the dimensions of the rectangles defining the dimensions of
138    /// the user interface, as well as the actual scene viewer.
139    fn update_rectangles(&mut self, viewport_size: (i32, i32)) {
140        // The scene (middle) rectangle
141        self.rect_view.width = viewport_size.0;
142        self.rect_view.height = viewport_size.1;
143
144        self.rect_full.width = viewport_size.0;
145        self.rect_full.height = viewport_size.1;
146    }
147
148    /// Processes user input events
149    fn process_events(&mut self, data: &mut MjData) {
150        self.glfw.poll_events();
151        while let Some((_, event)) = self.events.receive() {
152            match event {
153                WindowEvent::Key(Key::Q, _, _, modifier) if modifier == Modifiers::Control => {
154                    self.window.set_should_close(true);
155                    break;  // no use in polling other events
156                },
157                WindowEvent::Key(Key::Escape, _, _, _) => {
158                    self.camera.free();
159                },
160                WindowEvent::Scroll(_, change) => {
161                    self.process_scroll(change);
162                }
163                WindowEvent::CursorPos(x, y) => {
164                    self.process_cursor_pos(x, y);
165                },
166
167                // Match left button presses
168                WindowEvent::MouseButton(MouseButton::Left, action, modifiers) => {
169                    self.process_left_click(data, &action, &modifiers);
170                }
171                _ => {}  // ignore other events
172            }
173        }
174    }
175
176    fn process_scroll(&mut self, change: f64) {
177        self.camera.move_(MjtMouse::mjMOUSE_ZOOM, self.model, 0.0, -0.05 * change, &self.scene);
178    }
179
180    fn process_cursor_pos(&mut self, x: f64, y: f64) {
181        /* Calculate the change in mouse position since last call */
182        let dx = x - self.last_x;
183        let dy = y - self.last_y;
184        self.last_x = x;
185        self.last_y = y;
186
187        /* Check mouse presses and move the camera if any of them is pressed */
188        let action;
189        let shift = self.window.get_key(Key::LeftShift) == Action::Press;
190
191        if self.window.get_mouse_button(MouseButton::Left) == Action::Press {
192            action = if shift {MjtMouse::mjMOUSE_ROTATE_H} else {MjtMouse::mjMOUSE_ROTATE_V};
193        }
194        else if self.window.get_mouse_button(MouseButton::Right) == Action::Press {
195            action = if shift {MjtMouse::mjMOUSE_MOVE_H} else {MjtMouse::mjMOUSE_MOVE_V};
196        }
197        else if self.window.get_mouse_button(MouseButton::Middle) == Action::Press {
198            action = MjtMouse::mjMOUSE_ZOOM;
199        }
200        else {
201            return;  // If buttons aren't pressed, ignore.
202        }
203
204        let height = self.window.get_size().1 as mjtNum;
205        self.camera.move_(action, self.model, dx / height, dy / height, &self.scene);
206    }
207
208    fn process_left_click(&mut self, data: &mut MjData, action: &Action, modifiers: &Modifiers) {
209        self.left_click = match action {
210            Action::Press => {
211                /* Double click detection */
212                if !self.left_click && self.last_bnt_press_time.elapsed().as_millis() < DOUBLE_CLICK_WINDOW_MS {
213                    let (mut x, mut y) = self.window.get_cursor_pos();
214
215                    /* Fix the coordinates */
216                    let buffer_ratio = self.window.get_framebuffer_size().0 as mjtNum / self.window.get_size().0 as mjtNum;
217                    x *= buffer_ratio;
218                    y *= buffer_ratio;
219                    y = self.rect_full.height as mjtNum - y;  // match OpenGL's coordinate system.
220
221                    /* Obtain the selection */ 
222                    let rect: &mjrRect_ = &self.rect_view;
223                    let (body_id, _, flex_id, skin_id, xyz) = self.scene.find_selection(
224                        data, &MjvOption::default(),
225                        rect.width as mjtNum / rect.height as mjtNum,
226                        (x - rect.left as mjtNum) / rect.width as mjtNum,
227                        (y - rect.bottom as mjtNum) / rect.height as mjtNum
228                    );
229
230                    /* Mark selection */
231                    self.pert.select = body_id;
232                    self.pert.flexselect = flex_id;
233                    self.pert.skinselect = skin_id;
234                    self.pert.active = 0;
235
236                    let mut tmp = [0.0; 3];
237                    unsafe {
238                        mju_sub3(tmp.as_mut_ptr(), xyz.as_ptr(), data.ffi().xpos.add((3 *self.pert.select) as usize));
239                        mju_mulMatTVec(self.pert.localpos.as_mut_ptr(), data.ffi().xmat.add((9*self.pert.select) as usize), tmp.as_ptr(), 3, 3);
240                    }
241
242                    /* Set tracking camera */
243                    if modifiers == &Modifiers::Control && body_id >= 0 {
244                        self.camera.track(body_id as u32);
245                    }
246                }
247                self.last_bnt_press_time = Instant::now();
248                true
249            },
250            Action::Release => false,
251            Action::Repeat => self.left_click
252        };
253    }
254}
255
256/****************************************** */
257// C++ viewer wrapper
258/****************************************** */
259/// Wrapper around the C++ implementation of MujoCo viewer
260#[cfg(feature = "cpp-viewer")]
261pub struct MjViewerCpp<'m> {
262    sim: *mut mujoco_Simulate,
263    running: bool,
264
265    // Store these here since the C++ bindings save references to them.
266    // We don't actually need them ourselves, at least not here.
267    _cam: Box<MjvCamera>,
268    _opt: Box<MjvOption>,
269    _pert: Box<MjvPerturb>,
270    _user_scn: Box<MjvScene<'m>>,
271    _glfw: glfw::Glfw
272}
273
274#[cfg(feature = "cpp-viewer")]
275impl<'m> MjViewerCpp<'m> {
276    #[inline]
277    pub fn running(&self) -> bool {
278        self.running
279    }
280
281    #[inline]
282    pub fn user_scn_mut(&mut self) -> &mut MjvScene<'m> {
283        &mut self._user_scn
284    }
285
286    pub fn launch_passive(model: &'m MjModel, data: &MjData, scene_max_ngeom: usize) -> Self {
287        let mut _glfw = glfw::init(glfw::fail_on_errors).unwrap();
288
289        // Allocate on the heap as the data must not be moved due to C++ bindings
290        let mut _cam = Box::new(MjvCamera::default());
291        let mut _opt: Box<MjvOption> = Box::new(MjvOption::default());
292        let mut _pert = Box::new(MjvPerturb::default());
293        let mut _user_scn = Box::new(MjvScene::new(&model, scene_max_ngeom));
294        let sim;
295        unsafe {
296            sim = new_simulate(&mut *_cam, &mut *_opt, &mut *_pert, _user_scn.ffi_mut(), true);
297            (*sim).RenderInit();
298            (*sim).Load(model.__raw(), data.__raw(), CString::new("file.xml").unwrap().as_ptr());
299            (*sim).RenderStep(true);
300        }
301
302        Self {sim, running: true, _cam, _opt, _pert, _glfw, _user_scn}
303    }
304
305    /// Returns the underlying C++ binding object of the viewer.
306    pub fn __raw(&self) -> *mut mujoco_Simulate {
307        self.sim
308    }
309
310    /// Renders the simulation.
311    /// `update_timer` flag species whether the time should be updated
312    /// inside the viewer (for vsync purposes).
313    /// # SAFETY
314    /// This needs to be called periodically from the MAIN thread, otherwise
315    /// GLFW stops working.
316    pub fn render(&mut self, update_timer: bool) {
317        unsafe {
318            assert!(self.running, "render called after viewer has been closed!");
319            self.running = (*self.sim).RenderStep(update_timer);
320        }
321    }
322
323    pub fn sync(&mut self) {
324        unsafe {
325            (*self.sim).Sync(false);
326        }
327    }
328}
329
330#[cfg(feature = "cpp-viewer")]
331impl Drop for MjViewerCpp<'_> {
332    fn drop(&mut self) {
333        unsafe {
334            (*self.sim).RenderCleanup();
335            free_simulate(self.sim);
336        }
337    }
338}