Skip to main content

gl_utils/render/resource/
mod.rs

1//! A trait for resource types with arbitrary `draw` functionality and a
2//! default implementation that supports switching to a "quad viewport" mode.
3
4use std::iter::FromIterator;
5use std::ops::Deref;
6use std::sync::LazyLock;
7
8use glium;
9use image;
10use strum::{EnumCount, EnumIter, FromRepr};
11use vec_map::VecMap;
12use winit;
13
14use math_utils as math;
15
16use crate::{color, shader, texture, vertex, Render};
17
18pub mod draw2d;
19pub mod draw3d;
20#[cfg(feature="demo")]
21#[cfg_attr(docsrs, doc(cfg(feature="demo")))]
22pub mod demo;
23
24pub use self::draw2d::Draw2d;
25pub use self::draw3d::Draw3d;
26
27////////////////////////////////////////////////////////////////////////////////
28//  constants                                                                 //
29////////////////////////////////////////////////////////////////////////////////
30
31pub const MAIN_VIEWPORT        : usize = 0;
32pub const LOWER_RIGHT_VIEWPORT : usize = MAIN_VIEWPORT;
33pub const UPPER_LEFT_VIEWPORT  : usize = 1;
34pub const UPPER_RIGHT_VIEWPORT : usize = 2;
35pub const LOWER_LEFT_VIEWPORT  : usize = 3;
36pub const OVERLAY_VIEWPORT     : usize = 4;
37
38////////////////////////////////////////////////////////////////////////////////
39//  statics                                                                   //
40////////////////////////////////////////////////////////////////////////////////
41
42static DEFAULT_TEXTURES_16X16_BYTES : LazyLock <VecMap <&'static [u8]>> =
43  LazyLock::new (|| VecMap::from_iter ([
44    ( DefaultTexture16Id::Crosshair         as usize,
45      texture::CROSSHAIR_PNG_FILE_BYTES.as_slice()
46    ), (
47      DefaultTexture16Id::CrosshairInverse  as usize,
48      texture::CROSSHAIR_INVERSE_PNG_FILE_BYTES.as_slice()
49    )
50  ]));
51
52static DEFAULT_TEXTURES_POINTER_BYTES_OFFSETS :
53  LazyLock <VecMap <(&'static [u8], [i16; 2])>> = LazyLock::new (|| VecMap::from_iter ([
54    ( DefaultTexturePointerId::Hand as usize,
55      ( texture::POINTER_HAND_PNG_FILE_BYTES_OFFSET.0.as_slice(),
56        texture::POINTER_HAND_PNG_FILE_BYTES_OFFSET.1
57      )
58    )
59  ]));
60
61////////////////////////////////////////////////////////////////////////////////
62//  typedefs                                                                  //
63////////////////////////////////////////////////////////////////////////////////
64
65pub type PointerTextureIndexRepr = u16;
66
67////////////////////////////////////////////////////////////////////////////////
68//  traits                                                                    //
69////////////////////////////////////////////////////////////////////////////////
70
71/// Represents a generic Glium resource type for a renderer to use as a source
72/// of *drawing* (vertex rendering).
73///
74/// The resource should have all the shader programs, vertex buffers, textures,
75/// etc. necessary to make a `draw` call on a given `glium::Frame`.
76///
77/// The default frame functions will call the `draw_2d` and `draw_3d` methods,
78/// which do nothing by default.
79pub trait Resource {
80  fn new (glium_display : &glium::Display <glutin::surface::WindowSurface>)
81    -> Self;
82  fn init (_render : &mut Render <Self>) where Self : Sized
83    { /* default: do nothing */ }
84  /// Default replace self with `Self::new`. Can be overridden.
85  fn reset (render : &mut Render <Self>)  where Self : Sized {
86    render.resource = Self::new (&render.glium_display)
87  }
88  fn draw_2d (_render : &Render <Self>, _glium_frame : &mut glium::Frame) where
89    Self : Sized { /* default: do nothing */ }
90  fn draw_3d (_render : &Render <Self>, _glium_frame : &mut glium::Frame) where
91    Self : Sized { /* default: do nothing */ }
92}
93
94////////////////////////////////////////////////////////////////////////////////
95//  structs                                                                   //
96////////////////////////////////////////////////////////////////////////////////
97
98/// A default implementation of `Resource` containing the builtin shader programs and
99/// some pre-defined vertex sources.
100pub struct Default {
101  pub draw2d : Draw2d,
102  pub draw3d : Draw3d,
103  /// Textures of heterogeneous dimensions
104  pub textures_anysize        : VecMap <glium::texture::Texture2d>,
105  /// Custom pointer textures with offsets for non-centered cursors
106  pub textures_pointer : VecMap <(glium::texture::Texture2d, math::Vector2 <i16>)>,
107  /// 128x128 pixel tileset texture with 8x8 tile dimensions
108  pub tileset_128x128_texture : glium::texture::Texture2d,
109  /// 256x256 pixel tileset texture with 16x16 tile dimensions
110  pub tileset_256x256_texture : glium::texture::Texture2d,
111  // TODO: more tilesets?
112  shader_programs             : VecMap <glium::Program>,
113  /// Default 16x16 textures indexed by `DefaultTexture16Id`
114  default_textures_16x16      : glium::texture::Texture2dArray,
115  /// Texture array of 16x16 pixel general purpose textures
116  textures_16x16              : glium::texture::Texture2dArray,
117  /// Texture array of 64x64 pixel general purpose textures
118  textures_64x64              : glium::texture::Texture2dArray
119}
120
121////////////////////////////////////////////////////////////////////////////////
122//  enums                                                                     //
123////////////////////////////////////////////////////////////////////////////////
124
125#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, EnumCount, EnumIter,
126  FromRepr)]
127#[repr(u16)]
128pub enum DefaultTexturePointerId {
129  Hand
130}
131
132#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, EnumCount, EnumIter,
133  FromRepr)]
134#[repr(u16)]
135pub enum DefaultTexture16Id {
136  // for use with `BLEND_FUNC_NORMAL`
137  Crosshair,
138  // for use with `BLEND_FUNC_INVERT_COLOR`
139  CrosshairInverse
140}
141
142#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, EnumCount, EnumIter,
143  FromRepr)]
144#[repr(u16)]
145#[derive(Default)]
146pub enum DefaultTilesetId {
147  // for use with `BLEND_FUNC_NORMAL`
148  #[default]
149  EasciiAcorn128,
150  EasciiAcorn256,
151  // for use with `BLEND_FUNC_INVERT_COLOR`
152  EasciiAcornInverse128,
153  EasciiAcornInverse256
154}
155
156////////////////////////////////////////////////////////////////////////////////
157//  impls                                                                     //
158////////////////////////////////////////////////////////////////////////////////
159
160impl Default {
161  /// Default debug grid vertices (3): [X plane (red), Y plane (green), Z plane
162  /// (blue)]
163  pub fn debug_grid_vertices() -> [vertex::Vert3dOrientationScaleColor; 3] {
164    use std::f32::consts::FRAC_PI_2;
165    const HALF_GRID_DIMS : f32 = 0.5 * draw3d::MESH_GRID_DIMS as f32;
166    [
167      // X plane
168      vertex::Vert3dOrientationScaleColor {
169        position:    [HALF_GRID_DIMS, 0.0, HALF_GRID_DIMS],
170        orientation: (*math::Rotation3::from_angle_y (math::Rad (FRAC_PI_2)))
171          .into_col_arrays(),
172        scale:       [1.0, 1.0, 1.0],
173        color:       color::rgba_u8_to_rgba_f32 ([255, 0, 0, 255])
174      },
175      // Y plane
176      vertex::Vert3dOrientationScaleColor {
177        position:    [0.0, HALF_GRID_DIMS, HALF_GRID_DIMS],
178        orientation: (*math::Rotation3::from_angle_x (math::Rad (FRAC_PI_2)))
179          .into_col_arrays(),
180        scale:       [1.0, 1.0, 1.0],
181        color:       color::rgba_u8_to_rgba_f32 ([0, 255, 0, 255])
182      },
183      // Z plane
184      vertex::Vert3dOrientationScaleColor {
185        position:    [0.0, 0.0, 0.0],
186        orientation: math::Matrix3::identity().into_col_arrays(),
187        scale:       [1.0, 1.0, 1.0],
188        color:       color::rgba_u8_to_rgba_f32 ([0, 0, 255, 255])
189      }
190    ]
191  }
192  /// Returns tile pixel [width, height] of the tileset.
193  #[inline]
194  pub fn tile_dimensions (&self, tileset_id : DefaultTilesetId) -> [u32; 2] {
195    let (width, height) = match tileset_id {
196      DefaultTilesetId::EasciiAcorn128 =>
197        self.tileset_128x128_texture.dimensions(),
198      DefaultTilesetId::EasciiAcorn256 =>
199        self.tileset_256x256_texture.dimensions(),
200      _ => unimplemented!()
201    };
202    debug_assert_eq!(width  % 16, 0);
203    debug_assert_eq!(height % 16, 0);
204    [width / 16, height / 16]
205  }
206  #[inline]
207  pub const fn shader_programs (&self) -> &VecMap <glium::Program> {
208    &self.shader_programs
209  }
210  #[inline]
211  pub fn set_pointer_position (&mut self,
212    display  : &glium::Display <glutin::surface::WindowSurface>,
213    position : math::Point2 <f32>
214  ) {
215    let offset = self.draw2d.draw_pointer.map_or (
216      math::Vector2::zero(),
217      |texture_index| self.textures_pointer.get (texture_index as usize)
218        .unwrap().1.numcast().unwrap()
219    );
220    self.draw2d.set_pointer_vertex (display, position + offset);
221  }
222  #[inline]
223  pub fn set_textures_16x16 (&mut self,
224    textures_16x16 : glium::texture::Texture2dArray
225  ) {
226    assert_eq!(textures_16x16.width(),  16);
227    assert_eq!(textures_16x16.height(), 16);
228    self.textures_16x16 = textures_16x16;
229  }
230  #[inline]
231  pub fn set_textures_64x64 (&mut self,
232    textures_64x64 : glium::texture::Texture2dArray
233  ) {
234    assert_eq!(textures_64x64.width(),  64);
235    assert_eq!(textures_64x64.height(), 64);
236    self.textures_64x64 = textures_64x64;
237  }
238}
239
240impl Resource for Default {
241  fn new (glium_display : &glium::Display <glutin::surface::WindowSurface>) -> Self {
242    // shaders
243    let shader_programs  = shader::build_programs (glium_display).unwrap();
244    // shared resources
245    let default_textures_16x16 =
246      texture::texture2darray_with_mipmaps_from_bytes (
247        glium_display,
248        &DEFAULT_TEXTURES_16X16_BYTES.values().map (Deref::deref)
249          .collect::<Vec <&[u8]>>(),
250        image::ImageFormat::Png,
251        glium::texture::MipmapsOption::NoMipmap
252      ).unwrap();
253    let textures_16x16   =
254      glium::texture::Texture2dArray::empty (glium_display, 16, 16, 0).unwrap();
255    let textures_64x64   =
256      glium::texture::Texture2dArray::empty (glium_display, 64, 64, 0).unwrap();
257    let textures_anysize = VecMap::new();
258    let textures_pointer = DEFAULT_TEXTURES_POINTER_BYTES_OFFSETS.iter()
259      .map (|(i, (bytes, offset))|{
260        let texture = texture::texture2d_with_mipmaps_from_bytes (
261          glium_display, bytes, image::ImageFormat::Png,
262          glium::texture::MipmapsOption::NoMipmap
263        ).unwrap();
264        (i, (texture, math::Vector2::from (*offset)))
265      }).collect();
266    let tileset_128x128_texture = texture::texture2d_with_mipmaps_from_bytes (
267      glium_display,
268      // for use with normal blending
269      texture::TILESET_EASCII_ACORN_8X8_PNG_FILE_BYTES,
270      image::ImageFormat::Png,
271      glium::texture::MipmapsOption::NoMipmap
272    ).unwrap();
273    let tileset_256x256_texture = texture::texture2d_with_mipmaps_from_bytes (
274      glium_display,
275      // for use with normal blending
276      texture::TILESET_EASCII_ACORN_16X16_PNG_FILE_BYTES,
277      image::ImageFormat::Png,
278      glium::texture::MipmapsOption::NoMipmap
279    ).unwrap();
280    // TODO
281    /*
282    let tileset_128x128_inverse_texture =
283      texture::texture2d_with_mipmaps_from_bytes (
284        glium_display,
285        // for use with inverse blending
286        texture::TILESET_EASCII_ACORN_8X8_INVERSE_PNG_FILE_BYTES,
287        image::PNG,
288        glium::texture::MipmapsOption::NoMipmap
289      ).unwrap();
290    */
291
292    let draw2d = Draw2d::new (glium_display);
293    let draw3d = Draw3d::new (glium_display);
294
295    Default {
296      shader_programs,
297      default_textures_16x16,
298      textures_16x16,
299      textures_64x64,
300      textures_anysize,
301      textures_pointer,
302      tileset_128x128_texture,
303      tileset_256x256_texture,
304      draw2d,
305      draw3d
306    }
307  }
308
309  #[inline]
310  fn init (render : &mut Render <Self>) {
311    render.update_viewport_line_loops();
312  }
313
314  #[inline]
315  fn draw_2d (render : &Render <Self>, glium_frame : &mut glium::Frame) {
316    Draw2d::draw (render, glium_frame);
317  }
318
319  #[inline]
320  fn draw_3d (render : &Render <Self>, glium_frame : &mut glium::Frame) {
321    Draw3d::draw (render, glium_frame);
322  } // end fn draw_3d
323
324  #[inline]
325  fn reset (render : &mut Render <Self>) {
326    render.resource.draw2d = Draw2d::new (&render.glium_display);
327    render.resource.draw3d = Draw3d::new (&render.glium_display);
328  }
329} // end impl Resource for DefaultResource
330
331impl Render <Default> {
332
333  #[inline]
334  pub fn camera3d_position_set (&mut self, position : math::Point3 <f32>) {
335    for (_, viewport) in self.viewports.iter_mut() {
336      if viewport.camera3d().is_some() {
337        viewport.camera3d_set_position (position);
338      }
339    }
340  }
341
342  #[inline]
343  pub fn camera3d_orientation_set (&mut self,
344    orientation : math::Rotation3 <f32>
345  ) {
346    for (_, viewport) in self.viewports.iter_mut() {
347      if viewport.camera3d().is_some() {
348        viewport.camera3d_set_orientation (orientation);
349      }
350    }
351  }
352
353  #[inline]
354  pub fn camera3d_look_at (&mut self, target : math::Point3 <f32>) {
355    for (_, viewport) in self.viewports.iter_mut() {
356      if viewport.camera3d().is_some() {
357        viewport.camera3d_look_at (target);
358      }
359    }
360  }
361
362  pub fn camera3d_move_local_xy (&mut self, dx : f32, dy : f32, dz : f32) {
363    self.viewports[MAIN_VIEWPORT].camera3d_move_local_xy (dx, dy, dz);
364    let position = self.viewports[MAIN_VIEWPORT].camera3d().unwrap().position();
365    for (_, viewport) in self.viewports.iter_mut().skip (1) {
366      if viewport.camera3d().is_some() {
367        viewport.camera3d_set_position (position);
368      }
369    }
370  }
371
372  #[inline]
373  pub fn camera3d_rotate (&mut self,
374    dyaw : math::Rad <f32>, dpitch : math::Rad <f32>, droll : math::Rad <f32>
375  ) {
376    self.viewports[MAIN_VIEWPORT].camera3d_rotate (dyaw, dpitch, droll);
377  }
378
379  /// Scales 3D zoom of viewports 1-3.
380  ///
381  /// Does nothing in single viewport mode.
382  ///
383  /// # Panics
384  ///
385  /// Panics if scale is negative.
386  pub fn camera3d_orthographic_zoom_scale (&mut self, scale : f32) {
387    assert!(0.0 < scale);
388    for (_, viewport) in self.viewports.iter_mut().skip(1) {
389      if viewport.camera3d().is_some() {
390        debug_assert!(viewport.camera3d().unwrap().projection()
391          .is_orthographic());
392        viewport.camera3d_scale_fovy_or_zoom (scale);
393      }
394    }
395  }
396
397  /// Scales 3D zoom of viewport 0 (the main viewport) only.
398  ///
399  /// # Panics
400  ///
401  /// Panics if scale is negative.
402  pub fn camera3d_perspective_fovy_scale (&mut self, scale : f32) {
403    assert!(0.0 < scale);
404    debug_assert!(self.viewports[MAIN_VIEWPORT].camera3d().unwrap().projection()
405      .is_perspective());
406    self.viewports[MAIN_VIEWPORT].camera3d_scale_fovy_or_zoom (scale);
407  }
408
409  pub fn camera2d_zoom_set (&mut self, zoom : f32) {
410    assert!(0.0 < zoom);
411    for (_, viewport) in self.viewports.iter_mut() {
412      if viewport.camera2d().is_some() {
413        viewport.camera2d_set_zoom (zoom);
414      }
415    }
416    self.update_viewport_line_loops();
417  }
418
419  /// Modifies the 2D zoom by the given amount for all viewports.
420  pub fn camera2d_zoom_shift (&mut self, shift : f32) {
421    let mut dirty = false;
422    for (_, viewport) in self.viewports.iter_mut() {
423      if viewport.camera2d().is_some() {
424        let zoom = viewport.camera2d().unwrap().zoom() + shift;
425        if 0.0 < zoom {
426          viewport.camera2d_set_zoom (zoom);
427          dirty = true;
428        }
429      }
430    }
431    if dirty {
432      self.update_viewport_line_loops();
433    }
434  }
435
436  pub fn camera2d_move_local (&mut self, dx : f32, dy : f32) {
437    self.viewports[MAIN_VIEWPORT].camera2d_move_local (dx, dy);
438    let position = self.viewports[MAIN_VIEWPORT].camera2d().unwrap().position();
439    for (_, viewport) in self.viewports.iter_mut().skip (1) {
440      if viewport.camera2d().is_some() {
441        viewport.camera2d_set_position (position);
442      }
443    }
444  }
445
446  /// Moves origin for all viewports to lower left
447  pub fn camera2d_move_origin_to_bottom_left (&mut self) {
448    for (_, viewport) in self.viewports.iter_mut() {
449      if viewport.camera2d().is_some() {
450        viewport.camera2d_move_origin_to_bottom_left()
451      }
452    }
453    self.update_viewport_line_loops();
454  }
455
456  /// Resize the viewport(s) based on the `LogicalSize` that was returned by a
457  /// `WindowEvent::Resized` event
458  // TODO: come up with a better scheme for quad viewports, overlay viewports
459  pub fn window_resized (&mut self,
460    physical_size : winit::dpi::PhysicalSize <u32>
461  ) {
462    let (width, height) = physical_size.into();
463    if self.viewports.len() < 4 {
464      self.viewports[MAIN_VIEWPORT].set_rect (
465        glium::Rect { left: 0, bottom: 0, width, height }
466      );
467      self.viewports.get_mut (OVERLAY_VIEWPORT).map (|viewport|
468        viewport.set_rect (glium::Rect { left: 0, bottom: 0, width, height }));
469    } else {
470      // quad viewports
471      let left_width    = left_width   (width);
472      let right_width   = right_width  (width);
473      let upper_height  = upper_height (height);
474      let lower_height  = lower_height (height);
475      self.viewports[LOWER_RIGHT_VIEWPORT].set_rect (
476        glium::Rect {
477          width:  right_width,
478          height: lower_height,
479          left:   left_width,
480          bottom: 0
481        }
482      );
483      self.viewports[UPPER_LEFT_VIEWPORT].set_rect (
484        glium::Rect {
485          width:  left_width,
486          height: upper_height,
487          left:   0,
488          bottom: lower_height
489        }
490      );
491      self.viewports[UPPER_RIGHT_VIEWPORT].set_rect (
492        glium::Rect {
493          width:  right_width,
494          height: upper_height,
495          left:   left_width,
496          bottom: lower_height
497        }
498      );
499      self.viewports[LOWER_LEFT_VIEWPORT].set_rect (
500        glium::Rect {
501          width:  left_width,
502          height: lower_height,
503          left:   0,
504          bottom: 0
505        }
506      );
507      self.viewports.get_mut (OVERLAY_VIEWPORT).map (|viewport|
508        viewport.set_rect (glium::Rect { left: 0, bottom: 0, width, height }));
509    }
510    self.update_viewport_line_loops();
511  }
512
513  /// Updates vertex and index buffers for viewport line loops.
514  ///
515  /// Should be called whenever viewport sizes or 2D camera zoom or position
516  /// changes.
517  fn update_viewport_line_loops (&mut self) {
518    const VERTS_PER_VIEWPORT : usize = 4;
519    let num_vertices = VERTS_PER_VIEWPORT * self.viewports.len();
520    let vertices = {
521      let mut vertices = Vec::<vertex::Vert2d>::with_capacity (num_vertices);
522      for (_, viewport) in self.viewports.iter() {
523        if let Some (camera2d) = viewport.camera2d() {
524          // pixels are centered on 0.5 unit centers
525          let pixel_radius = 0.5 / camera2d.zoom();
526          let position     = camera2d.position();
527          let ortho        = camera2d.ortho();
528          let left         = position.0.x + ortho.left;
529          let bottom       = position.0.y + ortho.bottom;
530          let top          = position.0.y + ortho.top;
531          let right        = position.0.x + ortho.right;
532          vertices.append (&mut vec![
533            vertex::Vert2d {
534              position: [left  + pixel_radius, bottom + pixel_radius] },
535            vertex::Vert2d {
536              position: [left  + pixel_radius, top    - pixel_radius] },
537            vertex::Vert2d {
538              position: [right - pixel_radius, top    - pixel_radius] },
539            vertex::Vert2d {
540              position: [right - pixel_radius, bottom + pixel_radius] }
541          ]);
542        }
543      }
544      vertices
545    };
546    self.resource.draw2d
547      .line_loop_vertices_set (&self.glium_display, &vertices[..]);
548  }
549}
550
551
552////////////////////////////////////////////////////////////////////////////////
553//  private functions                                                         //
554////////////////////////////////////////////////////////////////////////////////
555
556// some functions to compute viewport widths for odd window dimensions:
557// right viewports will be 1 pixel wider on odd width windows and bottom
558// viewports will be 1 pixel taller on odd height windows
559
560#[inline]
561const fn left_width (window_width : u32) -> u32 {
562  window_width / 2
563}
564#[inline]
565const fn right_width (window_width : u32) -> u32 {
566  window_width / 2 + window_width % 2
567}
568#[inline]
569const fn upper_height (window_height : u32) -> u32 {
570  window_height / 2
571}
572#[inline]
573const fn lower_height (window_height : u32) -> u32 {
574  window_height / 2 + window_height % 2
575}