use bevy::prelude::*;
use bevy_shader_plugin::prelude::*;
const SHADER_PATH: &str = "shaders/shader.spv";
const FRAGMENT_ENTRY: &str = "draw";
const N: u32 = 500;
const SUBSTEPS: usize = 4;
const RADIUS: f32 = 8.0;
const GRAVITY: f32 = 1500.0;
const RESTITUTION: f32 = 0.5;
struct Particles;
struct Corrections;
#[derive(ShaderType, Clone, Copy, Default)]
struct Particle {
pos: Vec2,
vel: Vec2,
}
#[derive(ShaderType, Clone, Copy, Default)]
struct Correction {
dpos: Vec2,
dvel: Vec2,
}
#[derive(Resource, ShaderType, Clone, Default)]
struct SimParams {
resolution: Vec2,
gravity: f32,
dt: f32,
radius: f32,
restitution: f32,
seed: u32,
}
fn main() {
let mut compute =
ComputeShaderPlugin::new(SHADER_PATH).init_pass("init", Workgroups::over::<Particles>());
for _ in 0..SUBSTEPS {
compute = compute
.pass("collide", Workgroups::over::<Particles>())
.pass("integrate", Workgroups::over::<Particles>());
}
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resolution: (640, 360).into(),
..default()
}),
..default()
}))
.add_plugins((
compute,
FullscreenFragmentPlugin::new(SHADER_PATH).with_entry_point(FRAGMENT_ENTRY),
))
.register_uniform_buffer::<SimParams>(0, 0)
.insert_resource(SimParams {
gravity: GRAVITY,
radius: RADIUS,
restitution: RESTITUTION,
..default()
})
.register_gpu_buffer::<Particles, Particle>(1, 0, N)
.register_gpu_buffer::<Corrections, Correction>(1, 1, N)
.read_back_gpu_buffer::<Particles, Particle>(ReadbackMode::OnRequest)
.insert_resource(StatsTimer(Timer::from_seconds(1.0, TimerMode::Repeating)))
.add_systems(Startup, setup)
.add_systems(Update, (update_params, reset, request_stats, print_stats))
.run();
}
#[derive(Resource)]
struct StatsTimer(Timer);
fn setup(mut commands: Commands) {
commands.spawn((Camera3d::default(), Msaa::Off));
info!("Press R to respawn the particles");
}
fn update_params(
mut params: ResMut<SimParams>,
windows: Query<&Window, With<PrimaryWindow>>,
time: Res<Time>,
) {
let Ok(window) = windows.single() else { return };
params.resolution = Vec2::new(
window.physical_width() as f32,
window.physical_height() as f32,
);
params.dt = time.delta_secs().min(1.0 / 30.0) / SUBSTEPS as f32;
params.seed = time.elapsed().as_nanos() as u32;
}
fn reset(keys: Res<ButtonInput<KeyCode>>, mut passes: ResMut<ComputePasses>) {
if keys.just_pressed(KeyCode::KeyR) {
passes.rerun("init");
}
}
fn request_stats(
time: Res<Time>,
mut timer: ResMut<StatsTimer>,
readback: Res<GpuReadback<Particles, Particle>>,
) {
if timer.0.tick(time.delta()).just_finished() {
readback.request();
}
}
fn print_stats(readback: Res<GpuReadback<Particles, Particle>>) {
if !readback.is_changed() {
return;
}
let Some(particles) = readback.latest() else {
return;
};
let n = particles.len() as f32;
let avg_pos = particles.iter().map(|p| p.pos).sum::<Vec2>() / n;
let avg_vel = particles.iter().map(|p| p.vel).sum::<Vec2>() / n;
info!(
"average position ({:.1}, {:.1}) px, average velocity ({:.1}, {:.1}) px/s",
avg_pos.x, avg_pos.y, avg_vel.x, avg_vel.y
);
}