1use crate::components::*;
2use crate::math::*;
3use crate::resources::*;
4use crate::SPHConfig;
5use crate::*;
6
7use bevy::color::palettes::css::RED;
8use rand::Rng;
9use std::{collections::HashMap, sync::Mutex};
10
11pub fn setup(
12 mut commands: Commands,
13 mut meshes: ResMut<Assets<Mesh>>,
14 mut materials: ResMut<Assets<ColorMaterial>>,
15 config: Res<SPHConfig>,
16) {
17 commands.spawn((
19 Camera2d,
20 Camera {
21 hdr: true, ..default()
23 },
24 Transform {
25 scale: Vec3::splat(1.0 / config.render.render_scale), ..default()
27 },
28 ));
29
30 let mut rng = rand::thread_rng();
31
32 commands.spawn((
33 Mesh2d(meshes.add(Rectangle::new(
34 config.domain.box_size.x,
35 config.domain.box_size.y,
36 ))),
37 MeshMaterial2d(materials.add(ColorMaterial::from_color(Color::WHITE))),
38 Transform::from_xyz(0.0, 0.0, -1.0),
39 ));
40
41 let airfoil = SolidBoundary::new_airfoil(
42 config.airfoil.chord_length, config.airfoil.position, -config.airfoil.aoa.to_radians(), );
46
47 let num_points_per_surface = airfoil.points.len() / 2;
48
49 for i in 0..num_points_per_surface - 1 {
51 let start = airfoil.points[i * 2]; let end = airfoil.points[(i + 1) * 2];
53 let line_length = start.distance(end);
54 let angle = (end - start).y.atan2((end - start).x);
55
56 commands.spawn((
57 Mesh2d(meshes.add(Rectangle::new(line_length, 0.2))),
58 MeshMaterial2d(materials.add(ColorMaterial::from(Color::BLACK))),
59 Transform::from_translation(((start + end) / 2.0).extend(0.1))
60 .with_rotation(Quat::from_rotation_z(angle)),
61 ));
62 }
63
64 for i in 0..num_points_per_surface - 1 {
66 let start = airfoil.points[i * 2 + 1]; let end = airfoil.points[(i + 1) * 2 + 1];
68 let line_length = start.distance(end);
69 let angle = (end - start).y.atan2((end - start).x);
70
71 commands.spawn((
72 Mesh2d(meshes.add(Rectangle::new(line_length, 0.2))),
73 MeshMaterial2d(materials.add(ColorMaterial::from(Color::BLACK))),
74 Transform::from_translation(((start + end) / 2.0).extend(0.1))
75 .with_rotation(Quat::from_rotation_z(angle)),
76 ));
77 }
78
79 let last_upper = airfoil.points[(num_points_per_surface - 1) * 2];
81 let last_lower = airfoil.points[(num_points_per_surface - 1) * 2 + 1];
82 let line_length = last_upper.distance(last_lower);
83 let angle = (last_lower - last_upper)
84 .y
85 .atan2((last_lower - last_upper).x);
86
87 commands.spawn((
88 Mesh2d(meshes.add(Rectangle::new(line_length, 0.2))),
89 MeshMaterial2d(materials.add(ColorMaterial::from(Color::BLACK))),
90 Transform::from_translation(((last_upper + last_lower) / 2.0).extend(0.1))
91 .with_rotation(Quat::from_rotation_z(angle)),
92 ));
93
94 commands.spawn(airfoil); commands.spawn(SolidBoundary::new_wind_tunnel(config.domain.box_size)); for _ in 0..config.particle.initial_count {
98 let y = rng.gen_range(-config.domain.box_size.y / 2.0..config.domain.box_size.y / 2.0);
99 let position = Vec3::new(-config.domain.box_size.x / 2.0 + 0.0, y, 0.0);
100 let velocity =
101 wind_tunnel_velocity(y, config.domain.freestream_velocity, config.domain.box_size);
102
103 commands.spawn((
104 FluidParticle {
105 velocity,
106 density: config.fluid.rest_density,
107 pressure: 0.0,
108 },
109 Transform::from_translation(position),
110 Mesh2d(meshes.add(Circle::new(config.particle.radius))),
111 MeshMaterial2d(materials.add(ColorMaterial::from_color(RED))),
112 ));
113 }
114}
115
116pub fn update_spatial_grid(
117 mut grid: ResMut<SpatialGrid>,
118 query: Query<(Entity, &Transform)>,
119 config: Res<SPHConfig>,
120) {
121 let mut new_cells: HashMap<(i32, i32), Vec<(Entity, Vec2)>> = HashMap::new();
122
123 for (_, transform) in query.iter() {
125 let pos = transform.translation.truncate();
126 let cell = get_grid_cell(pos, config.particle.grid_cell_size);
127 new_cells
128 .entry(cell)
129 .or_insert_with(|| Vec::with_capacity(grid.expected_particles_per_cell));
130 }
131
132 for (entity, transform) in query.iter() {
134 let pos = transform.translation.truncate();
135 let cell = get_grid_cell(pos, config.particle.grid_cell_size);
136 if let Some(cell_vec) = new_cells.get_mut(&cell) {
137 cell_vec.push((entity, pos));
138 }
139 }
140
141 grid.cells = new_cells;
142}
143
144pub fn compute_density_pressure(
145 grid: Res<SpatialGrid>,
146 mut query: Query<(Entity, &Transform, &mut FluidParticle)>,
147 config: Res<SPHConfig>,
148) {
149 let particle_data: Vec<_> = query
150 .iter()
151 .map(|(entity, transform, _)| (entity, transform.translation.truncate()))
152 .collect();
153
154 query.par_iter_mut().for_each(|(entity, _, mut particle)| {
155 let pos = particle_data.iter().find(|(e, _)| *e == entity).unwrap().1;
156 let cell = get_grid_cell(pos, config.particle.grid_cell_size);
157
158 let mut density = config.particle.mass * w_poly6(0.0, config.fluid.smoothing_length);
159
160 for &neighbor_cell in &get_neighboring_cells(cell) {
162 if let Some(neighbors) = grid.cells.get(&neighbor_cell) {
163 for &(other_entity, other_pos) in neighbors {
164 if entity != other_entity {
165 let r = pos.distance(other_pos);
166 if r < config.fluid.smoothing_length {
167 density +=
168 config.particle.mass * w_poly6(r, config.fluid.smoothing_length);
169 }
170 }
171 }
172 }
173 }
174
175 let min_density = config.fluid.rest_density * 0.01;
176 particle.density = density.max(min_density);
177
178 particle.pressure = config.fluid.gas_constant
181 * particle.density
182 * ((particle.density / config.fluid.rest_density).powf(7.0) - 1.0); if particle.pressure.abs() < 1e-6 {
185 particle.pressure = 0.0; }
187 });
188}
189
190fn apply_boundary_forces(
191 particle_pos: Vec2,
192 particle_vel: Vec2,
193 particle_density: f32,
194 boundary: &SolidBoundary,
195 config: &SPHConfig,
196) -> Vec2 {
197 let mut force = Vec2::ZERO;
198 let ds = config.particle.radius * 0.25; const BOUNDARY_STIFFNESS: f32 = 500.0; const BOUNDARY_DAMPING: f32 = 1.0; let min_distance: f32 = config.particle.radius * 0.5; for i in 0..boundary.points.len() - 1 {
205 let p1 = boundary.points[i];
206 let p2 = boundary.points[i + 1];
207 let segment_length = p1.distance(p2);
208 let steps = (segment_length / ds).ceil() as usize;
209
210 for step in 0..steps {
211 let t = step as f32 / steps as f32;
212 let boundary_point = p1.lerp(p2, t);
213 let normal = boundary.normals[i];
214
215 let r = particle_pos.distance(boundary_point);
216
217 if r < config.fluid.smoothing_length {
218 let close_repulsion = if r < min_distance {
220 let penetration = (min_distance - r) / min_distance;
221 normal * BOUNDARY_STIFFNESS * 10.0 * penetration * penetration
222 } else {
223 Vec2::ZERO
224 };
225
226 let q = r / config.fluid.smoothing_length;
228 let pressure_force =
229 normal * BOUNDARY_STIFFNESS * (1.0 - q) * (1.0 - q) * (1.0 - q);
230
231 let relative_vel = particle_vel;
233 let damping_force = -relative_vel * BOUNDARY_DAMPING * (1.0 - q);
234
235 let force_contribution = (pressure_force + damping_force + close_repulsion) * ds;
237 force += force_contribution * (particle_density / config.fluid.rest_density);
238 }
239 }
240 }
241
242 force * config.particle.mass
243}
244
245pub fn apply_sph_forces(
246 mut commands: Commands,
247 grid: Res<SpatialGrid>,
248 boundary_query: Query<&SolidBoundary>,
249 mut query: Query<(Entity, &mut Transform, &mut FluidParticle)>,
250 mut particle_pool: ResMut<ParticlePool>,
251 mesh_query: Query<(&Mesh2d, &MeshMaterial2d<ColorMaterial>)>,
252 config: Res<SPHConfig>,
253) {
254 let particle_data: Vec<_> = query
256 .iter()
257 .map(|(entity, transform, particle)| {
258 (
259 entity,
260 transform.translation.truncate(),
261 particle.density,
262 particle.pressure,
263 particle.velocity,
264 )
265 })
266 .collect();
267
268 let forces = Mutex::new(Vec::with_capacity(particle_data.len()));
269 let config = config.clone();
270
271 query.par_iter().for_each(|(entity, _, _)| {
273 let (pos, density, pressure, velocity) = particle_data
274 .iter()
275 .find(|(e, ..)| *e == entity)
276 .map(|(_, pos, density, pressure, velocity)| (*pos, *density, *pressure, *velocity))
277 .unwrap();
278
279 let cell = get_grid_cell(pos, config.particle.grid_cell_size);
280 let mut pressure_force = Vec2::ZERO;
281 let mut viscosity_force = Vec2::ZERO;
282 let mut boundary_force = Vec2::ZERO;
283
284 for boundary in boundary_query.iter() {
285 boundary_force += apply_boundary_forces(pos, velocity, density, boundary, &config);
286 }
287
288 for &neighbor_cell in &get_neighboring_cells(cell) {
290 if let Some(neighbors) = grid.cells.get(&neighbor_cell) {
291 for &(other_entity, other_pos) in neighbors {
292 if entity != other_entity {
293 let r = pos.distance(other_pos);
294 if r < config.fluid.smoothing_length && r > 0.0 {
295 if let Some((_, _, other_density, other_pressure, other_velocity)) =
296 particle_data.iter().find(|(e, ..)| e == &other_entity)
297 {
298 let direction = (other_pos - pos) / r;
299
300 let pressure_factor =
302 (pressure + other_pressure) / (2.0 * density * other_density);
303 pressure_force -= direction
304 * config.particle.mass
305 * pressure_factor
306 * w_spiky_gradient(r, config.fluid.smoothing_length);
307
308 let velocity_diff = *other_velocity - velocity;
310 viscosity_force += config.fluid.viscosity
311 * config.particle.mass
312 * (velocity_diff / other_density)
313 * w_viscosity_laplacian(r, config.fluid.smoothing_length);
314 }
315 }
316 }
317 }
318 }
319 }
320
321 let total_force =
322 pressure_force + viscosity_force + boundary_force + config.domain.gravity * density;
323 forces.lock().unwrap().push((entity, total_force));
324 });
325
326 let forces = forces.lock().unwrap();
327 let to_despawn = Mutex::new(Vec::new());
328
329 query
331 .par_iter_mut()
332 .for_each(|(entity, mut transform, mut particle)| {
333 if let Some(&(_, force)) = forces.iter().find(|(e, _)| *e == entity) {
334 let dt = config.time.fixed_timestep;
335
336 let density = particle.density;
338 particle.velocity += force * dt / density;
339
340 let inlet_zone = -config.domain.box_size.x / 2.0;
342 if transform.translation.x < inlet_zone {
343 particle.velocity = wind_tunnel_velocity(
344 transform.translation.y,
345 config.domain.freestream_velocity,
346 config.domain.box_size,
347 );
348 }
349
350 let mut new_pos = transform.translation.truncate() + particle.velocity * dt;
352
353 let half_height = config.domain.box_size.y / 2.0 - config.particle.radius;
354 new_pos.y = new_pos.y.clamp(-half_height, half_height);
355
356 if new_pos.x + config.particle.radius > config.domain.box_size.x / 2.0 {
358 to_despawn.lock().unwrap().push(entity);
359 } else {
360 transform.translation = new_pos.extend(0.0);
361 }
362 }
363 });
364
365 for entity in to_despawn.lock().unwrap().iter() {
366 if let Ok((mesh, material)) = mesh_query.get(*entity) {
367 particle_pool
368 .available
369 .push((*entity, mesh.0.clone(), material.0.clone()));
370 commands
371 .entity(*entity)
372 .remove::<(Transform, FluidParticle)>();
373 }
374 }
375}
376
377pub fn spawn_particles(
378 mut commands: Commands,
379 mut pool: ResMut<ParticlePool>,
380 mut meshes: ResMut<Assets<Mesh>>,
381 mut materials: ResMut<Assets<ColorMaterial>>,
382 visualization_mode: Res<VisualizationMode>,
383 config: Res<SPHConfig>,
384) {
385 let mut rng = rand::thread_rng();
386
387 for _ in 0..config.particle.spawn_rate {
388 let y = rng.gen_range(-config.domain.box_size.y / 2.0..config.domain.box_size.y / 2.0);
389 let position = Vec3::new(-config.domain.box_size.x / 2.0 + 0.0, y, 0.0);
390 let velocity =
391 wind_tunnel_velocity(y, config.domain.freestream_velocity, config.domain.box_size);
392 let particle = FluidParticle {
393 velocity,
394 density: config.fluid.rest_density,
395 pressure: 0.0,
396 };
397
398 let color = match *visualization_mode {
400 VisualizationMode::Velocity => Color::hsl(240.0, 1.0, 0.5), VisualizationMode::Pressure => Color::hsl(120.0, 1.0, 0.5), VisualizationMode::Density => Color::hsl(270.0, 1.0, 0.5), };
404
405 if let Some((entity, _mesh, _material)) = pool.available.pop() {
406 commands
407 .entity(entity)
408 .insert((particle, Transform::from_translation(position)));
409 } else if pool.available.len() < pool.capacity {
410 let mesh = meshes.add(Circle::new(config.particle.radius));
411 let material = materials.add(ColorMaterial::from_color(color));
412
413 commands.spawn((
414 particle,
415 Transform::from_translation(position),
416 Mesh2d(mesh.clone()),
417 MeshMaterial2d(material.clone()),
418 ));
419 }
420 }
421}
422
423pub fn update_particle_colors(
424 visualization_mode: Res<VisualizationMode>,
425 mut query: Query<(&FluidParticle, &mut MeshMaterial2d<ColorMaterial>)>,
426 mut materials: ResMut<Assets<ColorMaterial>>,
427 config: Res<SPHConfig>,
428) {
429 for (particle, material) in query.iter_mut() {
430 let color = match *visualization_mode {
431 VisualizationMode::Velocity => {
432 let velocity_magnitude = particle.velocity.length();
433 let freestream = config.domain.freestream_velocity.length();
434 let normalized = (velocity_magnitude / (3.0 * freestream)).clamp(0.0, 1.0);
436 Color::hsl(240.0 * (1.0 - normalized), 1.0, 0.5)
438 }
439 VisualizationMode::Pressure => {
440 let particle_dynamic_pressure =
441 0.5 * particle.density * particle.velocity.length_squared();
442 let total_pressure = particle.pressure + particle_dynamic_pressure;
443
444 let max_q = 0.5
446 * config.fluid.rest_density
447 * config.domain.freestream_velocity.length_squared();
448 let normalized = (total_pressure / (max_q * 0.2)).clamp(-1.0, 1.0);
449 let hue = if normalized < 0.0 {
451 240.0 + (normalized * 240.0)
453 } else {
454 240.0 - (normalized * 240.0)
456 };
457 Color::hsl(hue.clamp(0.0, 360.0), 1.0, 0.5)
458 }
459 VisualizationMode::Density => {
460 let normalized =
462 ((particle.density / config.fluid.rest_density) - 1.0).clamp(-0.1, 0.1) * 2.0; let base_hue = 270.0;
464 let hue = base_hue - (normalized * 210.0); Color::hsl(hue.clamp(60.0, 270.0), 1.0, 0.5)
466 }
467 };
468 materials.get_mut(&material.0).unwrap().color = color;
469 }
470}
471
472pub fn handle_visualization_toggle(
473 keyboard: Res<ButtonInput<KeyCode>>,
474 mut visualization_mode: ResMut<VisualizationMode>,
475) {
476 if keyboard.just_pressed(KeyCode::Tab) {
477 *visualization_mode = match *visualization_mode {
478 VisualizationMode::Velocity => VisualizationMode::Pressure,
479 VisualizationMode::Pressure => VisualizationMode::Density,
480 VisualizationMode::Density => VisualizationMode::Velocity,
481 };
482 }
483}
484
485pub fn print_fps(
486 time: Res<Time>,
487 query: Query<&FluidParticle>,
488 mut stats: ResMut<SimulationStats>,
489) {
490 if time.elapsed_secs() - stats.last_update >= 1.0 {
491 stats.fps = 1.0 / time.delta_secs();
492 stats.particle_count = query.iter().count();
493 stats.last_update = time.elapsed_secs();
494
495 info!("FPS: {:.0}, Particles: {}", stats.fps, stats.particle_count);
496 }
497}
498
499pub fn pan_camera(
500 time: Res<Time>,
501 keyboard: Res<ButtonInput<KeyCode>>,
502 mut query: Query<(&mut Transform, &mut OrthographicProjection), With<Camera>>,
503) {
504 let (mut transform, mut projection) = query.single_mut();
505 let mut direction = Vec3::ZERO;
506 let speed = 150.0;
507
508 if keyboard.pressed(KeyCode::ArrowRight) {
509 direction.x += 1.0;
510 }
511 if keyboard.pressed(KeyCode::ArrowLeft) {
512 direction.x -= 1.0;
513 }
514 if keyboard.pressed(KeyCode::ArrowUp) {
515 direction.y += 1.0;
516 }
517 if keyboard.pressed(KeyCode::ArrowDown) {
518 direction.y -= 1.0;
519 }
520
521 if direction != Vec3::ZERO {
522 transform.translation +=
523 direction.normalize() * speed * time.delta_secs() * projection.scale;
524 }
525
526 let min_scale = 0.1;
528 let max_scale = 10.0;
529
530 if keyboard.pressed(KeyCode::Equal) {
531 projection.scale = (projection.scale * 0.95).max(min_scale);
532 }
533 if keyboard.pressed(KeyCode::Minus) {
534 projection.scale = (projection.scale * 1.05).min(max_scale);
535 }
536}