Added animated particles

master
Mark 2024-01-03 07:46:27 -08:00
parent a892e4e763
commit f05f2fbc45
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
11 changed files with 233 additions and 77 deletions

View File

@ -1,6 +1,7 @@
## Specific Jobs ## Specific Jobs
- Finish particles - Define projectile colliders & particles
- Projectile colliders & particles - Particle variation
- Animated sprites
- UI: health, shield, fuel, heat, energy bars - UI: health, shield, fuel, heat, energy bars
- UI: text arranger - UI: text arranger
- Sound system - Sound system

View File

@ -10,7 +10,7 @@ rate_rng = 0.1
projectile.sprite_texture = "projectile::blaster" projectile.sprite_texture = "projectile::blaster"
# Height of projectile in game units # Height of projectile in game units
projectile.size = 10 projectile.size = 6
projectile.size_rng = 0.0 projectile.size_rng = 0.0
# Speed of projectile, in game units/second # Speed of projectile, in game units/second
projectile.speed = 300 projectile.speed = 300

View File

@ -1,38 +1,45 @@
[texture."starfield"] [texture."starfield"]
path = "starfield.png" file = "starfield.png"
[texture."star::star"] [texture."star::star"]
path = "star/B-09.png" file = "star/B-09.png"
[texture."flare::ion"] [texture."flare::ion"]
path = "flare/1.png" file = "flare/1.png"
[texture."planet::earth"] [texture."planet::earth"]
path = "planet/earth.png" file = "planet/earth.png"
[texture."planet::luna"] [texture."planet::luna"]
path = "planet/luna.png" file = "planet/luna.png"
[texture."projectile::blaster"] [texture."projectile::blaster"]
path = "projectile/blaster.png" file = "projectile/blaster.png"
[texture."ship::gypsum"] [texture."ship::gypsum"]
path = "ship/gypsum.png" file = "ship/gypsum.png"
[texture."ui::radar"] [texture."ui::radar"]
path = "ui/radar.png" file = "ui/radar.png"
[texture."ui::shipblip"] [texture."ui::shipblip"]
path = "ui/ship-blip.png" file = "ui/ship-blip.png"
[texture."ui::planetblip"] [texture."ui::planetblip"]
path = "ui/planet-blip.png" file = "ui/planet-blip.png"
[texture."ui::radarframe"] [texture."ui::radarframe"]
path = "ui/radarframe.png" file = "ui/radarframe.png"
[texture."ui::centerarrow"] [texture."ui::centerarrow"]
path = "ui/center-arrow.png" file = "ui/center-arrow.png"
[texture."particle::blaster"] [texture."particle::blaster"]
path = "particle/blaster-01.png" duration = 0.15
repeat = "once"
frames = [
"particle/blaster-01.png",
"particle/blaster-02.png",
"particle/blaster-03.png",
"particle/blaster-04.png",
]

View File

@ -19,8 +19,8 @@ use walkdir::WalkDir;
pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle}; pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle};
pub use part::{ pub use part::{
EnginePoint, Faction, Gun, GunPoint, Outfit, OutfitSpace, Projectile, Relationship, Ship, EnginePoint, Faction, Gun, GunPoint, Outfit, OutfitSpace, Projectile, Relationship, RepeatMode,
System, Texture, Ship, System, Texture,
}; };
mod syntax { mod syntax {

View File

@ -14,4 +14,4 @@ pub use outfit::Outfit;
pub use shared::OutfitSpace; pub use shared::OutfitSpace;
pub use ship::{EnginePoint, GunPoint, Ship}; pub use ship::{EnginePoint, GunPoint, Ship};
pub use system::{Object, System}; pub use system::{Object, System};
pub use texture::Texture; pub use texture::{RepeatMode, Texture};

View File

@ -1,20 +1,60 @@
use std::{collections::HashMap, path::PathBuf};
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use image::io::Reader; use image::io::Reader;
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf};
use crate::{handle::TextureHandle, Content}; use crate::{handle::TextureHandle, Content};
pub(crate) mod syntax { pub(crate) mod syntax {
use serde::Deserialize;
use std::path::PathBuf; use std::path::PathBuf;
use serde::Deserialize; use super::RepeatMode;
// Raw serde syntax structs. // Raw serde syntax structs.
// These are never seen by code outside this crate. // These are never seen by code outside this crate.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Texture { #[serde(untagged)]
pub path: PathBuf, pub enum Texture {
Static(StaticTexture),
Frames(FramesTexture),
}
#[derive(Debug, Deserialize)]
pub struct StaticTexture {
pub file: PathBuf,
}
// TODO: loop mode
#[derive(Debug, Deserialize)]
pub struct FramesTexture {
pub frames: Vec<PathBuf>,
pub duration: f32,
pub repeat: RepeatMode,
}
}
/// How to replay a texture's animation
#[derive(Debug, Deserialize, Clone, Copy)]
pub enum RepeatMode {
/// Play this animation once, and stop at the last frame
#[serde(rename = "once")]
Once,
/// After the first frame, jump to the last frame
#[serde(rename = "repeat")]
Repeat,
}
impl RepeatMode {
/// Represent this repeatmode as an integer
/// Used to pass this enum into shaders
pub fn as_int(&self) -> u32 {
match self {
Self::Once => 0,
Self::Repeat => 1,
}
} }
} }
@ -27,8 +67,16 @@ pub struct Texture {
/// The handle for this texture /// The handle for this texture
pub handle: TextureHandle, pub handle: TextureHandle,
/// The path to this texture's image file /// The frames of this texture
pub path: PathBuf, /// (static textures have one frame)
pub frames: Vec<PathBuf>,
/// The speed of this texture's animation
/// (static textures have zero fps)
pub fps: f32,
/// How to replay this texture's animation
pub repeat: RepeatMode,
} }
impl crate::Build for Texture { impl crate::Build for Texture {
@ -36,19 +84,21 @@ impl crate::Build for Texture {
fn build(texture: Self::InputSyntax, ct: &mut Content) -> Result<()> { fn build(texture: Self::InputSyntax, ct: &mut Content) -> Result<()> {
for (texture_name, t) in texture { for (texture_name, t) in texture {
let path = ct.texture_root.join(t.path); match t {
let reader = Reader::open(&path).with_context(|| { syntax::Texture::Static(t) => {
let file = ct.texture_root.join(t.file);
let reader = Reader::open(&file).with_context(|| {
format!( format!(
"Failed to read texture `{}` from file `{}`", "Failed to read texture `{}` from file `{}`",
texture_name, texture_name,
path.display() file.display()
) )
})?; })?;
let dim = reader.into_dimensions().with_context(|| { let dim = reader.into_dimensions().with_context(|| {
format!( format!(
"Failed to dimensions of texture `{}` from file `{}`", "Failed to get dimensions of texture `{}` from file `{}`",
texture_name, texture_name,
path.display() file.display()
) )
})?; })?;
@ -67,12 +117,73 @@ impl crate::Build for Texture {
} }
ct.texture_index.insert(texture_name.clone(), h); ct.texture_index.insert(texture_name.clone(), h);
ct.textures.push(Self { ct.textures.push(Self {
name: texture_name, name: texture_name,
path, frames: vec![file],
fps: 0.0,
handle: h, handle: h,
repeat: RepeatMode::Once,
}); });
} }
syntax::Texture::Frames(t) => {
let mut dim = None;
for f in &t.frames {
let file = ct.texture_root.join(f);
let reader = Reader::open(&file).with_context(|| {
format!(
"Failed to read texture `{}` from file `{}`",
texture_name,
file.display()
)
})?;
let d = reader.into_dimensions().with_context(|| {
format!(
"Failed to get dimensions of texture `{}` from file `{}`",
texture_name,
file.display()
)
})?;
match dim {
None => dim = Some(d),
Some(e) => {
if d != e {
bail!(
"Failed to load frames of texture `{}`. Frames have different sizes `{}`",
texture_name,
file.display()
)
}
}
}
}
let h = TextureHandle {
index: ct.textures.len(),
aspect: dim.unwrap().0 as f32 / dim.unwrap().1 as f32,
};
if texture_name == ct.starfield_texture_name {
unreachable!("Starfield texture may not be animated")
}
let fps = t.duration / t.frames.len() as f32;
ct.texture_index.insert(texture_name.clone(), h);
ct.textures.push(Self {
name: texture_name,
frames: t
.frames
.into_iter()
.map(|f| ct.texture_root.join(f))
.collect(),
fps,
handle: h,
repeat: t.repeat,
});
}
}
}
if ct.starfield_handle.is_none() { if ct.starfield_handle.is_none() {
bail!( bail!(

View File

@ -6,7 +6,8 @@ struct InstanceInput {
@location(6) size: f32, @location(6) size: f32,
@location(7) created: f32, @location(7) created: f32,
@location(8) expires: f32, @location(8) expires: f32,
@location(9) texture_index: u32, @location(9) texture_index_len_rep: vec3<u32>,
@location(10) texture_aspect_fps: vec2<f32>,
}; };
struct VertexInput { struct VertexInput {
@ -42,7 +43,9 @@ var texture_array: binding_array<texture_2d<f32>>;
var sampler_array: binding_array<sampler>; var sampler_array: binding_array<sampler>;
fn fmod(x: f32, m: f32) -> f32 {
return x - floor(x / m) * m;
}
@vertex @vertex
fn vertex_main( fn vertex_main(
@ -52,21 +55,39 @@ fn vertex_main(
var out: VertexOutput; var out: VertexOutput;
out.texture_coords = vertex.texture_coords; out.texture_coords = vertex.texture_coords;
out.texture_index = instance.texture_index;
if instance.expires < global.current_time.x { if instance.expires < global.current_time.x {
out.texture_index = instance.texture_index_len_rep.x;
out.position = vec4<f32>(2.0, 2.0, 0.0, 1.0); out.position = vec4<f32>(2.0, 2.0, 0.0, 1.0);
return out; return out;
} }
let age = instance.created - global.current_time.x; let age = global.current_time.x - instance.created;
var frame: u32 = u32(0);
if instance.texture_index_len_rep.z == u32(1) {
// Repeat
frame = u32(fmod(
(age / instance.texture_aspect_fps.y),
f32(instance.texture_index_len_rep.y)
));
} else {
// Once
frame = u32(min(
(age / instance.texture_aspect_fps.y),
f32(instance.texture_index_len_rep.y) - 1.0
));
}
out.texture_index = instance.texture_index_len_rep.x + frame;
let rotation = mat2x2(instance.rotation_0, instance.rotation_1); let rotation = mat2x2(instance.rotation_0, instance.rotation_1);
var scale: f32 = instance.size / global.camera_zoom.x; var scale: f32 = instance.size / global.camera_zoom.x;
var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y); var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y);
pos = pos * vec2<f32>( pos = pos * vec2<f32>(
1.0 * scale / global.window_aspect.x, instance.texture_aspect_fps.x * scale / global.window_aspect.x,
scale scale
); );
pos = rotation * pos; pos = rotation * pos;

View File

@ -586,7 +586,8 @@ impl GPUState {
velocity: i.velocity.into(), velocity: i.velocity.into(),
rotation: Matrix2::from_angle(i.angle).into(), rotation: Matrix2::from_angle(i.angle).into(),
size: i.size, size: i.size,
texture_index: texture.index, texture_index_len_rep: [texture.index, texture.len, texture.repeat],
texture_aspect_fps: [texture.aspect, texture.fps],
created: framestate.current_time, created: framestate.current_time,
expires: framestate.current_time + i.lifetime, expires: framestate.current_time + i.lifetime,
}]), }]),

View File

@ -70,7 +70,10 @@ impl RawTexture {
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct Texture { pub struct Texture {
pub index: u32, // Index in texture array pub index: u32, // Index in texture array
pub len: u32, // Number of frames
pub fps: f32, // Frames per second
pub aspect: f32, // width / height pub aspect: f32, // width / height
pub repeat: u32, // How to re-play this texture
} }
pub struct TextureArray { pub struct TextureArray {
@ -98,18 +101,23 @@ impl TextureArray {
let mut textures = HashMap::new(); let mut textures = HashMap::new();
for t in &ct.textures { for t in &ct.textures {
let mut f = File::open(&t.path)?; let index = texture_data.len() as u32;
for f in &t.frames {
let mut f = File::open(&f)?;
let mut bytes = Vec::new(); let mut bytes = Vec::new();
f.read_to_end(&mut bytes)?; f.read_to_end(&mut bytes)?;
texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, &t.name)?);
}
textures.insert( textures.insert(
t.handle, t.handle,
Texture { Texture {
index: texture_data.len() as u32, index,
aspect: t.handle.aspect, aspect: t.handle.aspect,
fps: t.fps,
len: t.frames.len() as u32,
repeat: t.repeat.as_int(),
}, },
); );
texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, &t.name)?);
} }
let sampler = device.create_sampler(&wgpu::SamplerDescriptor { let sampler = device.create_sampler(&wgpu::SamplerDescriptor {

View File

@ -224,7 +224,8 @@ pub struct ParticleInstance {
pub expires: f32, pub expires: f32,
/// What texture to use for this particle /// What texture to use for this particle
pub texture_index: u32, pub texture_index_len_rep: [u32; 3],
pub texture_aspect_fps: [f32; 2],
} }
impl BufferObject for ParticleInstance { impl BufferObject for ParticleInstance {
@ -274,11 +275,17 @@ impl BufferObject for ParticleInstance {
shader_location: 8, shader_location: 8,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Texture // Texture index / len / repeat
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress,
shader_location: 9, shader_location: 9,
format: wgpu::VertexFormat::Uint32, format: wgpu::VertexFormat::Uint32x3,
},
// Texture aspect / fps
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 14]>() as wgpu::BufferAddress,
shader_location: 10,
format: wgpu::VertexFormat::Float32x2,
}, },
], ],
} }

View File

@ -224,7 +224,7 @@ impl<'a> World {
let pr = self.get_rigid_body(p.rigid_body); let pr = self.get_rigid_body(p.rigid_body);
let pos = util::rigidbody_position(pr); let pos = util::rigidbody_position(pr);
let angle: Deg<f32> = util::rigidbody_rotation(pr) let angle: Deg<f32> = util::rigidbody_rotation(pr)
.angle(Vector2 { x: 0.0, y: 1.0 }) .angle(Vector2 { x: 1.0, y: 0.0 })
.into(); .into();
// Match target ship velocity, so impact particles are "sticky". // Match target ship velocity, so impact particles are "sticky".
@ -241,10 +241,10 @@ impl<'a> World {
particles.push(ParticleBuilder { particles.push(ParticleBuilder {
texture: ct.get_texture_handle("particle::blaster"), texture: ct.get_texture_handle("particle::blaster"),
pos: Point2 { x: pos.x, y: pos.y }, pos: Point2 { x: pos.x, y: pos.y },
velocity: -velocity, velocity,
angle: -angle, angle: -angle,
lifetime: 0.1, lifetime: 0.15,
size: 10.0, size: 5.0,
}); });
self.remove_projectile(*a); self.remove_projectile(*a);
} }