Added animated particles
parent
a892e4e763
commit
f05f2fbc45
5
TODO.md
5
TODO.md
|
@ -1,6 +1,7 @@
|
|||
## Specific Jobs
|
||||
- Finish particles
|
||||
- Projectile colliders & particles
|
||||
- Define projectile colliders & particles
|
||||
- Particle variation
|
||||
- Animated sprites
|
||||
- UI: health, shield, fuel, heat, energy bars
|
||||
- UI: text arranger
|
||||
- Sound system
|
||||
|
|
|
@ -10,7 +10,7 @@ rate_rng = 0.1
|
|||
|
||||
projectile.sprite_texture = "projectile::blaster"
|
||||
# Height of projectile in game units
|
||||
projectile.size = 10
|
||||
projectile.size = 6
|
||||
projectile.size_rng = 0.0
|
||||
# Speed of projectile, in game units/second
|
||||
projectile.speed = 300
|
||||
|
|
|
@ -1,38 +1,45 @@
|
|||
[texture."starfield"]
|
||||
path = "starfield.png"
|
||||
file = "starfield.png"
|
||||
|
||||
[texture."star::star"]
|
||||
path = "star/B-09.png"
|
||||
file = "star/B-09.png"
|
||||
|
||||
[texture."flare::ion"]
|
||||
path = "flare/1.png"
|
||||
file = "flare/1.png"
|
||||
|
||||
[texture."planet::earth"]
|
||||
path = "planet/earth.png"
|
||||
file = "planet/earth.png"
|
||||
|
||||
[texture."planet::luna"]
|
||||
path = "planet/luna.png"
|
||||
file = "planet/luna.png"
|
||||
|
||||
[texture."projectile::blaster"]
|
||||
path = "projectile/blaster.png"
|
||||
file = "projectile/blaster.png"
|
||||
|
||||
[texture."ship::gypsum"]
|
||||
path = "ship/gypsum.png"
|
||||
file = "ship/gypsum.png"
|
||||
|
||||
[texture."ui::radar"]
|
||||
path = "ui/radar.png"
|
||||
file = "ui/radar.png"
|
||||
|
||||
[texture."ui::shipblip"]
|
||||
path = "ui/ship-blip.png"
|
||||
file = "ui/ship-blip.png"
|
||||
|
||||
[texture."ui::planetblip"]
|
||||
path = "ui/planet-blip.png"
|
||||
file = "ui/planet-blip.png"
|
||||
|
||||
[texture."ui::radarframe"]
|
||||
path = "ui/radarframe.png"
|
||||
file = "ui/radarframe.png"
|
||||
|
||||
[texture."ui::centerarrow"]
|
||||
path = "ui/center-arrow.png"
|
||||
file = "ui/center-arrow.png"
|
||||
|
||||
[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",
|
||||
]
|
||||
|
|
|
@ -19,8 +19,8 @@ use walkdir::WalkDir;
|
|||
|
||||
pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle};
|
||||
pub use part::{
|
||||
EnginePoint, Faction, Gun, GunPoint, Outfit, OutfitSpace, Projectile, Relationship, Ship,
|
||||
System, Texture,
|
||||
EnginePoint, Faction, Gun, GunPoint, Outfit, OutfitSpace, Projectile, Relationship, RepeatMode,
|
||||
Ship, System, Texture,
|
||||
};
|
||||
|
||||
mod syntax {
|
||||
|
|
|
@ -14,4 +14,4 @@ pub use outfit::Outfit;
|
|||
pub use shared::OutfitSpace;
|
||||
pub use ship::{EnginePoint, GunPoint, Ship};
|
||||
pub use system::{Object, System};
|
||||
pub use texture::Texture;
|
||||
pub use texture::{RepeatMode, Texture};
|
||||
|
|
|
@ -1,20 +1,60 @@
|
|||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use image::io::Reader;
|
||||
use serde::Deserialize;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
|
||||
use crate::{handle::TextureHandle, Content};
|
||||
|
||||
pub(crate) mod syntax {
|
||||
use serde::Deserialize;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde::Deserialize;
|
||||
use super::RepeatMode;
|
||||
|
||||
// Raw serde syntax structs.
|
||||
// These are never seen by code outside this crate.
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Texture {
|
||||
pub path: PathBuf,
|
||||
#[serde(untagged)]
|
||||
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
|
||||
pub handle: TextureHandle,
|
||||
|
||||
/// The path to this texture's image file
|
||||
pub path: PathBuf,
|
||||
/// The frames of this texture
|
||||
/// (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 {
|
||||
|
@ -36,19 +84,21 @@ impl crate::Build for Texture {
|
|||
|
||||
fn build(texture: Self::InputSyntax, ct: &mut Content) -> Result<()> {
|
||||
for (texture_name, t) in texture {
|
||||
let path = ct.texture_root.join(t.path);
|
||||
let reader = Reader::open(&path).with_context(|| {
|
||||
match t {
|
||||
syntax::Texture::Static(t) => {
|
||||
let file = ct.texture_root.join(t.file);
|
||||
let reader = Reader::open(&file).with_context(|| {
|
||||
format!(
|
||||
"Failed to read texture `{}` from file `{}`",
|
||||
texture_name,
|
||||
path.display()
|
||||
file.display()
|
||||
)
|
||||
})?;
|
||||
let dim = reader.into_dimensions().with_context(|| {
|
||||
format!(
|
||||
"Failed to dimensions of texture `{}` from file `{}`",
|
||||
"Failed to get dimensions of texture `{}` from file `{}`",
|
||||
texture_name,
|
||||
path.display()
|
||||
file.display()
|
||||
)
|
||||
})?;
|
||||
|
||||
|
@ -67,12 +117,73 @@ impl crate::Build for Texture {
|
|||
}
|
||||
|
||||
ct.texture_index.insert(texture_name.clone(), h);
|
||||
|
||||
ct.textures.push(Self {
|
||||
name: texture_name,
|
||||
path,
|
||||
frames: vec![file],
|
||||
fps: 0.0,
|
||||
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() {
|
||||
bail!(
|
||||
|
|
|
@ -6,7 +6,8 @@ struct InstanceInput {
|
|||
@location(6) size: f32,
|
||||
@location(7) created: 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 {
|
||||
|
@ -42,7 +43,9 @@ var texture_array: binding_array<texture_2d<f32>>;
|
|||
var sampler_array: binding_array<sampler>;
|
||||
|
||||
|
||||
|
||||
fn fmod(x: f32, m: f32) -> f32 {
|
||||
return x - floor(x / m) * m;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vertex_main(
|
||||
|
@ -52,21 +55,39 @@ fn vertex_main(
|
|||
|
||||
var out: VertexOutput;
|
||||
out.texture_coords = vertex.texture_coords;
|
||||
out.texture_index = instance.texture_index;
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
var scale: f32 = instance.size / global.camera_zoom.x;
|
||||
var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y);
|
||||
|
||||
pos = pos * vec2<f32>(
|
||||
1.0 * scale / global.window_aspect.x,
|
||||
instance.texture_aspect_fps.x * scale / global.window_aspect.x,
|
||||
scale
|
||||
);
|
||||
pos = rotation * pos;
|
||||
|
|
|
@ -586,7 +586,8 @@ impl GPUState {
|
|||
velocity: i.velocity.into(),
|
||||
rotation: Matrix2::from_angle(i.angle).into(),
|
||||
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,
|
||||
expires: framestate.current_time + i.lifetime,
|
||||
}]),
|
||||
|
|
|
@ -70,7 +70,10 @@ impl RawTexture {
|
|||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Texture {
|
||||
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 repeat: u32, // How to re-play this texture
|
||||
}
|
||||
|
||||
pub struct TextureArray {
|
||||
|
@ -98,18 +101,23 @@ impl TextureArray {
|
|||
let mut textures = HashMap::new();
|
||||
|
||||
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();
|
||||
f.read_to_end(&mut bytes)?;
|
||||
texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, &t.name)?);
|
||||
}
|
||||
textures.insert(
|
||||
t.handle,
|
||||
Texture {
|
||||
index: texture_data.len() as u32,
|
||||
index,
|
||||
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 {
|
||||
|
|
|
@ -224,7 +224,8 @@ pub struct ParticleInstance {
|
|||
pub expires: f32,
|
||||
|
||||
/// 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 {
|
||||
|
@ -274,11 +275,17 @@ impl BufferObject for ParticleInstance {
|
|||
shader_location: 8,
|
||||
format: wgpu::VertexFormat::Float32,
|
||||
},
|
||||
// Texture
|
||||
// Texture index / len / repeat
|
||||
wgpu::VertexAttribute {
|
||||
offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress,
|
||||
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,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
@ -224,7 +224,7 @@ impl<'a> World {
|
|||
let pr = self.get_rigid_body(p.rigid_body);
|
||||
let pos = util::rigidbody_position(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();
|
||||
|
||||
// Match target ship velocity, so impact particles are "sticky".
|
||||
|
@ -241,10 +241,10 @@ impl<'a> World {
|
|||
particles.push(ParticleBuilder {
|
||||
texture: ct.get_texture_handle("particle::blaster"),
|
||||
pos: Point2 { x: pos.x, y: pos.y },
|
||||
velocity: -velocity,
|
||||
velocity,
|
||||
angle: -angle,
|
||||
lifetime: 0.1,
|
||||
size: 10.0,
|
||||
lifetime: 0.15,
|
||||
size: 5.0,
|
||||
});
|
||||
self.remove_projectile(*a);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue