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