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//! 
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}