Skip to main content

lumenpyx/
animation.rs

1use glium::texture;
2
3use crate::load_image;
4use crate::primitives::{Normal, Sprite, Texture, TextureInput};
5use crate::TextureHandle;
6use crate::Transform;
7use crate::{drawable_object::Drawable, LumenpyxProgram};
8use glium::Surface;
9use std::iter::zip;
10use std::time::{Duration, Instant};
11
12#[derive(Clone)]
13pub struct Animation {
14    sprites: Vec<Sprite>,
15    time_between_frames: Duration,
16    time: AnimationTimeElapsed,
17    shadow_strength: f32,
18    pub transform: Transform,
19    /// If true, the animation will loop, if false, the animation will not draw after the last frame
20    loop_animation: bool,
21}
22
23impl Animation {
24    /// Takes a path to a series of images in format `path1.png`, `path2.png`, etc.
25    pub fn new_from_images(
26        albedo: Texture,
27        height: Texture,
28        roughness: Texture,
29        normal: Normal,
30        num_frames: usize,
31        time_between_frames: Duration,
32        transform: Transform,
33        program: &mut LumenpyxProgram,
34        loop_animation: bool,
35    ) -> (
36        Self,
37        Vec<TextureHandle>,
38        Vec<TextureHandle>,
39        Vec<TextureHandle>,
40        Vec<TextureHandle>,
41    ) {
42        let mut albedo_textures = load_tex_from_images_albedo(albedo, num_frames, program);
43        let mut height_textures =
44            load_tex_from_images_non_albedo(&albedo_textures, height, program);
45        let mut roughness_textures =
46            load_tex_from_images_non_albedo(&albedo_textures, roughness, program);
47        let mut normal_textures =
48            load_tex_from_images_normal(&albedo_textures, &height_textures, normal, program);
49
50        if albedo_textures.len() != num_frames
51            || height_textures.len() != num_frames
52            || roughness_textures.len() != num_frames
53            || normal_textures.len() != num_frames
54        {
55            panic!("The number of frames in the images must be the same");
56        }
57
58        let mut sprites = vec![];
59        let mut albedo_handles = vec![];
60        let mut height_handles = vec![];
61        let mut roughness_handles = vec![];
62        let mut normal_handles = vec![];
63        for _ in 0..num_frames {
64            let albedo_texture = albedo_textures.remove(0);
65            let height_texture = height_textures.remove(0);
66            let roughness_texture = roughness_textures.remove(0);
67            let normal_texture = normal_textures.remove(0);
68
69            let (sprite, albedo_handle, height_handle, roughness_handle, normal_handle) =
70                Sprite::new(
71                    albedo_texture.into(),
72                    height_texture.into(),
73                    roughness_texture.into(),
74                    normal_texture.into(),
75                    program,
76                    transform,
77                );
78            albedo_handles.push(albedo_handle);
79            height_handles.push(height_handle);
80            roughness_handles.push(roughness_handle);
81            normal_handles.push(normal_handle);
82            sprites.push(sprite);
83        }
84
85        (
86            Self {
87                sprites,
88                time_between_frames,
89                time: Instant::now().into(),
90                shadow_strength: 0.5,
91                transform,
92                loop_animation,
93            },
94            albedo_handles,
95            height_handles,
96            roughness_handles,
97            normal_handles,
98        )
99    }
100
101    /// Takes a path to a spritesheet
102    /// returns an Animation object and the handles to the textures in the order of albedo, height, roughness, normal
103    pub fn new_from_spritesheet(
104        albedo: Texture,
105        height: Texture,
106        roughness: Texture,
107        normal: Normal,
108        num_frames: usize,
109        time_between_frames: Duration,
110        transform: Transform,
111        program: &mut LumenpyxProgram,
112        loop_animation: bool,
113    ) -> (
114        Self,
115        Vec<TextureHandle>,
116        Vec<TextureHandle>,
117        Vec<TextureHandle>,
118        Vec<TextureHandle>,
119    ) {
120        let mut albedo_textures = load_albedo_from_spritesheet(albedo, num_frames, program);
121        let mut height_textures =
122            load_non_albedo_from_spritesheet(&albedo_textures, height, program);
123        let mut roughness_textures =
124            load_non_albedo_from_spritesheet(&albedo_textures, roughness, program);
125        let mut normal_textures =
126            load_normal_from_spritesheet(&albedo_textures, &height_textures, normal, program);
127
128        if albedo_textures.len() != num_frames
129            || height_textures.len() != num_frames
130            || roughness_textures.len() != num_frames
131            || normal_textures.len() != num_frames
132        {
133            panic!("The number of frames in the spritesheets must be the same");
134        }
135
136        let mut sprites = vec![];
137        let mut albedo_handles = vec![];
138        let mut height_handles = vec![];
139        let mut roughness_handles = vec![];
140        let mut normal_handles = vec![];
141        for _ in 0..num_frames {
142            let albedo_texture = albedo_textures.remove(0);
143            let height_texture = height_textures.remove(0);
144            let roughness_texture = roughness_textures.remove(0);
145            let normal_texture = normal_textures.remove(0);
146
147            let (sprite, albedo_handle, height_handle, roughness_handle, normal_handle) =
148                Sprite::new(
149                    albedo_texture.into(),
150                    height_texture.into(),
151                    roughness_texture.into(),
152                    normal_texture.into(),
153                    program,
154                    transform,
155                );
156
157            albedo_handles.push(albedo_handle);
158            height_handles.push(height_handle);
159            roughness_handles.push(roughness_handle);
160            normal_handles.push(normal_handle);
161            sprites.push(sprite);
162        }
163
164        (
165            Self {
166                sprites,
167                time_between_frames,
168                time: Instant::now().into(),
169                shadow_strength: 0.5,
170                transform,
171                loop_animation,
172            },
173            albedo_handles,
174            height_handles,
175            roughness_handles,
176            normal_handles,
177        )
178    }
179
180    pub fn new_from_handles(
181        albedo: Vec<TextureHandle>,
182        height: Vec<TextureHandle>,
183        roughness: Vec<TextureHandle>,
184        normal: Vec<TextureHandle>,
185        program: &mut LumenpyxProgram,
186        time_between_frames: Duration,
187        transform: Transform,
188        loop_animation: bool,
189    ) -> Self {
190        let mut sprites = vec![];
191        for i in 0..albedo.len() {
192            let (sprite, _, _, _, _) = Sprite::new(
193                albedo[i].clone().into(),
194                height[i].clone().into(),
195                roughness[i].clone().into(),
196                normal[i].clone().into(),
197                program,
198                transform,
199            );
200            sprites.push(sprite);
201        }
202
203        Self {
204            sprites,
205            time_between_frames,
206            time: Instant::now().into(),
207            shadow_strength: 0.5,
208            transform,
209            loop_animation,
210        }
211    }
212
213    pub fn restart_animation(&mut self) {
214        self.time = Instant::now().into();
215    }
216
217    pub fn set_time(&mut self, time: AnimationTimeElapsed) {
218        self.time = time;
219    }
220}
221
222#[derive(Clone)]
223pub enum AnimationTimeElapsed {
224    Time(Duration),
225    SecondsSinceStart(f32),
226    TimeSinceInstant(Instant),
227}
228
229impl AnimationTimeElapsed {
230    pub fn as_nanos(&self) -> u128 {
231        match self {
232            AnimationTimeElapsed::Time(time) => time.as_nanos(),
233            AnimationTimeElapsed::SecondsSinceStart(seconds) => {
234                (*seconds as f64 * 1_000_000_000.0) as u128
235            }
236            AnimationTimeElapsed::TimeSinceInstant(instant) => instant.elapsed().as_nanos(),
237        }
238    }
239
240    pub fn as_secs_f32(&self) -> f32 {
241        match self {
242            AnimationTimeElapsed::Time(time) => time.as_secs_f32(),
243            AnimationTimeElapsed::SecondsSinceStart(seconds) => *seconds,
244            AnimationTimeElapsed::TimeSinceInstant(instant) => instant.elapsed().as_secs_f32(),
245        }
246    }
247}
248
249impl Into<AnimationTimeElapsed> for Duration {
250    fn into(self) -> AnimationTimeElapsed {
251        AnimationTimeElapsed::Time(self)
252    }
253}
254
255impl Into<AnimationTimeElapsed> for f32 {
256    fn into(self) -> AnimationTimeElapsed {
257        AnimationTimeElapsed::SecondsSinceStart(self)
258    }
259}
260
261impl Into<AnimationTimeElapsed> for Instant {
262    fn into(self) -> AnimationTimeElapsed {
263        AnimationTimeElapsed::TimeSinceInstant(self)
264    }
265}
266
267impl Drawable for Animation {
268    fn draw_albedo(
269        &self,
270        program: &LumenpyxProgram,
271        transform: &Transform,
272        albedo_framebuffer: &mut glium::framebuffer::SimpleFrameBuffer,
273    ) {
274        let mut current_frame_num = self
275            .time
276            .as_nanos()
277            .checked_div(self.time_between_frames.as_nanos())
278            .expect("time between frames on an animation cannot be set to 0");
279
280        if current_frame_num as usize >= self.sprites.len() {
281            if self.loop_animation {
282                current_frame_num = current_frame_num % self.sprites.len() as u128;
283            } else {
284                return;
285            }
286        }
287
288        let current_frame = &self.sprites[current_frame_num as usize];
289
290        current_frame.draw_albedo(program, transform, albedo_framebuffer);
291    }
292
293    fn draw_height(
294        &self,
295        program: &LumenpyxProgram,
296        transform: &Transform,
297        height_framebuffer: &mut glium::framebuffer::SimpleFrameBuffer,
298    ) {
299        let mut current_frame_num = self
300            .time
301            .as_nanos()
302            .checked_div(self.time_between_frames.as_nanos())
303            .expect("time between frames on an animation cannot be set to 0");
304
305        if current_frame_num as usize >= self.sprites.len() {
306            if self.loop_animation {
307                current_frame_num = current_frame_num % self.sprites.len() as u128;
308            } else {
309                return;
310            }
311        }
312
313        let current_frame = &self.sprites[current_frame_num as usize];
314
315        current_frame.draw_height(program, transform, height_framebuffer);
316    }
317
318    fn draw_roughness(
319        &self,
320        program: &LumenpyxProgram,
321        transform: &Transform,
322        roughness_framebuffer: &mut glium::framebuffer::SimpleFrameBuffer,
323    ) {
324        let mut current_frame_num = self
325            .time
326            .as_nanos()
327            .checked_div(self.time_between_frames.as_nanos())
328            .expect("time between frames on an animation cannot be set to 0");
329
330        if current_frame_num as usize >= self.sprites.len() {
331            if self.loop_animation {
332                current_frame_num = current_frame_num % self.sprites.len() as u128;
333            } else {
334                return;
335            }
336        }
337
338        let current_frame = &self.sprites[current_frame_num as usize];
339
340        current_frame.draw_roughness(program, transform, roughness_framebuffer);
341    }
342
343    fn draw_normal(
344        &self,
345        program: &LumenpyxProgram,
346        transform: &Transform,
347        normal_framebuffer: &mut glium::framebuffer::SimpleFrameBuffer,
348    ) {
349        let mut current_frame_num = self
350            .time
351            .as_nanos()
352            .checked_div(self.time_between_frames.as_nanos())
353            .expect("time between frames on an animation cannot be set to 0");
354
355        if current_frame_num as usize >= self.sprites.len() {
356            if self.loop_animation {
357                current_frame_num = current_frame_num % self.sprites.len() as u128;
358            } else {
359                return;
360            }
361        }
362
363        let current_frame = &self.sprites[current_frame_num as usize];
364
365        current_frame.draw_normal(program, transform, normal_framebuffer);
366    }
367
368    fn try_load_shaders(&self, program: &mut LumenpyxProgram) {
369        for sprite in &self.sprites {
370            sprite.try_load_shaders(program);
371        }
372    }
373
374    fn get_transform(&self) -> Transform {
375        self.transform
376    }
377
378    fn get_recieve_shadows_strength(&self) -> f32 {
379        self.shadow_strength
380    }
381
382    fn set_transform(&mut self, transform: Transform) {
383        self.transform = transform;
384    }
385}
386
387/// splits a texture into multiple textures, one for each frame
388fn load_textures_from_spritesheet_tex(
389    texture: &glium::Texture2d,
390    num_frames: usize,
391    program: &LumenpyxProgram,
392) -> Vec<glium::Texture2d> {
393    let texture_framebuffer = glium::framebuffer::SimpleFrameBuffer::new(&program.display, texture)
394        .expect("failed to create texture framebuffer when creating animation from spritesheet");
395
396    // split the image into frames
397    let frame_width = texture.width() / num_frames as u32;
398    let frame_height = texture.height();
399
400    let mut textures = Vec::new();
401
402    for i in 0..num_frames {
403        let new_texture = texture::Texture2d::empty_with_format(
404            &program.display,
405            texture::UncompressedFloatFormat::U8U8U8U8,
406            texture::MipmapsOption::NoMipmap,
407            frame_width,
408            frame_height,
409        )
410        .expect("failed to create texture when creating animation from spritesheet");
411
412        let new_texture_framebuffer =
413            glium::framebuffer::SimpleFrameBuffer::new(&program.display, &new_texture).expect(
414                "failed to create texture framebuffer when creating animation from spritesheet",
415            );
416
417        let dest_rect = &glium::Rect {
418            left: (i as i32 * frame_width as i32) as u32,
419            bottom: 0,
420            width: frame_width as u32,
421            height: frame_height as u32,
422        };
423
424        let target_rect = &glium::BlitTarget {
425            left: 0,
426            bottom: 0,
427            width: frame_width as i32,
428            height: frame_height as i32,
429        };
430
431        texture_framebuffer.blit_color(
432            dest_rect,
433            &new_texture_framebuffer,
434            target_rect,
435            glium::uniforms::MagnifySamplerFilter::Nearest,
436        );
437
438        textures.push(new_texture);
439    }
440
441    textures
442}
443
444fn load_textures_from_spritesheet_path(
445    path: &str,
446    num_frames: usize,
447    program: &LumenpyxProgram,
448) -> Vec<glium::Texture2d> {
449    let image = load_image(path);
450    let texture = texture::Texture2d::new(&program.display, image)
451        .expect("failed to create texture when creating animation from spritesheet");
452
453    return load_textures_from_spritesheet_tex(&texture, num_frames, program);
454}
455
456fn load_albedo_from_spritesheet(
457    texture: Texture,
458    num_frames: usize,
459    program: &LumenpyxProgram,
460) -> Vec<glium::Texture2d> {
461    match texture {
462        Texture::Path(path) => load_textures_from_spritesheet_path(&path, num_frames, program),
463        _ => panic!("The albedo texture must be a path to a spritesheet"),
464    }
465}
466
467fn load_non_albedo_from_spritesheet(
468    albedo_textures: &Vec<glium::Texture2d>,
469    texture: Texture,
470    program: &LumenpyxProgram,
471) -> Vec<glium::Texture2d> {
472    match texture {
473        Texture::Path(path) => {
474            load_textures_from_spritesheet_path(&path, albedo_textures.len(), program)
475        }
476        Texture::Texture(texture) => {
477            load_textures_from_spritesheet_tex(&texture, albedo_textures.len(), program)
478        }
479        _ => {
480            let mut textures = vec![];
481            for albedo_texture in albedo_textures {
482                let new_texture = crate::primitives::new_non_albedo_texture(
483                    program,
484                    texture.try_clone(),
485                    &albedo_texture,
486                );
487
488                textures.push(new_texture);
489            }
490            textures
491        }
492    }
493}
494
495fn load_normal_from_spritesheet(
496    albedo_textures: &Vec<glium::Texture2d>,
497    height_textures: &Vec<glium::Texture2d>,
498    normal: Normal,
499    program: &LumenpyxProgram,
500) -> Vec<glium::Texture2d> {
501    match normal {
502        Normal::Path(path) => {
503            load_textures_from_spritesheet_path(&path, albedo_textures.len(), program)
504        }
505        _ => {
506            let mut textures = vec![];
507            for (albedo_texture, height_texture) in zip(albedo_textures, height_textures) {
508                let new_texture = crate::primitives::new_normal_texture(
509                    program,
510                    normal.try_clone(),
511                    height_texture,
512                    &albedo_texture,
513                );
514
515                textures.push(new_texture);
516            }
517            textures
518        }
519    }
520}
521
522/// Loads multiple images from a path in format `path1.png`, `path2.png`, etc.
523fn load_tex_from_images_path(
524    albedo_path: &str,
525    num_frames: usize,
526    program: &LumenpyxProgram,
527) -> Vec<glium::Texture2d> {
528    let path_parts;
529    {
530        let mut path_parts_fully_split = albedo_path.split_inclusive('.').collect::<Vec<&str>>();
531        let mut path_parts_new: [String; 2] = ["".to_string(), "".to_string()];
532
533        path_parts_new[1] = path_parts_fully_split
534            .pop()
535            .expect("Path must have a file extension")
536            .to_string();
537
538        for part in path_parts_fully_split.iter() {
539            path_parts_new[0] += part;
540        }
541
542        path_parts = path_parts_new;
543    }
544
545    if path_parts.len() != 2 {
546        panic!("Path must be in format `path1.png`, `path2.png`, etc.");
547    }
548
549    let mut textures = Vec::new();
550    for i in 0..num_frames {
551        let mut actual_path = path_parts[0].clone();
552        actual_path.remove(actual_path.len() - 1);
553        let file_extension = path_parts[1].clone();
554        let full_path = format!(
555            "{}{}.{}",
556            actual_path, // remove the last character which is the .
557            i + 1,
558            file_extension
559        );
560
561        let image = load_image(&full_path);
562        let texture = texture::Texture2d::new(&program.display, image)
563            .expect("failed to create texture when creating animation from images");
564
565        textures.push(texture);
566    }
567
568    textures
569}
570
571fn load_tex_from_images_albedo(
572    albedo: Texture,
573    num_frames: usize,
574    program: &LumenpyxProgram,
575) -> Vec<glium::Texture2d> {
576    match albedo {
577        Texture::Path(path) => load_tex_from_images_path(&path, num_frames, program),
578        _ => panic!("The albedo texture must be a path to a series of images"),
579    }
580}
581
582fn load_tex_from_images_non_albedo(
583    albedo_textures: &Vec<glium::Texture2d>,
584    texture: Texture,
585    program: &LumenpyxProgram,
586) -> Vec<glium::Texture2d> {
587    match texture {
588        Texture::Path(path) => load_tex_from_images_path(&path, albedo_textures.len(), program),
589        Texture::Texture(texture) => panic!("Not sure how to handle this yet as the meaning is sort of ambiguous, if you need this feature please open an issue on the github page"),
590        _ => {
591            let mut textures = vec![];
592            for albedo_texture in albedo_textures {
593                let new_texture = crate::primitives::new_non_albedo_texture(
594                    program,
595                    texture.try_clone(),
596                    &albedo_texture,
597                );
598
599                textures.push(new_texture);
600            }
601            textures
602        }
603    }
604}
605
606fn load_tex_from_images_normal(
607    albedo_textures: &Vec<glium::Texture2d>,
608    height_textures: &Vec<glium::Texture2d>,
609    normal: Normal,
610    program: &LumenpyxProgram,
611) -> Vec<glium::Texture2d> {
612    match normal {
613        Normal::Path(path) => load_tex_from_images_path(&path, albedo_textures.len(), program),
614        _ => {
615            let mut textures = vec![];
616            for (albedo_texture, height_texture) in zip(albedo_textures, height_textures) {
617                let new_texture = crate::primitives::new_normal_texture(
618                    program,
619                    normal.try_clone(),
620                    height_texture,
621                    &albedo_texture,
622                );
623
624                textures.push(new_texture);
625            }
626            textures
627        }
628    }
629}
630
631pub struct AnimationStateMachine {
632    transform: Transform, // not the most efficient way to do this, but it works
633    animations: Vec<Animation>,
634    current_animation: usize,
635}
636
637impl AnimationStateMachine {
638    pub fn new(animations: Vec<Animation>) -> Self {
639        Self {
640            transform: Transform::default(),
641            animations,
642            current_animation: 0,
643        }
644    }
645
646    pub fn set_current_animation(&mut self, index: usize) {
647        self.current_animation = index;
648    }
649
650    /// sets the time of all animations in the state machine
651    pub fn set_time(&mut self, time: AnimationTimeElapsed) {
652        for animation in &mut self.animations {
653            animation.set_time(time.clone());
654        }
655    }
656
657    pub fn restart_current_animation(&mut self) {
658        self.animations[self.current_animation].restart_animation();
659    }
660}
661
662impl Drawable for AnimationStateMachine {
663    fn draw_albedo(
664        &self,
665        program: &LumenpyxProgram,
666        transform: &Transform,
667        albedo_framebuffer: &mut glium::framebuffer::SimpleFrameBuffer,
668    ) {
669        self.animations[self.current_animation].draw_albedo(program, transform, albedo_framebuffer);
670    }
671
672    fn draw_height(
673        &self,
674        program: &LumenpyxProgram,
675        transform: &Transform,
676        height_framebuffer: &mut glium::framebuffer::SimpleFrameBuffer,
677    ) {
678        self.animations[self.current_animation].draw_height(program, transform, height_framebuffer);
679    }
680
681    fn draw_roughness(
682        &self,
683        program: &LumenpyxProgram,
684        transform: &Transform,
685        roughness_framebuffer: &mut glium::framebuffer::SimpleFrameBuffer,
686    ) {
687        self.animations[self.current_animation].draw_roughness(
688            program,
689            transform,
690            roughness_framebuffer,
691        );
692    }
693
694    fn draw_normal(
695        &self,
696        program: &LumenpyxProgram,
697        transform: &Transform,
698        normal_framebuffer: &mut glium::framebuffer::SimpleFrameBuffer,
699    ) {
700        self.animations[self.current_animation].draw_normal(program, transform, normal_framebuffer);
701    }
702
703    fn try_load_shaders(&self, program: &mut LumenpyxProgram) {
704        for animation in &self.animations {
705            animation.try_load_shaders(program);
706        }
707    }
708
709    fn get_transform(&self) -> Transform {
710        self.transform
711    }
712
713    fn get_recieve_shadows_strength(&self) -> f32 {
714        self.animations[self.current_animation].get_recieve_shadows_strength()
715    }
716
717    fn set_transform(&mut self, transform: Transform) {
718        self.transform = transform;
719    }
720}