egaku2d/
lib.rs

1//! # Overview
2//!
3//! A library that lets you draw various simple 2d geometry primitives and sprites fast using
4//! vertex buffer objects with a safe api. Uses the builder pattern for a convinient api.
5//! The main design goal is to be able to draw thousands of shapes efficiently.
6//! Uses glutin and opengl es 3.0.
7//!
8//! ![](https://raw.githubusercontent.com/tiby312/egaku2d/master/assets/screenshot.gif)
9//!
10//! # Pipeline
11//!
12//! The egaku2d drawing pipeline works as follows:
13//!
14//! * 1. Pick a drawing type (a particular shape or a sprite) and set mandatory values for the particular shape or sprite.
15//! * 2. Build up a large group of verticies by calling **`add()`**
16//!     * 2.1 Optionally save off verticies to a static vbo on the gpu for fast drawing at a later time by calling **`save()`**.
17//! * 3. Send the vertex data to the gpu and set mandatory shader uniform values bt calling **`send_and_uniforms()`**
18//!     * 3.1 Set optional uniform values e.g. **`with_color()`**.
19//! * 4. Draw the verticies by calling **`draw()`**
20//!
21//! Additionally, there is a way to draw the vertices we saved off to the gpu.
22//! To do that, instead of steps 1 and 2, we use the saved off verticies,
23//! and then set the uniform values by valling **`uniforms()`** and then draw by calling **`draw()`**.
24//!
25//! Using this pipeline, the user can efficiently draw thousands of circles, for example, with the caveat that
26//! they all will be the same radius and color/transparency values. This api does not allow the user
27//! to efficiently draw thousands of circles where each circle has a different color or radius.
28//! This was a design decision to make each vertex as lightweight as possible (just a x and y position),
29//! making it more efficient to set and send to the gpu.
30//!
31//! # Key Design Goals
32//!
33//! The main goal was to make a very performat simple 2d graphics library.
34//! There is special focus on reducing traffic between the cpu and the gpu by using compact vertices,
35//! point sprites, and by allowing the user to save vertex data to the gpu on their own.
36//!
37//! Providing a safe api is also a goal. All draw functions require a mutable version to the canvas, ensuring
38//! they happen sequentially. The user is prevented from making multiple instances of the system using an atomic counter.
39//! The system also does not implement Send so that the drop calls from vertex buffers going out of scope happen sequentially
40//! as well. If the user were to call opengl functions on their own, then some safety guarentees might be lost.
41//! However, if the user does not, this api should be completely safe.
42//!
43//! Writing fast shader programs is a seconady goal. This is a 2d drawing library even though most of the hardware out there
44//! is made to handle 3d. This means that the gpu is most likely under-utilized with this library.
45//! Because of this, it was decided there is little point to make a non-rotatable sprite shader to save
46//! on gpu time, for example. Especially since the vertex layout is the same size (with 32bit alignment) (`[f32;2],i16,i16` vs `[f32;2],i16`),
47//! so there are no gains from having to send less data to the gpu.
48//!
49//! # Using Shapes
50//!
51//! The user can draw the following:
52//!
53//! Shape                     | Representation                        | Opengl Primitive Type
54//! --------------------------|---------------------------------------|-----------------
55//! Circles                   | `(point,radius)`                      | POINTS
56//! Axis Aligned Rectangles   | `(startx,endx,starty,endy)`           | TRIANGLES
57//! Axis Aligned Squares      | `(point,radius)`                      | POINTS
58//! Lines                     | `(point,point,thickness)`             | TRIANGLES
59//! Arrows                    | `(point_start,point_end,thickness)`   | TRIANGLES 
60//!   
61//! # Using Sprites
62//!
63//! This crate also allows the user to draw sprites. You can upload a tileset texture to the gpu and then draw thousands of sprites
64//! using a similar api to the shape drawing api.
65//! The sprites are point sprites drawn using the opengl POINTS primitive in order to cut down on the data
66//! that needs to be sent to the gpu.
67//!
68//! Each sprite vertex is composed of the following:
69//!
70//! * position:`[f32;2]`
71//! * index:`u16` - the user can index up to 256*256 different sprites in a tile set.
72//! * rotation:`u16` - this gets normalized to a float internally. The user passes a f32 float in radians.
73//!
74//! So each sprite vertex is compact at 4*3=12 bytes.
75//!
76//! Each texture object has functions to create this index from a x and y coordinate.
77//! On the gpu, the index will be split into a x and y coordinate.
78//! If the index is larger than texture.dim.x*texture.dim.y then it will be modded so that
79//! it can be mapped to a tile set. Therefore it is impossible for the index
80//! to have a 'invalid' value. But obviously, the user should be picking an index
81//! that maps to a valid tile in the tile set to begin with. 
82//!
83//! The rotation is normalized to a float on the gpu. The fact that the tile index has size u16,
84//! means you can have a texture with a mamimum of 256x256 tiles. The user simply passes a f32 through
85//! the api. The rotation is in radians with 0 being no rotation and grows with a clockwise rotation.
86//! 
87//!
88//! # Batch drawing
89//!
90//! While you can pretty efficiently draw thousands of objects by calling add() a bunch of times,
91//! you might already have all of the vertex data embeded somewhere, in which case it can seem
92//! wasteful to iterate through your data structure to just build up another list that is then sent
93//! to the gpu. egaku2d has `Batches` that lets you map verticies to an existing data structure that you might have.
94//! This lets us skip building up a new verticies list by sending your entire data structure to the gpu.
95//!
96//! The downside to this approach is that you might have the vertex data in a list, but it might not be
97//! tightly packed since you have a bunch of other  data associated with each element,
98//! in which case we might end up sending a lot of useless data to the gpu.
99//!
100//! Currently this is only supported for circle drawing.
101//!
102//! # View
103//!
104//! The top left corner is the origin (0,0) and x and y grow to the right and downwards respectively.
105//!
106//! In windowed mode, the dimenions of the window defaults to scale exactly to the world.
107//! For example, if the user made a window of size 800,600, and then drew a circle at 400,300, the
108//! circle would appear in the center of the window.
109//! Similarily, if the user had a monitor with a resolution of 800,600 and started in fullscreen mode,
110//! and drew a circle at 400,300, it would also appear in the center of the screen.
111//!
112//! The ratio between the scale of x and y are fixed to be 1:1 so that there is no distortion in the
113//! shapes. The user can manually set the scale either by x or y and the other axis is automaically inferred
114//! so that to keep a 1:1 ratio.
115//!
116//!  
117//!
118//! # Fullscreen
119//!
120//! Fullscreen is kept behind a feature gate since on certain platforms like wayland linux it does not work.
121//! I suspect this is a problem with glutin, so I have just disabled it for the time behing in the hope that
122//! once glutin leaves alpha it will work. I think the problem is that when the window is resized, I can't manually change
123//! the size of the context to match using resize().
124//!
125//! # Example
126//!
127//! ```rust,no_run
128//! use axgeom::*;
129//! let events_loop = glutin::event_loop::EventLoop::new();
130//! let mut glsys = egaku2d::WindowedSystem::new([600, 480], &events_loop,"test window");
131//!
132//! //Make a tileset texture from a png that has 64 different tiles.
133//! let food_texture = glsys.texture("food.png",[8,8]).unwrap();
134//!
135//! let canvas = glsys.canvas_mut();
136//!
137//! //Make the background dark gray.
138//! canvas.clear_color([0.2,0.2,0.2]);
139//!
140//! //Push some squares to a static vertex buffer object on the gpu.
141//! let rect_save = canvas.squares()
142//!   .add([40., 40.])
143//!   .add([40., 40.])
144//!   .save(canvas);
145//!
146//! //Draw the squares we saved.
147//! rect_save.uniforms(canvas,5.0).with_color([0.0, 1.0, 0.1, 0.5]).draw();
148//!
149//! //Draw some arrows.
150//! canvas.arrows(5.0)
151//!   .add([40., 40.], [40., 200.])
152//!   .add([40., 40.], [200., 40.])
153//!   .send_and_uniforms(canvas).draw();
154//!
155//! //Draw some circles.
156//! canvas.circles()
157//!   .add([5.,6.])
158//!   .add([7.,8.])
159//!   .add([9.,5.])
160//!   .send_and_uniforms(canvas,4.0).with_color([0., 1., 1., 0.1]).draw();
161//!
162//! //Draw some circles from f32 primitives.
163//! canvas.circles()
164//!   .add([5.,6.])
165//!   .add([7.,8.])
166//!   .add([9.,5.])
167//!   .send_and_uniforms(canvas,4.0).with_color([0., 1., 1., 0.1]).draw();
168//!
169//! //Draw the first tile in the top left corder of the texture.
170//! canvas.sprites().add([100.,100.],food_texture.coord_to_index([0,0]),3.14).send_and_uniforms(canvas,&food_texture,4.0).draw();
171//!
172//! //Swap buffers on the opengl context.
173//! glsys.swap_buffers();
174//! ```
175
176use egaku2d_core::axgeom;
177pub use glutin;
178use glutin::PossiblyCurrent;
179
180use egaku2d_core;
181use egaku2d_core::gl;
182
183pub use egaku2d_core::batch;
184pub use egaku2d_core::shapes;
185pub use egaku2d_core::sprite;
186pub use egaku2d_core::uniforms;
187pub use egaku2d_core::SimpleCanvas;
188use egaku2d_core::FixedAspectVec2;
189use egaku2d_core::AspectRatio;
190
191mod onein {
192    use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
193    static INSTANCES: AtomicUsize = AtomicUsize::new(0);
194
195    pub fn assert_only_one_instance() {
196        assert_eq!(
197            INSTANCES.fetch_add(1, SeqCst),
198            0,
199            "Cannot have multiple instances of the egaku2d system at the same time!"
200        );
201    }
202    pub fn decrement_one_instance() {
203        assert_eq!(
204            INSTANCES.fetch_sub(1, SeqCst),
205            1,
206            "The last egaku2d system object was not properly destroyed"
207        );
208    }
209}
210
211///A timer to determine how often to refresh the screen.
212///You pass it the desired refresh rate, then you can poll
213///with is_ready() to determine if it is time to refresh.
214pub struct RefreshTimer {
215    interval: usize,
216    last_time: std::time::Instant,
217}
218impl RefreshTimer {
219    pub fn new(interval: usize) -> RefreshTimer {
220        RefreshTimer {
221            interval,
222            last_time: std::time::Instant::now(),
223        }
224    }
225    pub fn is_ready(&mut self) -> bool {
226        if self.last_time.elapsed().as_millis() >= self.interval as u128 {
227            self.last_time = std::time::Instant::now();
228            true
229        } else {
230            false
231        }
232    }
233}
234
235///Unlike a windowed system, we do not have control over the dimensions of the
236///window we end up with.
237///After construction, the user must set the viewport using the window dimension
238///information.
239#[cfg(feature = "fullscreen")]
240pub use self::fullscreen::FullScreenSystem;
241#[cfg(feature = "fullscreen")]
242pub mod fullscreen {
243    use super::*;
244
245    impl Drop for FullScreenSystem {
246        fn drop(&mut self) {
247            onein::decrement_one_instance();
248        }
249    }
250
251    pub struct FullScreenSystem {
252        inner: SimpleCanvas,
253        window_dim: FixedAspectVec2,
254        windowed_context: Option<glutin::WindowedContext<PossiblyCurrent>>,
255    }
256    impl FullScreenSystem {
257        pub fn new(events_loop: &glutin::event_loop::EventLoop<()>) -> Self {
258            onein::assert_only_one_instance();
259
260            use glutin::window::Fullscreen;
261            let fullscreen = Fullscreen::Borderless(prompt_for_monitor(events_loop));
262
263            let gl_window = glutin::window::WindowBuilder::new().with_fullscreen(Some(fullscreen));
264
265            //we are targeting only opengl 3.0 es. and glsl 300 es.
266
267            let windowed_context = glutin::ContextBuilder::new()
268                .with_gl(glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (3, 0)))
269                .with_vsync(true)
270                .build_windowed(gl_window, &events_loop)
271                .unwrap();
272
273            let windowed_context = unsafe { windowed_context.make_current().unwrap() };
274
275            let glutin::dpi::PhysicalSize { width, height } =
276                windowed_context.window().inner_size();
277
278            // Load the OpenGL function pointers
279            gl::load_with(|symbol| windowed_context.get_proc_address(symbol) as *const _);
280            assert_eq!(unsafe { gl::GetError() }, gl::NO_ERROR);
281
282            let window_dim = axgeom::FixedAspectVec2 {
283                ratio: AspectRatio(vec2(width as f64, height as f64)),
284                width: width as f64,
285            };
286
287            let windowed_context = Some(windowed_context);
288
289            let mut f = FullScreenSystem {
290                windowed_context,
291                window_dim,
292                inner: unsafe { SimpleCanvas::new(window_dim) },
293            };
294
295            f.set_viewport_from_width(width as f32);
296
297            f
298        }
299
300        //After this is called, you should update the viewport!!!!
301        pub fn update_window_dim(&mut self) {
302            let dpi = self
303                .windowed_context
304                .as_ref()
305                .unwrap()
306                .window()
307                .scale_factor();
308
309            let size = self
310                .windowed_context
311                .as_ref()
312                .unwrap()
313                .window()
314                .inner_size();
315
316            println!("resizing context!!! {:?}", (dpi, size));
317
318            self.windowed_context.as_mut().unwrap().resize(size);
319            self.window_dim = axgeom::FixedAspectVec2 {
320                ratio: AspectRatio(vec2(size.width as f64, size.height as f64)),
321                width: size.width as f64,
322            };
323
324            let ctx = unsafe {
325                self.windowed_context
326                    .take()
327                    .unwrap()
328                    .make_not_current()
329                    .unwrap()
330            };
331
332            self.windowed_context = Some(unsafe { ctx.make_current().unwrap() });
333        }
334
335        pub fn set_viewport_from_width(&mut self, width: f32) {
336            self.inner.set_viewport(self.window_dim, width);
337        }
338
339        pub fn set_viewport_min(&mut self, d: f32) {
340            if self.get_dim().x < self.get_dim().y {
341                self.set_viewport_from_width(d);
342            } else {
343                self.set_viewport_from_height(d);
344            }
345        }
346
347        pub fn set_viewport_from_height(&mut self, height: f32) {
348            let width = self.window_dim.ratio.width_over_height() as f32 * height;
349            self.inner.set_viewport(self.window_dim, width);
350        }
351
352        ///Creates a new texture from the specified file.
353        ///The fact that we need a mutable reference to this object
354        ///Ensures that we make the texture in the same thread.
355        ///The grid dimensions passed are the tile dimensions is
356        ///the texture is a tile set.
357        pub fn texture(
358            &mut self,
359            file: &str,
360            grid_dim: [u8; 2],
361        ) -> image::ImageResult<sprite::Texture> {
362            crate::texture(file, grid_dim)
363        }
364
365        pub fn canvas(&self) -> &SimpleCanvas {
366            &self.inner
367        }
368        pub fn canvas_mut(&mut self) -> &mut SimpleCanvas {
369            &mut self.inner
370        }
371
372        pub fn get_dim(&self) -> Vec2<usize> {
373            self.window_dim.as_vec().inner_as()
374        }
375        pub fn swap_buffers(&mut self) {
376            self.windowed_context
377                .as_mut()
378                .unwrap()
379                .swap_buffers()
380                .unwrap();
381            assert_eq!(unsafe { gl::GetError() }, gl::NO_ERROR);
382        }
383    }
384}
385
386///A version where the user can control the size of the window.
387pub struct WindowedSystem {
388    inner: SimpleCanvas,
389    window_dim: FixedAspectVec2,
390    windowed_context: glutin::WindowedContext<PossiblyCurrent>,
391}
392
393impl Drop for WindowedSystem {
394    fn drop(&mut self) {
395        onein::decrement_one_instance();
396    }
397}
398
399impl WindowedSystem {
400    pub fn new(
401        dim: [usize; 2],
402        events_loop: &glutin::event_loop::EventLoop<()>,
403        title: &str,
404    ) -> WindowedSystem {
405        onein::assert_only_one_instance();
406
407        let dim = axgeom::vec2(dim[0], dim[1]);
408        let dim = dim.inner_as::<f32>();
409
410        let game_world = axgeom::Rect::new(0.0, dim.x, 0.0, dim.y);
411
412        let width = game_world.x.distance() as f64;
413        let height = game_world.y.distance() as f64;
414
415        let monitor = prompt_for_monitor(events_loop);
416        let dpi = monitor.scale_factor();
417        let p: glutin::dpi::LogicalSize<f64> =
418            glutin::dpi::PhysicalSize { width, height }.to_logical(dpi);
419
420        let gl_window = glutin::window::WindowBuilder::new()
421            .with_inner_size(p)
422            .with_resizable(false)
423            .with_title(title);
424
425        //we are targeting only opengl 3.0 es. and glsl 300 es.
426
427        let windowed_context = glutin::ContextBuilder::new()
428            .with_gl(glutin::GlRequest::Specific(glutin::Api::OpenGlEs, (3, 0)))
429            .with_vsync(true)
430            .build_windowed(gl_window, &events_loop)
431            .unwrap();
432
433        let windowed_context = unsafe { windowed_context.make_current().unwrap() };
434
435        // Load the OpenGL function pointers
436        gl::load_with(|symbol| windowed_context.get_proc_address(symbol) as *const _);
437        assert_eq!(unsafe { gl::GetError() }, gl::NO_ERROR);
438
439        //let dpi = windowed_context.window().scale_factor();
440        let glutin::dpi::PhysicalSize { width, height } = windowed_context.window().inner_size();
441        assert_eq!(width as usize, dim.x as usize);
442        assert_eq!(height as usize, dim.y as usize);
443
444        let window_dim = FixedAspectVec2 {
445            ratio: AspectRatio(axgeom::vec2(width as f64, height as f64)),
446            width: width as f64,
447        };
448
449        WindowedSystem {
450            windowed_context,
451            window_dim,
452            inner: unsafe { SimpleCanvas::new(window_dim) },
453        }
454    }
455
456    pub fn set_viewport_from_width(&mut self, width: f32) {
457        self.inner.set_viewport(self.window_dim, width);
458    }
459
460    pub fn set_viewport_min(&mut self, d: f32) {
461        if self.get_dim()[0] < self.get_dim()[1] {
462            self.set_viewport_from_width(d);
463        } else {
464            self.set_viewport_from_height(d);
465        }
466    }
467
468    pub fn set_viewport_from_height(&mut self, height: f32) {
469        let width = self.window_dim.ratio.width_over_height() as f32 * height;
470        self.inner.set_viewport(self.window_dim, width);
471    }
472
473    pub fn get_dim(&self) -> [usize; 2] {
474        let v = self.window_dim.as_vec().inner_as();
475        [v.x, v.y]
476    }
477
478    ///Creates a new texture from the specified file.
479    ///The fact that we need a mutable reference to this object
480    ///Ensures that we make the texture in the same thread.
481    ///The grid dimensions passed are the tile dimensions is
482    ///the texture is a tile set.
483    pub fn texture(
484        &mut self,
485        file: &str,
486        grid_dim: [u8; 2],
487    ) -> image::ImageResult<sprite::Texture> {
488        crate::texture(file, grid_dim)
489    }
490
491    pub fn canvas(&self) -> &SimpleCanvas {
492        &self.inner
493    }
494    pub fn canvas_mut(&mut self) -> &mut SimpleCanvas {
495        &mut self.inner
496    }
497    pub fn swap_buffers(&mut self) {
498        self.windowed_context.swap_buffers().unwrap();
499        assert_eq!(unsafe { gl::GetError() }, gl::NO_ERROR);
500    }
501}
502
503use glutin::event_loop::EventLoop;
504use glutin::monitor::MonitorHandle;
505
506// Enumerate monitors and prompt user to choose one
507fn prompt_for_monitor(el: &EventLoop<()>) -> MonitorHandle {
508    let num = 0;
509    let monitor = el
510        .available_monitors()
511        .nth(num)
512        .expect("Please enter a valid ID");
513
514    monitor
515}
516
517use egaku2d_core::gl::types::GLuint;
518use egaku2d_core::gl_ok;
519use egaku2d_core::sprite::*;
520
521///Creates a new texture from the specified file.
522///The fact that we need a mutable reference to this object
523///Ensures that we make the texture in the same thread.
524///The grid dimensions passed are the tile dimensions is
525///the texture is a tile set.
526fn texture(file: &str, grid_dim: [u8; 2]) -> image::ImageResult<sprite::Texture> {
527    match image::open(&file.to_string()) {
528        Err(err) => Err(err),
529        Ok(img) => {
530            use image::GenericImageView;
531
532            let (width, height) = img.dimensions();
533
534            let img = match img {
535                image::DynamicImage::ImageRgba8(img) => img,
536                img => img.to_rgba(),
537            };
538
539            let id = build_opengl_mipmapped_texture(width, height, img);
540            Ok(unsafe { Texture::new(id, grid_dim, [width as f32, height as f32]) })
541        }
542    }
543}
544
545fn build_opengl_mipmapped_texture(width: u32, height: u32, image: image::RgbaImage) -> GLuint {
546    unsafe {
547        let mut texture_id: GLuint = 0;
548        gl::GenTextures(1, &mut texture_id);
549        gl_ok!();
550
551        gl::BindTexture(gl::TEXTURE_2D, texture_id);
552        gl_ok!();
553
554        let raw = image.into_raw();
555
556        gl::TexImage2D(
557            gl::TEXTURE_2D,
558            0,
559            gl::RGBA as i32,
560            width as i32,
561            height as i32,
562            0,
563            gl::RGBA,
564            gl::UNSIGNED_BYTE,
565            raw.as_ptr() as *const _,
566        );
567        gl_ok!();
568
569        //TODO convert these into options? with_linear() with_nearest() ??
570        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::NEAREST as i32);
571        gl_ok!();
572        gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::NEAREST as i32);
573        gl_ok!();
574
575        gl::BindTexture(gl::TEXTURE_2D, 0);
576        gl_ok!();
577
578        texture_id
579    }
580}