mujoco_rs/
renderer.rs

1//! Module related to implementation of the [`MjRenderer`].
2use crate::wrappers::mj_visualization::MjvScene;
3use crate::wrappers::mj_rendering::MjrContext;
4
5#[cfg(target_os = "linux")]
6use crate::renderer::egl::GlStateEgl;
7
8use crate::vis_common::sync_geoms;
9use crate::builder_setters;
10use crate::prelude::*;
11
12use bitflags::bitflags;
13use png::Encoder;
14
15use std::io::{self, BufWriter, ErrorKind, Write};
16use std::fmt::Display;
17use std::error::Error;
18use std::marker::PhantomData;
19use std::num::NonZero;
20use std::ops::Deref;
21use std::path::Path;
22use std::fs::File;
23
24#[cfg(feature = "renderer-winit-fallback")]
25mod universal;
26
27#[cfg(feature = "renderer-winit-fallback")]
28use universal::GlStateWinit;
29
30#[cfg(target_os = "linux")]
31mod egl;
32
33
34const RGB_NOT_FOUND_ERR_STR: &str = "RGB rendering is not enabled (renderer.with_rgb_rendering(true))";
35const DEPTH_NOT_FOUND_ERR_STR: &str = "depth rendering is not enabled (renderer.with_depth_rendering(true))";
36const INVALID_INPUT_SIZE: &str = "the input width and height don't match the renderer's configuration";
37const EXTRA_INTERNAL_VISUAL_GEOMS: u32 = 100;
38
39
40/// GlState enum wrapper. By default, headless implementation will be used
41/// when supported. Only on failure an invisible winit window will be used.
42pub(crate) enum GlState {
43    #[cfg(feature = "renderer-winit-fallback")] Winit(GlStateWinit),
44    #[cfg(target_os = "linux")] Egl(egl::GlStateEgl),
45}
46
47impl GlState {
48    /// Creates a new [`GlState`], which by default tries to use
49    /// an offscreen implementation. As a fallback, winit will be used.
50    pub(crate) fn new(width: NonZero<u32>, height: NonZero<u32>) -> Result<Self, RendererError> {
51        #[cfg(target_os = "linux")]
52        #[allow(unused_variables)]
53        let egl_err = match GlStateEgl::new(width, height) {
54            Ok(egl_state) => return Ok(Self::Egl(egl_state)),
55            Err(e) => e,
56        };
57
58        #[cfg(feature = "renderer-winit-fallback")]
59        match GlStateWinit::new(width, height) {
60            Ok(winit_state) => return Ok(Self::Winit(winit_state)),
61            #[cfg(not(target_os = "linux"))]
62            Err(e) => {
63                return Err(e);
64            },
65
66            #[cfg(target_os = "linux")]
67            _ => {}
68        }
69
70        #[cfg(target_os = "linux")]
71        Err(RendererError::GlutinError(egl_err))
72    }
73
74    pub(crate) fn make_current(&self) -> glutin::error::Result<()> {
75        match self {
76            #[cfg(target_os = "linux")]
77            Self::Egl(egl_state) => egl_state.make_current(),
78            #[cfg(feature = "renderer-winit-fallback")]
79            Self::Winit(winit_state) => winit_state.make_current()
80        }
81    }
82}
83
84
85/// A builder for [`MjRenderer`].
86#[derive(Debug)]
87pub struct MjRendererBuilder<M: Deref<Target = MjModel> + Clone> {
88    width: u32,
89    height: u32,
90    num_visual_internal_geom: u32,
91    num_visual_user_geom: u32,
92    rgb: bool,
93    depth: bool,
94    font_scale: MjtFontScale,
95    camera: MjvCamera,
96    opts: MjvOption,
97    model_type: PhantomData<M>
98}
99
100impl<M: Deref<Target = MjModel> + Clone> MjRendererBuilder<M> {
101    /// Create a builder with default configuration.
102    /// Defaults are:
103    /// - `width` and `height`: use offwidth and offheight of MuJoCo's visual/global settings from the model,
104    /// - `num_visual_internal_geom`: 100,
105    /// - `num_visual_user_geom`: 0,
106    /// - `rgb`: true,
107    /// - `depth`: false.
108    pub fn new() -> Self {
109        Self {
110            width: 0, height: 0,
111            num_visual_internal_geom: EXTRA_INTERNAL_VISUAL_GEOMS, num_visual_user_geom: 0,
112            rgb: true, depth: false, font_scale: MjtFontScale::mjFONTSCALE_100,
113            camera: MjvCamera::default(), opts: MjvOption::default(),
114            model_type: PhantomData
115        }
116    }
117
118    builder_setters! {
119        width: u32; "
120image width.
121
122<div class=\"warning\">
123
124The width must be less or equal to the offscreen buffer width,
125which can be configured at the top of the model's XML like so:
126
127```xml
128<visual>
129    <global offwidth=\"1920\" .../>
130</visual>
131```
132
133</div>";
134
135        height: u32; "\
136image height.
137
138<div class=\"warning\">
139
140The height must be less or equal to the offscreen buffer height,
141which can be configured at the top of the model's XML like so:
142
143```xml
144<visual>
145    <global offheight=\"1080\" .../>
146</visual>
147```
148
149</div>";
150
151        num_visual_internal_geom: u32; "\
152            maximum number of additional visual-only internal geoms to allocate for.
153            Note that the total number of geoms in the internal scene will be
154            `num_visual_internal_geom` + `num_visual_user_geom`.";
155
156        num_visual_user_geom: u32;      "maximum number of additional visual-only user geoms (drawn by the user).";
157        rgb: bool;                      "RGB rendering enabled (true) or disabled (false).";
158        depth: bool;                    "depth rendering enabled (true) or disabled (false).";
159        font_scale: MjtFontScale;       "font scale of drawn text (with [MjrContext]).";
160        camera: MjvCamera;              "camera used for drawing.";
161        opts: MjvOption;                "visualization options.";
162    }
163
164    /// Builds a [`MjRenderer`].
165    pub fn build(self, model: M) -> Result<MjRenderer<M>, RendererError> {
166        // Assume model's maximum should be used
167        let mut height = self.height;
168        let mut width = self.width;
169        if width == 0 && height == 0 {
170            let global = &model.vis().global;
171            height = global.offheight as u32;
172            width = global.offwidth as u32;
173        }
174
175        let gl_state = GlState::new(width.try_into().unwrap(), height.try_into().unwrap())?;
176
177        // Initialize the rendering context to render to the offscreen buffer.
178        let mut context = MjrContext::new(&model);
179        context.offscreen();
180
181        // The 3D scene for visualization
182        let scene = MjvScene::new(
183            model.clone(),
184            model.ffi().ngeom as usize + self.num_visual_internal_geom as usize
185            + self.num_visual_user_geom as usize
186        );
187
188        let user_scene = MjvScene::new(
189            model.clone(),
190            self.num_visual_user_geom as usize
191        );
192
193        // Construct the renderer and create allocated buffers.
194        let renderer = MjRenderer {
195            scene, user_scene, context, model, camera: self.camera, option: self.opts,
196            flags: RendererFlags::empty(), rgb: None, depth: None,
197            width: width as usize, height: height as usize, gl_state
198        }   // These require special care
199            .with_rgb_rendering(self.rgb)
200            .with_depth_rendering(self.depth);
201
202        Ok(renderer)
203    }
204}
205
206
207impl<M: Deref<Target = MjModel> + Clone> Default for MjRendererBuilder<M> {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213/// A renderer for rendering 3D scenes.
214/// By default, RGB rendering is enabled and depth rendering is disabled.
215pub struct MjRenderer<M: Deref<Target = MjModel> + Clone> {
216    scene: MjvScene<M>,
217    user_scene: MjvScene<M>,
218    context: MjrContext,
219    model: M,
220
221    /* OpenGL */
222    gl_state: GlState,
223
224    /* Configuration */
225    camera: MjvCamera,
226    option: MjvOption,
227    flags: RendererFlags,
228
229    /* Storage */
230    // Use Box to allow less space to be used
231    // when rgb or depth rendering is disabled
232    rgb: Option<Box<[u8]>>,
233    depth: Option<Box<[f32]>>,
234
235    width: usize,
236    height: usize,
237}
238
239impl<M: Deref<Target = MjModel> + Clone> MjRenderer<M> {
240    /// Construct a new renderer.
241    /// The `max_user_geom` parameter
242    /// defines how much space will be allocated for additional, user-defined visual-only geoms.
243    /// It can thus be set to 0 if no additional geoms will be drawn by the user.
244    /// # Scene allocation
245    /// The renderer uses two scenes:
246    /// - the internal scene: used by the renderer to draw the model's state.
247    /// - the user scene: used by the user to add additional geoms to the internal scene
248    /// 
249    /// The **internal scene** allocates the amount of space needed to fit every pre-existing
250    /// model geom + user visual-only geoms + additional visual-only geoms that aren't from the user (e.g., tendons).
251    /// By default, the renderer reserves 100 extra geom slots for drawing the additional visual-only geoms.
252    /// If that is not enough or it is too much, you can construct [`MjRenderer`] via its builder
253    /// ([`MjRenderer::builder`]), which allows more configuration. 
254    /// 
255    /// <div class="warning">
256    /// 
257    /// Parameters `width` and `height` must be less or equal to the offscreen buffer size,
258    /// which can be configured at the top of the model's XML like so:
259    /// 
260    /// ```xml
261    /// <visual>
262    ///    <global offwidth="1920" offheight="1080"/>
263    /// </visual>
264    /// ```
265    /// 
266    /// </div>
267    pub fn new(model: M, width: usize, height: usize, max_user_geom: usize) -> Result<Self, RendererError> {
268        let builder = Self::builder()
269            .width(width as u32).height(height as u32).num_visual_user_geom(max_user_geom as u32);
270        builder.build(model)
271    }
272
273    /// Create a [`MjRendererBuilder`] to configure [`MjRenderer`].
274    pub fn builder() -> MjRendererBuilder<M> {
275        MjRendererBuilder::new()
276    }
277
278    /// Return an immutable reference to the internal scene.
279    pub fn scene(&self) -> &MjvScene<M>{
280        &self.scene
281    }
282
283    /// Return an immutable reference to a user scene for drawing custom visual-only geoms.
284    pub fn user_scene(&self) -> &MjvScene<M>{
285        &self.user_scene
286    }
287
288    /// Return a mutable reference to a user scene for drawing custom visual-only geoms.
289    pub fn user_scene_mut(&mut self) -> &mut MjvScene<M>{
290        &mut self.user_scene
291    }
292
293    /// Return an immutable reference to visualization options.
294    pub fn opts(&self) -> &MjvOption {
295        &self.option
296    }
297
298    /// Return a mutable reference to visualization options.
299    pub fn opts_mut(&mut self) -> &mut MjvOption {
300        &mut self.option
301    }
302
303    /// Return an immutable reference to the camera.
304    pub fn camera(&self) -> &MjvCamera {
305        &self.camera
306    }
307
308    /// Return a mutable reference to the camera.
309    pub fn camera_mut(&mut self) -> &mut MjvCamera {
310        &mut self.camera
311    }
312
313    /// Check if RGB rendering is enabled.
314    pub fn rgb_enabled(&self) -> bool {
315        self.flags.contains(RendererFlags::RENDER_RGB)
316    }
317
318    /// Check if depth rendering is enabled.
319    pub fn depth_enabled(&self) -> bool {
320        self.flags.contains(RendererFlags::RENDER_DEPTH)
321    }
322
323    /// Sets the font size.
324    pub fn set_font_scale(&mut self, font_scale: MjtFontScale) {
325        self.context.change_font(font_scale);
326    }
327
328    /// Update the visualization options and return a reference to self.
329    pub fn set_opts(&mut self, options: MjvOption) {
330        self.option = options;
331    }
332
333    /// Set the camera used for rendering.
334    pub fn set_camera(&mut self, camera: MjvCamera)  {
335        self.camera = camera;
336    }
337
338    /// Enables/disables RGB rendering.
339    pub fn set_rgb_rendering(&mut self, enable: bool) {
340        self.flags.set(RendererFlags::RENDER_RGB, enable);
341        self.rgb = if enable { Some(vec![0; 3 * self.width * self.height].into_boxed_slice()) } else { None } ;
342    }
343
344    /// Enables/disables depth rendering.
345    pub fn set_depth_rendering(&mut self, enable: bool) {
346        self.flags.set(RendererFlags::RENDER_DEPTH, enable);
347        self.depth = if enable { Some(vec![0.0; self.width * self.height].into_boxed_slice()) } else { None } ;
348    }
349
350    /// Sets the font size. To be used on construction.
351    pub fn with_font_scale(mut self, font_scale: MjtFontScale) -> Self {
352        self.set_font_scale(font_scale);
353        self
354    }
355
356    /// Update the visualization options and return a reference to self. To be used on construction.
357    pub fn with_opts(mut self, options: MjvOption) -> Self {
358        self.set_opts(options);
359        self
360    }
361
362    /// Set the camera used for rendering. To be used on construction.
363    pub fn with_camera(mut self, camera: MjvCamera) -> Self  {
364        self.set_camera(camera);
365        self
366    }
367
368    /// Enables/disables RGB rendering. To be used on construction.
369    pub fn with_rgb_rendering(mut self, enable: bool) -> Self {
370        self.set_rgb_rendering(enable);
371        self
372    }
373
374    /// Enables/disables depth rendering. To be used on construction.
375    pub fn with_depth_rendering(mut self, enable: bool) -> Self {
376        self.set_depth_rendering(enable);
377        self
378    }
379
380    /// Update the scene with new data from data.
381    pub fn sync(&mut self, data: &mut MjData<M>) {
382        assert_eq!(data.model().signature(), self.model.signature(), "'data' must be created from the same model as the renderer.");
383
384        self.scene.update(data, &self.option, &MjvPerturb::default(), &mut self.camera);
385
386        /* Draw user scene geoms */
387        sync_geoms(&self.user_scene, &mut self.scene)
388            .expect("could not sync the user scene with the internal scene; this is a bug, please report it.");
389
390        self.render();
391    }
392
393    /// Return a flattened RGB image of the scene.
394    pub fn rgb_flat(&self) -> Option<&[u8]> {
395        self.rgb.as_deref()
396    }
397
398    /// Return an RGB image of the scene. This methods accepts two generic parameters <WIDTH, HEIGHT>
399    /// that define the shape of the output slice.
400    pub fn rgb<const WIDTH: usize, const HEIGHT: usize>(&self) -> io::Result<&[[[u8; 3]; WIDTH]; HEIGHT]> {
401        if let Some(flat) = self.rgb_flat() {
402            if flat.len() == WIDTH * HEIGHT * 3 {
403                let p_shaped = flat.as_ptr() as *const [[[u8; 3]; WIDTH]; HEIGHT];
404
405                // SAFETY: The alignment of the output is the same as the original.
406                // The lifetime also matches  'a in &'a self, which prevents data races.
407                // Length (number of elements) matches the output's.
408                Ok(unsafe { p_shaped.as_ref().unwrap() })
409            }
410            else {
411                Err(io::Error::new(io::ErrorKind::InvalidInput, INVALID_INPUT_SIZE))
412            }
413        }
414        else {
415            Err(io::Error::new(io::ErrorKind::NotFound, RGB_NOT_FOUND_ERR_STR))
416        }
417    }
418
419    /// Return a flattened depth image of the scene.
420    pub fn depth_flat(&self) -> Option<&[f32]> {
421        self.depth.as_deref()
422    }
423
424    /// Return a depth image of the scene. This methods accepts two generic parameters <WIDTH, HEIGHT>
425    /// that define the shape of the output slice.
426    pub fn depth<const WIDTH: usize, const HEIGHT: usize>(&self) -> io::Result<&[[f32; WIDTH]; HEIGHT]> {
427        if let Some(flat) = self.depth_flat() {
428            if flat.len() == WIDTH * HEIGHT {
429                let p_shaped = flat.as_ptr() as *const [[f32; WIDTH]; HEIGHT];
430
431                // SAFETY: The alignment of the output is the same as the original.
432                // The lifetime matches  'a in &'a self, which prevents data races.
433                // Length (number of elements) matches the output's.
434                Ok(unsafe { p_shaped.as_ref().unwrap() })
435            }
436            else {
437                Err(io::Error::new(io::ErrorKind::InvalidInput, INVALID_INPUT_SIZE))
438            }
439        }
440        else {
441            Err(io::Error::new(io::ErrorKind::NotFound, DEPTH_NOT_FOUND_ERR_STR))
442        }
443    }
444
445    /// Save an RGB image of the scene to a path.
446    /// # Errors
447    /// - [`ErrorKind::NotFound`] when RGB rendering is disabled,
448    /// - other errors related to write.
449    pub fn save_rgb<T: AsRef<Path>>(&self, path: T) -> io::Result<()> {
450        if let Some(rgb) = &self.rgb {
451            let file = File::create(path.as_ref())?;
452            let w = BufWriter::new(file);
453
454            let mut encoder = Encoder::new(w, self.width as u32, self.height as u32);
455            encoder.set_color(png::ColorType::Rgb);
456            encoder.set_depth(png::BitDepth::Eight);
457            encoder.set_compression(png::Compression::NoCompression);
458
459            let mut writer = encoder.write_header()?;
460            writer.write_image_data(rgb)?;
461            Ok(())
462        }
463        else {
464            Err(io::Error::new(ErrorKind::NotFound, RGB_NOT_FOUND_ERR_STR))
465        }
466    }
467
468    /// Save a depth image of the scene to a path. The image is 16-bit PNG, which
469    /// can be converted into depth (distance) data by dividing the grayscale values by
470    /// 65535.0 and applying inverse normalization (if it was enabled with `normalize`): `depth = min + (grayscale / 65535.0) * (max - min)`.
471    /// If `normalize` is `true`, then the data is normalized with min-max normalization.
472    /// Use of [`MjRenderer::save_depth_raw`] is recommended if performance is critical, as
473    /// it skips PNG encoding and also saves the true depth values directly.
474    /// # Returns
475    /// An [`Ok`]`((min, max))` is returned, where min and max represent the normalization parameters.
476    /// # Errors
477    /// - [`ErrorKind::NotFound`] when depth rendering is disabled,
478    /// - other errors related to write.
479    pub fn save_depth<T: AsRef<Path>>(&self, path: T, normalize: bool) -> io::Result<(f32, f32)> {
480        if let Some(depth) = &self.depth {
481            let file = File::create(path.as_ref())?;
482            let w = BufWriter::new(file);
483
484            let mut encoder = Encoder::new(w, self.width as u32, self.height as u32);
485            encoder.set_color(png::ColorType::Grayscale);
486            encoder.set_depth(png::BitDepth::Sixteen);
487            encoder.set_compression(png::Compression::NoCompression);
488
489            let (norm, min, max) =
490            if normalize {
491                let max = depth.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
492                let min = depth.iter().cloned().fold(f32::INFINITY, f32::min);
493                (depth.iter().flat_map(|&x| (((x - min) / (max - min) * 65535.0).min(65535.0) as u16).to_be_bytes()).collect::<Box<_>>(), min, max)
494            }
495            else {
496                (depth.iter().flat_map(|&x| ((x * 65535.0).min(65535.0) as u16).to_be_bytes()).collect::<Box<_>>(), 0.0, 1.0)
497            };
498
499            let mut writer = encoder.write_header()?;
500            writer.write_image_data(&norm)?;
501            Ok((min, max))
502        }
503        else {
504            Err(io::Error::new(ErrorKind::NotFound, DEPTH_NOT_FOUND_ERR_STR))
505        }
506    }
507
508    /// Save the raw depth data to the `path`. The data is encoded
509    /// as a sequence of bytes, where groups of four represent a single f32 value.
510    /// The lower bytes of individual f32 appear first (low-endianness).
511    /// # Errors
512    /// - [`ErrorKind::NotFound`] when depth rendering is disabled,
513    /// - other errors related to write.
514    pub fn save_depth_raw<T: AsRef<Path>>(&self, path: T) -> io::Result<()> {
515        if let Some(depth) = &self.depth {
516            let file = File::create(path.as_ref())?;
517            let mut writer = BufWriter::new(file);
518
519            /* Fast conversion to a byte slice to prioritize performance */
520            let p = unsafe { std::slice::from_raw_parts(
521                depth.as_ptr() as *const u8,
522                std::mem::size_of::<f32>() * depth.len()
523            ) };
524
525            writer.write_all(p)?;
526            Ok(())
527        }
528        else {
529            Err(io::Error::new(ErrorKind::NotFound, DEPTH_NOT_FOUND_ERR_STR))
530        }
531    }
532
533    /// Draws the scene to internal arrays.
534    /// Use [`MjRenderer::rgb`] or [`MjRenderer::depth`] to obtain the rendered image.
535    fn render(&mut self) {
536        self.gl_state.make_current().expect("failed to make OpenGL context current");
537        let vp = MjrRectangle::new(0, 0, self.width as i32, self.height as i32);
538        self.scene.render(&vp, &self.context);
539
540        /* Fully flatten everything */
541        let flat_rgb = self.rgb.as_deref_mut();
542        let flat_depth = self.depth.as_deref_mut();
543
544        /* Read to whatever is enabled */
545        self.context.read_pixels(
546            flat_rgb,
547            flat_depth,
548            &vp
549        );
550
551        /* Make depth values be the actual distance in meters */
552        if let Some(depth) = self.depth.as_deref_mut() {
553            let map = &self.model.vis().map;
554            let stat = &self.model.stat();
555
556            let extent = stat.extent as f32;
557            let near = map.znear * extent;
558            let far = map.zfar * extent;
559            for value in depth {
560                *value = near / (1.0 - *value * (1.0 - near / far));
561            }
562        }
563    }
564}
565
566
567#[derive(Debug)]
568pub enum RendererError {
569    #[cfg(feature = "renderer-winit-fallback")]
570    EventLoopError(winit::error::EventLoopError),
571    GlutinError(glutin::error::Error)
572}
573
574impl Display for RendererError {
575    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
576        match self {
577            #[cfg(feature = "renderer-winit-fallback")]
578            Self::EventLoopError(e) => write!(f, "event loop failed to initialize: {}", e),
579            Self::GlutinError(e) => write!(f, "glutin failed to initialize: {}", e)
580        }
581    }
582}
583
584impl Error for RendererError {
585    fn source(&self) -> Option<&(dyn Error + 'static)> {
586        match self {
587            #[cfg(feature = "renderer-winit-fallback")]
588            Self::EventLoopError(e) => Some(e),
589            Self::GlutinError(e) => Some(e)
590        }
591    }
592}
593
594bitflags! { 
595    /// Flags that enable features of the renderer.
596    struct RendererFlags: u8 {
597        const RENDER_RGB = 1 << 0;
598        const RENDER_DEPTH = 1 << 1;
599    }
600}
601
602
603
604/* 
605** Don't run any tests as OpenGL hates if anything
606** runs outside the main thread.
607*/
608
609#[cfg(test)]
610mod test {
611    use crate::assert_relative_eq;
612
613    use super::*;
614
615    const MODEL: &str = stringify!(
616        <mujoco>
617
618        <visual>
619            <global offwidth="1280" offheight="720"/>
620        </visual>
621
622        <worldbody>
623            <geom name="floor" type="plane" size="10 10 1" euler="0 0 0"/>
624            <geom type="box" size="1 10 10" pos="-1 0 0" euler="0 0 0"/>
625
626            <camera name="depth_test" euler="90 90 0" pos="2.25 0 1"/>
627
628        </worldbody>
629        </mujoco>
630    );
631
632    /// Depth calculation test.
633    /// This is only run on Linux due to EGL requirements (winit cannot be used on multiple threads).
634    #[test]
635    #[cfg(target_os = "linux")]
636    fn test_depth() {
637        let model = MjModel::from_xml_string(MODEL).expect("could not load the model");
638        let mut data = MjData::new(&model);
639        data.step();
640
641        let mut renderer = MjRenderer::builder()
642            .rgb(false)
643            .depth(true)
644            .camera(MjvCamera::new_fixed(model.name_to_id(MjtObj::mjOBJ_CAMERA, "depth_test") as u32))
645            .build(&model)
646            .unwrap();
647
648        renderer.sync(&mut data);
649        let min = renderer.depth_flat().unwrap().iter().fold(f32::INFINITY, |a , &b| a.min(b));
650        let max = renderer.depth_flat().unwrap().iter().fold(f32::NEG_INFINITY, |a , &b| a.max(b));
651
652        assert_relative_eq!(min, max, epsilon = 1e-4);
653        assert_relative_eq!(min, 2.25, epsilon = 1e-4);
654    }
655}