summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs2
-rw-r--r--src/main.rs46
-rw-r--r--src/renderer/mod.rs248
-rw-r--r--src/shader/instance.wgsl (renamed from src/shader/shader.wgsl)0
-rw-r--r--src/shader/particle.wgsl46
-rw-r--r--src/sim/cell_sim/chunk.rs5
-rw-r--r--src/sim/mod.rs6
-rw-r--r--src/sim/particle_sim/mod.rs101
-rw-r--r--src/sim/particle_sim/particle.rs21
9 files changed, 419 insertions, 56 deletions
diff --git a/src/config.rs b/src/config.rs
index eeb4f7e..7506c44 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -3,7 +3,7 @@ pub const WINDOW_TITLE: &str = "pxs";
pub const CHUNK_SIZE: i32 = 128;
pub const CELLS_IN_CHUNK: usize = (CHUNK_SIZE * CHUNK_SIZE) as usize;
-pub const CAMERA_MOVEMENT_SPEED: f32 = 40.0;
+pub const CAMERA_MOVEMENT_SPEED: f32 = 2400.0;
pub const SIM_FPS: u32 = 120;
// pub const SIM_DELTA_TIME: f32 = 1.0 / SIM_FPS as f32;
diff --git a/src/main.rs b/src/main.rs
index e7f2084..08bc61c 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -4,6 +4,7 @@ mod renderer;
mod sim;
use futures::executor;
+use glam::Vec2;
use rand::random_range;
use std::{collections::VecDeque, sync::Arc, time::Instant};
use winit::{
@@ -24,6 +25,7 @@ use crate::{
sim::{
cell::{cell::Cell, materials::MaterialId},
cell_sim::{sim::sim_tick, world::World},
+ particle_sim::{ParticleManager, particle::Particle},
rb_sim::{DebugRenderMode, RbSimManager, debug_ops::DebugOperator},
write_rb_entity_to_world,
},
@@ -51,6 +53,7 @@ struct Input {
is_rmb_pressed: bool,
trigger_test_1: bool,
trigger_test_2: bool,
+ trigger_test_3: bool,
// keybindings
is_up_pressed: bool,
@@ -77,6 +80,7 @@ struct App {
// physics
rb_sim_manager: Option<RbSimManager>,
+ particle_manager: Option<ParticleManager>,
// sim state
// the last/current (not yet completed) seqno
@@ -122,6 +126,24 @@ impl App {
rbsm.test_spawn_ball(lm.0, lm.1, self.config.dropper_material);
}
+ if self.input.trigger_test_3
+ && let Some(lm) = self.input.last_mouse_world_pos
+ && let Some(pm) = &mut self.particle_manager
+ {
+ self.input.trigger_test_3 = false;
+ for i in 0..100 {
+ pm.particles.push(Particle {
+ position: Vec2::new(
+ lm.0 + random_range(-25.0..25.0),
+ lm.1 + random_range(-25.0..25.0),
+ ),
+ velocity: Vec2::ZERO,
+ material: MaterialId::Sand,
+ life: 2.0,
+ })
+ }
+ }
+
// --TEST DRAWING--
if (self.input.is_lmb_pressed || self.input.is_rmb_pressed)
&& let Some(lm) = self.input.last_mouse_world_pos
@@ -227,6 +249,12 @@ impl App {
}
physics_manager.rb_tick(physics_delta_time);
}
+ // move the particles
+ if let Some(particle_manager) = &mut self.particle_manager
+ && let Some(world) = &mut self.world
+ {
+ particle_manager.particle_tick(world, physics_delta_time);
+ }
}
}
@@ -244,6 +272,7 @@ impl Default for App {
trigger_test_1: false,
trigger_test_2: false,
+ trigger_test_3: false,
is_lmb_pressed: false,
is_rmb_pressed: false,
is_up_pressed: false,
@@ -257,6 +286,7 @@ impl Default for App {
world: Some(World::from_default_size()),
rb_sim_manager: Some(RbSimManager::new()),
+ particle_manager: Some(ParticleManager::new()),
sim_seqno: 0,
sim_paused: false,
@@ -341,6 +371,11 @@ impl ApplicationHandler for App {
KeyCode::KeyS => self.input.is_down_pressed = pressed,
KeyCode::KeyD => self.input.is_right_pressed = pressed,
KeyCode::KeyC => self.world = Some(World::from_default_size()),
+ KeyCode::KeyP => {
+ self.particle_manager
+ .as_mut()
+ .map(|pm| pm.particles = Vec::new());
+ }
KeyCode::KeyV => {
if let Some(rbsm) = self.rb_sim_manager.as_mut() {
let entity_ids: Vec<u32> = rbsm.rb_entities.keys().copied().collect();
@@ -370,6 +405,7 @@ impl ApplicationHandler for App {
KeyCode::KeyX if pressed => self.ignore_pause_next_tick = true,
KeyCode::Digit1 if pressed && !repeat => self.input.trigger_test_1 = true,
KeyCode::Digit2 if pressed && !repeat => self.input.trigger_test_2 = true,
+ KeyCode::Digit3 if pressed && !repeat => self.input.trigger_test_3 = true,
_ => {}
}
}
@@ -412,14 +448,10 @@ impl ApplicationHandler for App {
puffin::profile_scope!("redraw_requested");
// compute FPS diagnostics
let now = Instant::now();
- let secs_since_last_frame = (now - self.last_render).as_secs_f32();
+ let delta_time = (now - self.last_render).as_secs_f32();
self.last_render = now;
- let delta_time = secs_since_last_frame / (1.0 / 60.0);
-
- self.diagnostics
- .frame_times
- .push_back(secs_since_last_frame);
+ self.diagnostics.frame_times.push_back(delta_time);
if self.diagnostics.frame_times.len() > 30 {
self.diagnostics.frame_times.pop_front();
@@ -475,11 +507,13 @@ impl ApplicationHandler for App {
if let Some(renderer_state) = &mut self.renderer_state
&& let Some(world) = &mut self.world
&& let Some(rb_sim_manager) = &mut self.rb_sim_manager
+ && let Some(particle_manager) = &mut self.particle_manager
&& let Some(camera) = &mut self.camera
{
renderer_state.render(
world,
rb_sim_manager,
+ particle_manager,
camera,
&mut self.config,
&self.diagnostics,
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs
index ff503c8..2aabd48 100644
--- a/src/renderer/mod.rs
+++ b/src/renderer/mod.rs
@@ -13,6 +13,7 @@ use crate::{
sim::{
cell::materials::MaterialId,
cell_sim::world::World,
+ particle_sim::{ParticleManager, particle::Particle},
rb_sim::{RbSimManager, debug_render::DebugVertex, rb_entity::RbEntity},
},
};
@@ -21,12 +22,13 @@ use crate::{
const CHUNK_SLOTS: usize = 11 * 200;
const RB_ENTITY_SLOTS: usize = 64;
const CELL_SLOTS: usize = CHUNK_SLOTS + RB_ENTITY_SLOTS;
+const PARTICLE_SLOTS: usize = 10_000;
const INITIAL_DEBUG_VERTEX_CAPACITY: usize = 4096;
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
-struct Instance {
+struct RendererInstance {
centre: [f32; 2],
cos_sin: [f32; 2],
half_size: [f32; 2],
@@ -35,6 +37,14 @@ struct Instance {
_padding: u32,
}
+#[repr(C)]
+#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+struct RendererParticle {
+ position: [f32; 2],
+ data: u32,
+ _padding: u32,
+}
+
fn srgb_to_linear(channel: u8) -> f32 {
let channel = channel as f32 / 255.0;
if channel <= 0.04045 {
@@ -70,12 +80,19 @@ pub struct RendererState {
pub egui_state: egui_winit::State,
egui_renderer: egui_wgpu::Renderer,
- // world pixels
- pixels_pipeline: wgpu::RenderPipeline,
+ // common
camera_uniform_buffer: wgpu::Buffer,
+
+ // world pixels
+ instance_pipeline: wgpu::RenderPipeline,
instance_buffer: wgpu::Buffer,
cell_buffer: wgpu::Buffer,
- pixels_bind_group: wgpu::BindGroup,
+ instance_bind_group: wgpu::BindGroup,
+
+ // particles
+ particle_pipeline: wgpu::RenderPipeline,
+ particle_buffer: wgpu::Buffer,
+ particle_bind_group: wgpu::BindGroup,
// physics debug lines
debug_pipeline: wgpu::RenderPipeline,
@@ -157,7 +174,26 @@ impl RendererState {
}
});
- let pixels_bind_group_layout =
+ // --- PALETTE ---
+ let mut palette = [0.0f32; MaterialId::ALL.len() * 4];
+ for material in MaterialId::ALL {
+ let def = material.def();
+ let i = material as usize * 4;
+ palette[i] = srgb_to_linear(def.color.0);
+ palette[i + 1] = srgb_to_linear(def.color.1);
+ palette[i + 2] = srgb_to_linear(def.color.2);
+ palette[i + 3] = def.color.3 as f32 / 255.0;
+ }
+
+ let palette_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Palette buffer"),
+ size: size_of_val(&palette) as u64,
+ usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ // --- INSTANCES ---
+ let instance_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: None,
entries: &[
@@ -208,29 +244,29 @@ impl RendererState {
],
});
- let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
- label: Some("Shader"),
- source: wgpu::ShaderSource::Wgsl(include_str!("../shader/shader.wgsl").into()),
+ let instance_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Instance shader"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("../shader/instance.wgsl").into()),
});
- let pixels_pipeline_layout =
+ let instance_pipeline_layout =
device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
immediate_size: 0,
- bind_group_layouts: &[Some(&pixels_bind_group_layout)],
+ bind_group_layouts: &[Some(&instance_bind_group_layout)],
});
- let pixels_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ let instance_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
- layout: Some(&pixels_pipeline_layout),
+ layout: Some(&instance_pipeline_layout),
vertex: wgpu::VertexState {
- module: &shader,
+ module: &instance_shader,
entry_point: Some("vs_main"),
buffers: &[],
compilation_options: wgpu::PipelineCompilationOptions::default(),
},
fragment: Some(wgpu::FragmentState {
- module: &shader,
+ module: &instance_shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
@@ -261,28 +297,11 @@ impl RendererState {
mapped_at_creation: false,
});
- let mut palette = [0.0f32; MaterialId::ALL.len() * 4];
- for material in MaterialId::ALL {
- let def = material.def();
- let i = material as usize * 4;
- palette[i] = srgb_to_linear(def.color.0);
- palette[i + 1] = srgb_to_linear(def.color.1);
- palette[i + 2] = srgb_to_linear(def.color.2);
- palette[i + 3] = def.color.3 as f32 / 255.0;
- }
-
- let palette_buffer = device.create_buffer(&wgpu::BufferDescriptor {
- label: Some("Palette buffer"),
- size: size_of_val(&palette) as u64,
- usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
- mapped_at_creation: false,
- });
-
queue.write_buffer(&palette_buffer, 0, bytemuck::cast_slice(&palette));
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Instance buffer"),
- size: (CELL_SLOTS * size_of::<Instance>()) as u64,
+ size: (CELL_SLOTS * size_of::<RendererInstance>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
@@ -294,9 +313,9 @@ impl RendererState {
mapped_at_creation: false,
});
- let pixels_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ let instance_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
- layout: &pixels_bind_group_layout,
+ layout: &instance_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
@@ -317,6 +336,120 @@ impl RendererState {
],
});
+ // --- PARTICLES ---
+ let particle_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: None,
+ entries: &[
+ // camera uniform
+ wgpu::BindGroupLayoutEntry {
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ binding: 0,
+ count: None,
+ visibility: wgpu::ShaderStages::VERTEX,
+ },
+ // palette
+ wgpu::BindGroupLayoutEntry {
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Storage { read_only: true },
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ binding: 1,
+ count: None,
+ visibility: wgpu::ShaderStages::FRAGMENT,
+ },
+ // instance buffer
+ wgpu::BindGroupLayoutEntry {
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Storage { read_only: true },
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ binding: 2,
+ count: None,
+ visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
+ },
+ ],
+ });
+
+ let particle_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Particle shader"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("../shader/particle.wgsl").into()),
+ });
+
+ let particle_pipeline_layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: None,
+ immediate_size: 0,
+ bind_group_layouts: &[Some(&particle_bind_group_layout)],
+ });
+
+ let particle_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: None,
+ layout: Some(&particle_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &particle_shader,
+ entry_point: Some("vs_main"),
+ buffers: &[],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &particle_shader,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: config.format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::TriangleStrip,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: None,
+ polygon_mode: wgpu::PolygonMode::Fill,
+ unclipped_depth: false,
+ conservative: false,
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview_mask: None,
+ cache: None,
+ });
+
+ let particle_buffer = device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Particle buffer"),
+ size: (PARTICLE_SLOTS * size_of::<RendererParticle>()) as u64,
+ usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ });
+
+ let particle_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: None,
+ layout: &particle_bind_group_layout,
+ entries: &[
+ wgpu::BindGroupEntry {
+ binding: 0,
+ resource: camera_uniform_buffer.as_entire_binding(),
+ },
+ wgpu::BindGroupEntry {
+ binding: 1,
+ resource: palette_buffer.as_entire_binding(),
+ },
+ wgpu::BindGroupEntry {
+ binding: 2,
+ resource: particle_buffer.as_entire_binding(),
+ },
+ ],
+ });
+
+ // --- DEBUG ---
let debug_bind_group_layout =
device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("Debug bind group layout"),
@@ -409,11 +542,16 @@ impl RendererState {
egui_state,
egui_renderer,
- pixels_pipeline,
camera_uniform_buffer,
+
+ instance_pipeline,
instance_buffer,
cell_buffer,
- pixels_bind_group,
+ instance_bind_group,
+
+ particle_pipeline,
+ particle_buffer,
+ particle_bind_group,
debug_pipeline,
debug_bind_group,
@@ -437,6 +575,7 @@ impl RendererState {
&mut self,
world: &mut World,
rb_sim_manager: &mut RbSimManager,
+ particle_manager: &mut ParticleManager,
camera: &mut Camera,
config: &mut Config,
diagnostics: &Diagnostics,
@@ -528,7 +667,7 @@ impl RendererState {
);
let mut cell_buffer: [u8; CELLS_IN_CHUNK] = [0; CELLS_IN_CHUNK];
- let mut instances: Vec<Instance> = Vec::new();
+ let mut instances: Vec<RendererInstance> = Vec::new();
{
puffin::profile_scope!("Upload chunk cells");
@@ -587,7 +726,7 @@ impl RendererState {
for cx in cxl..=cxu {
for cy in cyl..=cyu {
if let Some(idx) = world.chunk_position_to_chunk_idx.get(&(cx, cy)) {
- instances.push(Instance {
+ instances.push(RendererInstance {
centre: [
(cx * CHUNK_SIZE) as f32 + half_size[0],
(cy * CHUNK_SIZE) as f32 + half_size[1],
@@ -606,7 +745,7 @@ impl RendererState {
if let Some(&RbEntity { width, height, .. }) = rb_sim_manager.rb_entities.get(id)
&& let Some((x, y, cos, sin)) = rb_sim_manager.get_rb_entity_transform(*id)
{
- instances.push(Instance {
+ instances.push(RendererInstance {
centre: [x, y],
cos_sin: [cos, sin],
half_size: [width as f32 / 2.0, height as f32 / 2.0],
@@ -621,6 +760,25 @@ impl RendererState {
.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
}
+ let mut particles: Vec<RendererParticle> = Vec::new();
+
+ {
+ puffin::profile_scope!("Build particles");
+ // TODO could cull this to visible, maybe it's slower?
+ particles = particle_manager
+ .particles
+ .iter()
+ .map(|p| RendererParticle {
+ position: [p.position.x, p.position.y],
+ data: p.material as u32,
+ _padding: 0,
+ })
+ .collect();
+
+ self.queue
+ .write_buffer(&self.particle_buffer, 0, bytemuck::cast_slice(&particles));
+ }
+
let debug_vertex_count = if config.debug_render {
puffin::profile_scope!("Build physics debug lines");
let vertices = rb_sim_manager.debug_render(config.debug_render_mode);
@@ -671,6 +829,7 @@ impl RendererState {
multiview_mask: None,
});
+ // this is kind of a hack
let instances_start = u32::min(
if config.cells_render {
0
@@ -680,10 +839,17 @@ impl RendererState {
instances.len() as u32 - 1,
);
- render_pass.set_pipeline(&self.pixels_pipeline);
- render_pass.set_bind_group(0, &self.pixels_bind_group, &[]);
+ // instances
+ render_pass.set_pipeline(&self.instance_pipeline);
+ render_pass.set_bind_group(0, &self.instance_bind_group, &[]);
render_pass.draw(0..4, instances_start..instances.len() as u32);
+ // particles
+ render_pass.set_pipeline(&self.particle_pipeline);
+ render_pass.set_bind_group(0, &self.particle_bind_group, &[]);
+ render_pass.draw(0..4, 0..particles.len() as u32);
+
+ // debug
if debug_vertex_count > 0 {
render_pass.set_pipeline(&self.debug_pipeline);
render_pass.set_bind_group(0, &self.debug_bind_group, &[]);
diff --git a/src/shader/shader.wgsl b/src/shader/instance.wgsl
index fb1e0b1..fb1e0b1 100644
--- a/src/shader/shader.wgsl
+++ b/src/shader/instance.wgsl
diff --git a/src/shader/particle.wgsl b/src/shader/particle.wgsl
new file mode 100644
index 0000000..704cfbe
--- /dev/null
+++ b/src/shader/particle.wgsl
@@ -0,0 +1,46 @@
+struct Camera {
+ scale: vec2f,
+ centre: vec2f,
+};
+
+struct Particle {
+ position: vec2f,
+ data: u32,
+};
+
+@group(0) @binding(0) var<uniform> camera: Camera;
+@group(0) @binding(1) var<storage, read> palette: array<vec4f>;
+@group(0) @binding(2) var<storage, read> particles: array<Particle>;
+
+struct VertexOutput {
+ @builtin(position) clip_position: vec4<f32>,
+ @location(0) world: vec2<f32>,
+ @location(1) @interpolate(flat) instance: u32,
+};
+
+@vertex
+fn vs_main(
+ @builtin(vertex_index) i: u32,
+ @builtin(instance_index) n: u32,
+) -> VertexOutput {
+ let particle = particles[n];
+
+ // (0,0),(1,0),(0,1),(1,1)
+ let corner = vec2<f32>(f32(i & 1u), f32((i >> 1u) & 1u));
+ let world = particle.position + corner;
+
+ var out: VertexOutput;
+ out.clip_position = vec4f((world - camera.centre) * camera.scale, 0.0, 1.0);
+ out.world = world;
+ out.instance = n;
+
+ return out;
+}
+
+@fragment
+fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+ let particle = particles[in.instance];
+ // TODO can pack more in here and shift
+ let c = palette[particle.data];
+ return c;
+}
diff --git a/src/sim/cell_sim/chunk.rs b/src/sim/cell_sim/chunk.rs
index 85d2c09..3fe5056 100644
--- a/src/sim/cell_sim/chunk.rs
+++ b/src/sim/cell_sim/chunk.rs
@@ -1,10 +1,7 @@
use crate::{
config::{CELLS_IN_CHUNK, CHUNK_SIZE},
sim::{
- cell::{
- cell::Cell,
- materials::{MaterialForm, MaterialId},
- },
+ cell::{cell::Cell, materials::MaterialForm},
lib::marching_squares::Marchable,
},
};
diff --git a/src/sim/mod.rs b/src/sim/mod.rs
index c73a373..d9e358b 100644
--- a/src/sim/mod.rs
+++ b/src/sim/mod.rs
@@ -1,11 +1,9 @@
-use crate::{
- config::CHUNK_SIZE,
- sim::{cell::materials::MaterialId, cell_sim::world::World, rb_sim::RbSimManager},
-};
+use crate::sim::{cell::materials::MaterialId, cell_sim::world::World, rb_sim::RbSimManager};
pub mod cell;
pub mod cell_sim;
pub mod lib;
+pub mod particle_sim;
pub mod rb_sim;
pub fn write_rb_entity_to_world(
diff --git a/src/sim/particle_sim/mod.rs b/src/sim/particle_sim/mod.rs
new file mode 100644
index 0000000..8bad61d
--- /dev/null
+++ b/src/sim/particle_sim/mod.rs
@@ -0,0 +1,101 @@
+use glam::{IVec2, Vec2};
+
+use crate::{
+ config::PIXELS_TO_METRES,
+ sim::{
+ cell::{cell::Cell, materials::MaterialId},
+ cell_sim::world::World,
+ particle_sim::particle::Particle,
+ },
+};
+
+pub mod particle;
+
+pub struct ParticleManager {
+ pub particles: Vec<Particle>,
+}
+
+const PARTICLE_GRAVITY: f32 = 9.81 * PIXELS_TO_METRES;
+
+impl ParticleManager {
+ pub fn particle_tick(&mut self, world: &mut World, delta_time: f32) {
+ let mut i = 0;
+ 'outer: while i < self.particles.len() {
+ let p = &mut self.particles[i];
+
+ p.life -= delta_time;
+ if p.life <= 0.0 {
+ self.particles.swap_remove(i);
+ continue 'outer;
+ }
+
+ p.velocity.y += PARTICLE_GRAVITY * delta_time;
+ let dt_velocity = p.velocity * delta_time;
+
+ // Amanatides and Woo's fast Voxel Traversal
+ {
+ let mut cur = IVec2::new(p.position.x.round() as i32, p.position.y.round() as i32);
+ // direction we step for each component on each iteration
+ let step_sign =
+ IVec2::new(dt_velocity.x.signum() as i32, dt_velocity.y.signum() as i32);
+ let delta = Vec2::new(1.0 / dt_velocity.x.abs(), 1.0 / dt_velocity.y.abs());
+
+ let frac = Vec2::new(
+ if step_sign.x > 0 {
+ (cur.x as f32 + 0.5) - p.position.x
+ } else {
+ p.position.x - (cur.x as f32 - 0.5)
+ },
+ if step_sign.y > 0 {
+ (cur.y as f32 + 0.5) - p.position.y
+ } else {
+ p.position.y - (cur.y as f32 - 0.5)
+ },
+ );
+ let mut t_max = frac * delta;
+
+ let mut prev = cur;
+ if let Some(cell) = world.get_cell_from_game_position(prev.x, prev.y)
+ && cell.material != MaterialId::Void
+ {
+ // already occupied, die
+ self.particles.swap_remove(i);
+ continue 'outer;
+ }
+ while t_max.min_element() <= 1.0 {
+ if t_max.x < t_max.y {
+ cur.x += step_sign.x;
+ t_max.x += delta.x;
+ } else {
+ cur.y += step_sign.y;
+ t_max.y += delta.y;
+ }
+ if let Some(cell) = world.get_cell_from_game_position(cur.x, cur.y)
+ && cell.material != MaterialId::Void
+ {
+ // write ourselves to the board
+ world.set_cell_from_game_position(
+ prev.x,
+ prev.y,
+ Cell::from_material(p.material),
+ false,
+ );
+ self.particles.swap_remove(i);
+ continue 'outer;
+ }
+ prev = cur;
+ }
+ }
+
+ // no collision, just move
+ p.position = p.position + dt_velocity;
+ i += 1;
+ }
+ }
+
+ pub fn new() -> Self {
+ ParticleManager {
+ particles: Vec::new(),
+ }
+ }
+}
diff --git a/src/sim/particle_sim/particle.rs b/src/sim/particle_sim/particle.rs
new file mode 100644
index 0000000..cc7dd28
--- /dev/null
+++ b/src/sim/particle_sim/particle.rs
@@ -0,0 +1,21 @@
+use glam::Vec2;
+
+use crate::sim::cell::materials::MaterialId;
+
+pub struct Particle {
+ pub position: Vec2,
+ pub velocity: Vec2,
+ pub material: MaterialId,
+ pub life: f32,
+}
+
+impl Particle {
+ pub fn new(position: Vec2, velocity: Vec2, material: MaterialId, life: f32) -> Self {
+ Particle {
+ position,
+ velocity,
+ material,
+ life,
+ }
+ }
+}