Skip to main content

gl_utils/render/resource/
demo.rs

1use std::sync::{Arc, LazyLock, RwLock};
2use strum::IntoEnumIterator;
3use winit;
4
5use math_utils as math;
6use math_utils::num;
7
8use crate::{render, tile, Render};
9use super::*;
10
11/// Winit deprecated the modifiers field of keyboard input so we have to track modifier
12/// changes
13static MODIFIERS : LazyLock <Arc <RwLock <winit::keyboard::ModifiersState>>> =
14  LazyLock::new (|| Arc::new (RwLock::new (winit::keyboard::ModifiersState::empty())));
15
16/// A type to implement winit `ApplicationHandler` trait
17pub struct App {
18  pub render             : Render <Default>,
19  pub mouse_position     : (f64, f64),
20  pub mouse_button_event : Option <winit::event::ElementState>,
21  pub running            : bool
22}
23
24impl App {
25  pub fn new (
26    glium_display : glium::Display <glutin::surface::WindowSurface>,
27    window        : winit::window::Window
28  ) -> Self {
29    let mut render = Render::<Default>::new (glium_display, window);
30    render.demo_init();
31    let mouse_position      = (0.0, 0.0);
32    let mouse_button_event  = None;
33    let running             = true;
34    Self { render, mouse_position, mouse_button_event, running }
35  }
36}
37
38impl winit::application::ApplicationHandler for App {
39  // required
40  fn resumed (&mut self, _event_loop : &winit::event_loop::ActiveEventLoop) { }
41
42  fn window_event (&mut self,
43    _event_loop : &winit::event_loop::ActiveEventLoop,
44    _window_id  : winit::window::WindowId,
45    event       : winit::event::WindowEvent
46  ) {
47    // winit events:
48    // TODO: review
49    // - window events includes input events received by the window
50    // - device events are received independent of window focus
51    // there may be differences in the events received depending on platform:
52    // - on Linux keyboard input is received as both a device and window event
53    // - on Windows only mouse motion device events are received, all other
54    //   input is received as window events
55    self.render.demo_handle_winit_window_event (
56      event,
57      &mut self.running,
58      &mut self.mouse_position,
59      &mut self.mouse_button_event)
60  }
61}
62
63impl Render <Default> {
64  /// Get current keyboard modifiers
65  pub fn modifiers() -> winit::keyboard::ModifiersState {
66    *MODIFIERS.read().unwrap()
67  }
68  /// Initializes "demo" state such as example mesh instances and viewport text
69  /// tiles.
70  ///
71  /// This should normally be called immediately after render context creation
72  /// as some assumptions are made about the current state (see Panics below).
73  /// The renderer can be returned to this state with the `reset` method.
74  ///
75  /// After `demo_init`, `demo_handle_winit_event` can be called on incoming
76  /// events to interact with the demo.
77  ///
78  /// A usage example is provided in `./examples/demo.rs`.
79  ///
80  /// # Panics
81  ///
82  /// A prerequisite is that the first four `viewport_resources` entries are
83  /// empty.
84  ///
85  /// The currently allocated per-instance buffers should be empty.
86  // TODO: doctests, make safe to call from any state?
87  fn demo_init (&mut self) {
88    use draw3d::*;
89
90    println!(">>> Initializing gl-utils demo...");
91    println!("  Press 'Q' or 'Esc' to quit");
92    println!("  Horizontal movement: 'W', 'A', 'S', 'D'");
93    println!("  Vertical movement: 'Space', 'LCtrl' or 'R', 'F'");
94    println!("  Rotation: 'Up', 'Down', 'Left', 'Right' or 'H', 'J', 'K', 'L'");
95    println!("  Zoom in/out 3D (perspective): 'I', 'O'");
96    println!("  Zoom in/out 3D (orthographic): 'Alt+I', 'Alt+O'");
97    println!("  Zoom in/out 2D: 'Shift+Alt+I', 'Shift+Alt+O'");
98    println!("  Toggle split into 4 viewports with orthographic views: 'X'");
99    println!("  Screenshot: 'F10'");
100
101    // set pointer texture
102    self.resource.draw2d.draw_pointer =
103      Some (DefaultTexturePointerId::Hand as PointerTextureIndexRepr);
104    // hide hardware cursor
105    self.window.set_cursor_visible (false);
106
107    //
108    // 3D per-instance data
109    //
110    // cube vertex data
111    let cube_vertex = [vertex::Vert3dOrientationScaleColor {
112      position:    [-1.5, 2.5, 1.0],
113      orientation: math::Matrix3::identity().into_col_arrays(),
114      scale:       [std::f32::consts::SQRT_3 / 6.0; 3],
115      color:       color::rgba_u8_to_rgba_f32 (color::CYAN)
116    }];
117    // aabb lines vertex data
118    let aabb_lines_vertex = [vertex::Vert3dOrientationScaleColor {
119      position:    [-0.5, 2.5, 1.0],
120      orientation: math::Matrix3::identity().into_col_arrays(),
121      scale:       [0.5, 0.5, 1.0],
122      color:       color::rgba_u8_to_rgba_f32 (color::DEBUG_PINK)
123    }];
124    // aabb triangles vertex data
125    let aabb_triangles_vertex = [vertex::Vert3dOrientationScaleColor {
126      color: color::rgba_u8_to_rgba_f32 (color::DEBUG_GREY),
127      .. aabb_lines_vertex[0]
128    }];
129    // grid vertex data
130    let grid_vertex_data = Default::debug_grid_vertices();
131    // hemisphere vertex data
132    let hemisphere_vertex = [vertex::Vert3dOrientationScaleColor {
133      position:    [2.5, 2.5, 0.0],
134      orientation: math::Matrix3::identity().into_col_arrays(),
135      scale:       [0.5, 0.5, 0.5],
136      color:       color::rgba_u8_to_rgba_f32 (color::DEBUG_VIOLET)
137    }];
138    // sphere vertex data
139    // sphere: 1.0 diameter
140    let sphere_vertex = [vertex::Vert3dOrientationScaleColor {
141      position:    [1.5, 2.5, 0.5],
142      orientation: math::Matrix3::identity().into_col_arrays(),
143      scale:       [0.5, 0.5, 0.5],
144      color:       color::rgba_u8_to_rgba_f32 (color::DEBUG_RED)
145    }];
146    // capsule vertex data
147    let capsule_vertex = [vertex::Vert3dOrientationScaleColor {
148      position:    [0.5, 2.5, 1.0],
149      orientation: math::Matrix3::identity().into_col_arrays(),
150      scale:       [0.5, 0.5, 1.0],
151      color:       color::rgba_u8_to_rgba_f32 (color::DEBUG_AZURE)
152    }];
153    // cylinder vertex data
154    let cylinder_vertex = [vertex::Vert3dOrientationScaleColor {
155      position:    [3.5, 2.5, 1.0],
156      orientation: math::Matrix3::identity().into_col_arrays(),
157      scale:       [0.5, 0.5, 1.0],
158      color:       color::rgba_u8_to_rgba_f32 (color::DEBUG_LIGHT_BLUE)
159    }];
160    let aabb_lines               = Some (&aabb_lines_vertex[..]);
161    let aabb_triangles           = Some (&aabb_triangles_vertex[..]);
162    let aabb_lines_and_triangles = None;
163    let meshes = {
164      let mut v = VecMap::with_capacity (MeshId::COUNT);
165      assert!(v.insert (MeshId::Grid       as usize, &grid_vertex_data[..]).is_none());
166      assert!(v.insert (MeshId::Hemisphere as usize, &hemisphere_vertex[..]).is_none());
167      assert!(v.insert (MeshId::Sphere     as usize, &sphere_vertex[..]).is_none());
168      assert!(v.insert (MeshId::Capsule    as usize, &capsule_vertex[..]).is_none());
169      assert!(v.insert (MeshId::Cube       as usize, &cube_vertex[..]).is_none());
170      assert!(v.insert (MeshId::Cylinder   as usize, &cylinder_vertex[..]).is_none());
171      v
172    };
173    // note: no billboard instances are created here, instead they are assigned
174    // to share instances with meshes
175    let billboards = VecMap::new();
176    self.resource.draw3d.instance_vertices_set (
177      &self.glium_display,
178      InstancesInit {
179        aabb_lines, aabb_triangles, aabb_lines_and_triangles, meshes, billboards
180      }
181    );
182    // tile billboards
183    for mesh_id in MeshId::iter() {
184      let key = mesh_id as usize;
185      self.resource.draw3d.tile_billboard_create (
186        &self.glium_display, &self.resource.tileset_128x128_texture,
187        key, self.resource.draw3d.instanced_meshes()[key].instances_range.clone(),
188        format!("{mesh_id:?}").as_str()
189      );
190    }
191    let aabb_billboard_key = MeshId::COUNT;
192    self.resource.draw3d.tile_billboard_create (
193      &self.glium_display, &self.resource.tileset_128x128_texture,
194      aabb_billboard_key,  // start immediately after mesh billboards
195      self.resource.draw3d.instanced_aabb_lines().clone(),
196      "Aabb"
197    );
198
199    let (width, height) = self.window.inner_size().into();
200    let [_tile_width, tile_height] =
201      self.resource.tile_dimensions (DefaultTilesetId::EasciiAcorn128);
202    //
203    // 2D per-viewport tiles
204    //
205    let mut tile_2d_vertices = tile::vertices ("Viewport0", (0, 0));
206    let viewport0_tile_range = 0..tile_2d_vertices.len() as u32;
207    tile_2d_vertices.extend (tile::vertices ("Viewport1: +Y", (0, 0)));
208    let viewport1_tile_range = viewport0_tile_range.end..tile_2d_vertices.len()
209      as u32;
210    tile_2d_vertices.extend (tile::vertices ("Viewport2: +X", (0, 0)));
211    let viewport2_tile_range = viewport1_tile_range.end..tile_2d_vertices.len()
212      as u32;
213    tile_2d_vertices.extend (tile::vertices ("Viewport3: -Z", (0, 0)));
214    let viewport3_tile_range = viewport2_tile_range.end..tile_2d_vertices.len()
215      as u32;
216    //let center_tile = ((width / tile_width) / 2) as i32;
217    let fps_row = ((height / tile_height) - 1) as i32;
218    tile_2d_vertices.extend (tile::vertices ("FPS: 0     ", (fps_row, 0)));
219    let viewport4_tile_range = viewport3_tile_range.end..tile_2d_vertices.len()
220      as u32;
221    self.resource.draw2d.tile_2d_vertices = glium::VertexBuffer::dynamic (
222      &self.glium_display, &tile_2d_vertices[..]
223    ).unwrap();
224    let tile_color_2d_vertices = {
225      let tiles  = tile::vertices ("abc123",    (2, 0));
226      let colors = vec![
227        ( [1.0, 0.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0] ),
228        ( [0.0, 1.0, 0.0, 1.0], [0.0, 0.0, 0.0, 0.0] ),
229        ( [0.0, 0.0, 1.0, 1.0], [0.0, 0.0, 0.0, 0.0] ),
230        ( [0.0, 0.0, 0.0, 0.0], [1.0, 1.0, 0.0, 1.0] ),
231        ( [0.0, 0.0, 0.0, 0.0], [1.0, 0.0, 1.0, 1.0] ),
232        ( [0.0, 0.0, 0.0, 0.0], [0.0, 1.0, 1.0, 1.0] )
233      ];
234      tiles.into_iter().zip (colors).map (
235        |(vertex::Vert2dTile { tile, row, column }, (fg, bg))|
236        vertex::Vert2dTileColor { tile, row, column, fg, bg }
237      ).collect::<Vec <_>>()
238    };
239    let viewport0_tile_color_range = 0..tile_color_2d_vertices.len() as u32;
240    self.resource.draw2d.tile_color_2d_vertices = glium::VertexBuffer::dynamic (
241      &self.glium_display, &tile_color_2d_vertices[..]
242    ).unwrap();
243    let draw_tiles = draw2d::Tiles {
244      vertex_range: 0..0, origin: (2, 2).into(),
245      tileset_id: DefaultTilesetId::EasciiAcorn128
246    };
247    let draw_tiles_color = draw2d::Tiles {
248      vertex_range: 0..0, origin: draw2d::TilesOrigin::World,
249      tileset_id: DefaultTilesetId::EasciiAcorn256
250    };
251    self.resource.draw2d.viewport_resources_set (MAIN_VIEWPORT,
252      draw2d::ViewportResources {
253        draw_indices: vec![draw2d::DrawIndices {
254          rectangle:        None,
255          draw_tiles:       Some (draw2d::Tiles {
256            vertex_range: viewport0_tile_range,
257            .. draw_tiles.clone()
258          }),
259          draw_color_tiles: Some (draw2d::Tiles {
260            vertex_range: viewport0_tile_color_range,
261            .. draw_tiles_color
262          })
263        }],
264        .. draw2d::ViewportResources::default()
265      }
266    );
267    self.resource.draw2d.viewport_resources_set (UPPER_LEFT_VIEWPORT,
268      draw2d::ViewportResources {
269        draw_indices: vec![draw2d::DrawIndices {
270          draw_tiles: Some (draw2d::Tiles {
271            vertex_range: viewport1_tile_range,
272            .. draw_tiles.clone()
273          }),
274          .. draw2d::DrawIndices::default()
275        }],
276        .. draw2d::ViewportResources::default()
277      }
278    );
279    self.resource.draw2d.viewport_resources_set (UPPER_RIGHT_VIEWPORT,
280      draw2d::ViewportResources {
281        draw_indices: vec![draw2d::DrawIndices {
282          draw_tiles: Some (draw2d::Tiles {
283            vertex_range: viewport2_tile_range,
284            .. draw_tiles.clone()
285          }),
286          .. draw2d::DrawIndices::default()
287        }],
288        .. draw2d::ViewportResources::default()
289      }
290    );
291    self.resource.draw2d.viewport_resources_set (LOWER_LEFT_VIEWPORT,
292      draw2d::ViewportResources {
293        draw_indices: vec![draw2d::DrawIndices {
294          draw_tiles: Some (draw2d::Tiles {
295            vertex_range: viewport3_tile_range,
296            .. draw_tiles.clone()
297          }),
298          .. draw2d::DrawIndices::default()
299        }],
300        .. draw2d::ViewportResources::default()
301      }
302    );
303    self.resource.draw2d.viewport_resources_set (OVERLAY_VIEWPORT,
304      draw2d::ViewportResources {
305        draw_indices: vec![draw2d::DrawIndices {
306          draw_tiles: Some (draw2d::Tiles {
307            vertex_range: viewport4_tile_range,
308            .. draw_tiles
309          }),
310          .. draw2d::DrawIndices::default()
311        }],
312        draw_lineloop: false,
313        draw_crosshair: false
314      }
315    );
316    // create overlay viewport
317    let overlay = render::viewport::Builder::new (
318      glium::Rect { width, height, left: 0, bottom: 0 }
319    ).with_camera_3d (false).build();
320    assert!(self.viewports.insert (OVERLAY_VIEWPORT, overlay).is_none());
321  }
322
323  pub fn demo_handle_winit_window_event (&mut self,
324    event              : winit::event::WindowEvent,
325    running            : &mut bool,
326    mouse_position     : &mut (f64, f64),
327    mouse_button_event : &mut Option <winit::event::ElementState>
328  ) {
329    use std::f32::consts::PI;
330    use winit::{event, keyboard};
331    use num::Zero;
332    log::debug!("demo handle winit window event: {event:?}");
333    match event {
334      // window resized event
335      event::WindowEvent::Resized (physical_size) => {
336        log::debug!("window event: {event:?}");
337        self.window_resized (physical_size);
338      }
339      // window closed event
340      event::WindowEvent::CloseRequested => { *running = false; }
341      // TODO: for some reason a destroyed event is received on startup even though
342      // window has not been destroyed (winit 27.5)
343      #[expect(clippy::match_same_arms)]
344      event::WindowEvent::Destroyed => { /* *running = false; */ }
345      // modifiers changed event
346      event::WindowEvent::ModifiersChanged (modifiers) =>
347        *MODIFIERS.write().unwrap() = modifiers.state(),
348      // keyboard input pressed event
349      event::WindowEvent::KeyboardInput {
350        event: event::KeyEvent {
351          state: event::ElementState::Pressed,
352          physical_key, logical_key, location, ..
353        },
354        ..
355      } => match (logical_key, location) {
356        ( keyboard::Key::Named (keyboard::NamedKey::Control),
357          keyboard::KeyLocation::Left
358        ) =>
359          if Self::modifiers().shift_key() {
360            self.camera3d_move_local_xy (0.0, 0.0, -0.5)
361          } else {
362            self.camera3d_move_local_xy (0.0, 0.0, -1.0)
363          }
364        _ => match physical_key {
365          keyboard::PhysicalKey::Code (key_code) => match key_code {
366            // quit
367            keyboard::KeyCode::KeyQ | keyboard::KeyCode::Escape =>
368              *running = false,
369            // choose a frame function 0-9
370            keyboard::KeyCode::Digit1 =>
371              self.frame_fun = render::frame_fun_default::<Default>,
372            // TODO: more frame functions
373            // TODO: cycle between frame functions
374            //keyboard::KeyCode::Tab => { }
375            keyboard::KeyCode::KeyW =>
376              if Self::modifiers().shift_key() {
377                self.camera3d_move_local_xy (0.0, 0.5, 0.0)
378              } else {
379                self.camera3d_move_local_xy (0.0, 1.0, 0.0)
380              }
381            keyboard::KeyCode::KeyS =>
382              if Self::modifiers().shift_key() {
383                self.camera3d_move_local_xy (0.0, -0.5, 0.0)
384              } else {
385                self.camera3d_move_local_xy (0.0, -1.0, 0.0)
386              }
387            keyboard::KeyCode::KeyD =>
388              if Self::modifiers().shift_key() {
389                self.camera3d_move_local_xy (0.5, 0.0, 0.0)
390              } else {
391                self.camera3d_move_local_xy (1.0, 0.0, 0.0)
392              }
393            keyboard::KeyCode::KeyA =>
394              if Self::modifiers().shift_key() {
395                self.camera3d_move_local_xy (-0.5, 0.0, 0.0)
396              } else {
397                self.camera3d_move_local_xy (-1.0, 0.0, 0.0)
398              }
399            keyboard::KeyCode::Space | keyboard::KeyCode::KeyR =>
400              if Self::modifiers().shift_key() {
401                self.camera3d_move_local_xy (0.0, 0.0, 0.5)
402              } else {
403                self.camera3d_move_local_xy (0.0, 0.0, 1.0)
404              }
405            keyboard::KeyCode::KeyF =>
406              if Self::modifiers().shift_key() {
407                self.camera3d_move_local_xy (0.0, 0.0, -0.5)
408              } else {
409                self.camera3d_move_local_xy (0.0, 0.0, -1.0)
410              }
411            keyboard::KeyCode::KeyJ | keyboard::KeyCode::ArrowDown => {
412              let modifiers = Self::modifiers();
413              if modifiers.alt_key() && modifiers.shift_key() {
414                self.camera2d_move_local (0.0, -1.0);
415              } else {
416                self.camera3d_rotate (
417                  math::Rad::zero(),
418                  math::Rad (-PI / 12.0),
419                  math::Rad::zero());
420              }
421            }
422            keyboard::KeyCode::KeyK | keyboard::KeyCode::ArrowUp => {
423              let modifiers = Self::modifiers();
424              if modifiers.alt_key() && modifiers.shift_key() {
425                self.camera2d_move_local (0.0, 1.0);
426              } else {
427                self.camera3d_rotate (
428                  math::Rad::zero(),
429                  math::Rad (PI / 12.0),
430                  math::Rad::zero());
431              }
432            }
433            keyboard::KeyCode::KeyH | keyboard::KeyCode::ArrowLeft => {
434              let modifiers = Self::modifiers();
435              if modifiers.alt_key() && modifiers.shift_key() {
436                self.camera2d_move_local (-1.0, 0.0);
437              } else {
438                self.camera3d_rotate (
439                  math::Rad (PI / 12.0),
440                  math::Rad::zero(),
441                  math::Rad::zero());
442              }
443            }
444            keyboard::KeyCode::KeyL | keyboard::KeyCode::ArrowRight => {
445              let modifiers = Self::modifiers();
446              if modifiers.alt_key() && modifiers.shift_key() {
447                self.camera2d_move_local (1.0, 0.0);
448              } else {
449                self.camera3d_rotate (
450                  math::Rad (-PI / 12.0),
451                  math::Rad::zero(),
452                  math::Rad::zero());
453              }
454            }
455            keyboard::KeyCode::KeyI => {
456              let modifiers = Self::modifiers();
457              if modifiers.alt_key() && modifiers.shift_key() {
458                self.camera2d_zoom_shift (1.0);
459              } else if modifiers.alt_key() {
460                self.camera3d_orthographic_zoom_scale (1.1);
461              } else {
462                self.camera3d_perspective_fovy_scale (1.0 / 1.1);
463              }
464            }
465            keyboard::KeyCode::KeyO => {
466              let modifiers = Self::modifiers();
467              if modifiers.alt_key() && modifiers.shift_key() {
468                self.camera2d_zoom_shift (-1.0);
469              } else if modifiers.alt_key() {
470                self.camera3d_orthographic_zoom_scale (1.0 / 1.1);
471              } else {
472                self.camera3d_perspective_fovy_scale (1.1);
473              }
474            }
475            // toggle 4-viewport mode
476            keyboard::KeyCode::KeyX   => self.demo_toggle_quad_viewports(),
477            // end camera controls
478            keyboard::KeyCode::F10 | keyboard::KeyCode::PrintScreen =>
479              self.screenshot(),
480            _ => {}
481          }
482          keyboard::PhysicalKey::Unidentified (_) => {}
483        }
484      } // end keyboard input
485      event::WindowEvent::MouseInput { button: event::MouseButton::Left, state, .. } =>
486        *mouse_button_event = Some (state),
487      event::WindowEvent::CursorMoved { position, ..  } => {
488        if self.resource.draw2d.draw_pointer.is_some() {
489          let (_, height) = self.glium_display.get_framebuffer_dimensions();
490          let x = position.x as f32;
491          let y = height as f32 - position.y as f32;
492          self.resource
493            .set_pointer_position (&self.glium_display, [x, y].into());
494        }
495        *mouse_position = (position.x, position.y);
496      }
497      _ => {}
498    }
499  }
500
501  /// Switch between single (perspective) and quad viewport modes (perspective
502  /// + three ortho viewports).
503  ///
504  /// Generally should be called from a render context that was initialized
505  /// with `demo_init`.
506  ///
507  /// # Panics
508  ///
509  /// If there is a single viewport (other than the `OVERLAY_VIEWPORT`) it must
510  /// be `MAIN_VIEWPORT`.
511  ///
512  /// If there are multiple viewports, they must be exactly the first four
513  /// viewports only (other than the `OVERLAY_VIEWPORT`).
514  // TODO: doctests, make safe to call from any state?
515  fn demo_toggle_quad_viewports (&mut self) {
516    if self.viewports.len() <= 2 {
517      // switch to four viewports from single viewport
518      // main viewport becomes "lower right"
519      let (
520        left_width, right_width, upper_height, lower_height, position,
521        position_2d, zoom_2d
522      ) = {
523        let lower_right   = &mut self.viewports[MAIN_VIEWPORT];
524        let window_width  = lower_right.rect().width;
525        let window_height = lower_right.rect().height;
526        let left_width    = left_width   (window_width);
527        let right_width   = right_width  (window_width);
528        let upper_height  = upper_height (window_height);
529        let lower_height  = lower_height (window_height);
530        lower_right.set_rect (glium::Rect {
531          width:  right_width,
532          height: lower_height,
533          left:   left_width,
534          bottom: 0
535        });
536        ( left_width, right_width, upper_height, lower_height,
537          lower_right.camera3d().unwrap().position(),
538          lower_right.camera2d().unwrap().position(),
539          lower_right.camera2d().unwrap().zoom()
540        )
541      };
542
543      let upper_left  = render::viewport::Builder::new (glium::Rect {
544        width:  left_width,
545        height: upper_height,
546        left:   0,
547        bottom: lower_height
548      }).with_zoom_2d (zoom_2d).with_position_2d (position_2d)
549        .orthographic_3d (1.0)
550        .with_pose_3d (
551          // looking down positive Y axis
552          math::Pose3 { position, angles: math::Angles3::default() })
553        .build();
554
555      let upper_right = render::viewport::Builder::new (glium::Rect {
556        width:  right_width,
557        height: upper_height,
558        left:   left_width,
559        bottom: lower_height,
560      }).with_zoom_2d (zoom_2d).with_position_2d (position_2d)
561        .orthographic_3d (1.0)
562        .with_pose_3d (
563          math::Pose3 {
564            position,
565            // looking down positive X axis
566            angles: math::Angles3::wrap (
567              math::Deg (-90.0).into(),
568              math::Rad (0.0),
569              math::Rad (0.0))
570          })
571        .build();
572
573      let lower_left  = render::viewport::Builder::new (glium::Rect {
574        width:  left_width,
575        height: lower_height,
576        left:   0,
577        bottom: 0,
578      }).with_zoom_2d (zoom_2d).with_position_2d (position_2d)
579        .orthographic_3d (1.0)
580        .with_pose_3d (
581          math::Pose3 {
582            position,
583            // looking down negative Z axis
584            angles: math::Angles3::wrap (
585              math::Rad (0.0),
586              math::Deg (-90.0).into(),
587              math::Rad (0.0))
588          })
589        .build();
590
591      assert!(self.viewports.insert (UPPER_LEFT_VIEWPORT, upper_left)
592        .is_none());
593      assert!(self.viewports.insert (UPPER_RIGHT_VIEWPORT, upper_right)
594        .is_none());
595      assert!(self.viewports.insert (LOWER_LEFT_VIEWPORT, lower_left)
596        .is_none());
597    } else {
598      // switch to single viewport from four viewports
599      assert!(self.viewports.len() == 4 || self.viewports.len() == 5);
600      assert!(self.viewports.remove (UPPER_LEFT_VIEWPORT) .is_some());
601      assert!(self.viewports.remove (UPPER_RIGHT_VIEWPORT).is_some());
602      assert!(self.viewports.remove (LOWER_LEFT_VIEWPORT) .is_some());
603      let main_viewport   = &mut self.viewports[MAIN_VIEWPORT];
604      let (width, height) = self.window.inner_size().into();
605      main_viewport.set_rect (glium::Rect {
606        width,
607        height,
608        left:   0,
609        bottom: 0
610      });
611    }
612    self.update_viewport_line_loops();
613  } // end fn toggle_quad_viewports
614}