use gizmo_math::Vec3;
const PI: f32 = std::f32::consts::PI;
#[derive(Clone, Debug)]
pub struct Particle {
pub position: Vec3,
pub velocity: Vec3,
pub density: f32,
pub pressure: f32,
pub mass: f32,
pub id: u32,
}
impl Particle {
pub fn new(id: u32, position: Vec3, mass: f32) -> Self {
Self {
position,
velocity: Vec3::ZERO,
density: 0.0,
pressure: 0.0,
mass,
id,
}
}
pub fn kinetic_energy(&self) -> f32 {
0.5 * self.mass * self.velocity.length_squared()
}
}
#[derive(Clone, Debug)]
pub struct FluidConfig {
pub rest_density: f32,
pub viscosity: f32,
pub surface_tension: f32,
pub smoothing_radius: f32,
pub cfl_number: f32,
pub max_substeps: i32,
}
impl Default for FluidConfig {
fn default() -> Self {
Self {
rest_density: 1000.0,
viscosity: 0.001,
surface_tension: 0.0728,
smoothing_radius: 0.1,
cfl_number: 0.4,
max_substeps: 8,
}
}
}
pub fn w_poly6(r_sq: f32, h: f32) -> f32 {
let h_sq = h * h;
if r_sq >= 0.0 && r_sq <= h_sq {
let diff = h_sq - r_sq;
(315.0 / (64.0 * PI * h.powi(9))) * diff.powi(3)
} else {
0.0
}
}
pub fn grad_w_spiky(r: Vec3, r_len: f32, h: f32) -> Vec3 {
if r_len > 0.0 && r_len <= h {
let diff = h - r_len;
(r / r_len) * (-45.0 / (PI * h.powi(6))) * diff * diff
} else {
Vec3::ZERO
}
}
pub fn laplacian_w_viscosity(r_len: f32, h: f32) -> f32 {
if r_len > 0.0 && r_len <= h {
(45.0 / (PI * h.powi(6))) * (h - r_len)
} else {
0.0
}
}
pub fn w_cohesion(r_len: f32, h: f32) -> f32 {
let coeff = 32.0 / (PI * h.powi(9));
if r_len <= h && r_len > 0.0 {
let half_h = h * 0.5;
if r_len <= half_h {
let t1 = h - r_len;
coeff * (2.0 * t1.powi(3) * r_len.powi(3) - h.powi(6) / 64.0)
} else {
coeff * (h - r_len).powi(3) * r_len.powi(3)
}
} else {
0.0
}
}
pub struct FluidSystem {
particles: Vec<Particle>,
config: FluidConfig,
}
impl FluidSystem {
pub fn init(cfg: FluidConfig) -> Self {
Self {
particles: Vec::new(),
config: cfg,
}
}
pub fn add_particles(&mut self, ps: &[Particle]) {
self.particles.extend_from_slice(ps);
}
pub fn particles(&self) -> &[Particle] {
&self.particles
}
pub fn get_neighbors(&self, i: usize) -> Vec<usize> {
let pi = &self.particles[i];
let h_sq = self.config.smoothing_radius * self.config.smoothing_radius;
(0..self.particles.len())
.filter(|&j| {
j != i && (pi.position - self.particles[j].position).length_squared() < h_sq
})
.collect()
}
pub fn compute_density(&self, i: usize) -> f32 {
let pi = &self.particles[i];
let h = self.config.smoothing_radius;
let mut density = pi.mass * w_poly6(0.0, h);
for &j in &self.get_neighbors(i) {
let r_sq = (pi.position - self.particles[j].position).length_squared();
density += self.particles[j].mass * w_poly6(r_sq, h);
}
density
}
pub fn compute_pressure(&self, density: f32) -> f32 {
let k = 1000.0; k * (density / self.config.rest_density - 1.0).max(0.0)
}
pub fn compute_pressure_force(&self, i: usize) -> Vec3 {
let pi = &self.particles[i];
let h = self.config.smoothing_radius;
let mut force = Vec3::ZERO;
for &j in &self.get_neighbors(i) {
let pj = &self.particles[j];
let r = pi.position - pj.position;
let r_len = r.length().max(1e-6);
let avg_pressure = (pi.pressure + pj.pressure) * 0.5;
let grad = grad_w_spiky(r, r_len, h);
force -= (pj.mass / pj.density.max(1e-6)) * avg_pressure * grad;
}
force
}
pub fn compute_viscosity_force(&self, i: usize) -> Vec3 {
let pi = &self.particles[i];
let h = self.config.smoothing_radius;
let mut force = Vec3::ZERO;
for &j in &self.get_neighbors(i) {
let pj = &self.particles[j];
let r_len = (pi.position - pj.position).length().max(1e-6);
let lap = laplacian_w_viscosity(r_len, h);
force += (pj.mass / pj.density.max(1e-6)) * (pj.velocity - pi.velocity) * lap;
}
force * self.config.viscosity
}
pub fn compute_surface_tension_force(&self, i: usize) -> Vec3 {
let pi = &self.particles[i];
let h = self.config.smoothing_radius;
let mut force = Vec3::ZERO;
for &j in &self.get_neighbors(i) {
let pj = &self.particles[j];
let r = pi.position - pj.position;
let r_len = r.length().max(1e-6);
let w_coh = w_cohesion(r_len, h);
force -= pi.mass * pj.mass * w_coh * (r / r_len);
}
force * self.config.surface_tension
}
pub fn compute_adaptive_dt(&self, requested_dt: f32) -> f32 {
let v_max = self
.particles
.iter()
.map(|p| p.velocity.length())
.fold(0.0_f32, f32::max);
if v_max < 1e-6 {
return requested_dt;
}
let dt_cfl = self.config.cfl_number * self.config.smoothing_radius / v_max;
dt_cfl.min(requested_dt)
}
pub fn step(&mut self, dt: f32) {
let sub_dt = self.compute_adaptive_dt(dt);
let substeps = ((dt / sub_dt).ceil() as i32).min(self.config.max_substeps);
let actual_dt = dt / substeps as f32;
for _ in 0..substeps {
let densities: Vec<f32> = (0..self.particles.len())
.map(|i| self.compute_density(i))
.collect();
for (i, p) in self.particles.iter_mut().enumerate() {
p.density = densities[i];
p.pressure = 1000.0 * (p.density / self.config.rest_density - 1.0).max(0.0);
}
let forces: Vec<Vec3> = (0..self.particles.len())
.map(|i| {
let f_pressure = self.compute_pressure_force(i);
let f_viscosity = self.compute_viscosity_force(i);
let f_gravity = Vec3::new(0.0, -9.81, 0.0) * self.particles[i].mass;
f_pressure + f_viscosity + f_gravity
})
.collect();
for (i, p) in self.particles.iter_mut().enumerate() {
let accel = forces[i] / p.mass;
p.velocity += accel * actual_dt;
p.position += p.velocity * actual_dt;
}
}
}
}
fn make_system(particle_count: usize) -> FluidSystem {
let mut sys = FluidSystem::init(FluidConfig::default());
if particle_count > 0 {
let ps: Vec<Particle> = (0..particle_count)
.map(|i| Particle::new(i as u32, Vec3::new(i as f32 * 0.05, 0.0, 0.0), 0.01))
.collect();
sys.add_particles(&ps);
}
sys
}
pub fn weber_number(density: f32, velocity: f32, diameter: f32, surface_tension: f32) -> f32 {
density * velocity * velocity * diameter / surface_tension
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_particle_creation() {
let p = Particle::new(42, Vec3::new(1.0, 2.0, 3.0), 0.457);
assert_eq!(p.id, 42);
assert_eq!(p.position, Vec3::new(1.0, 2.0, 3.0));
assert_eq!(p.mass, 0.457);
assert_eq!(p.velocity, Vec3::ZERO);
}
#[test]
fn test_fluid_config_defaults() {
let cfg = FluidConfig::default();
assert_eq!(cfg.rest_density, 1000.0);
assert!((cfg.surface_tension - 0.0728).abs() < 1e-6);
assert_eq!(cfg.smoothing_radius, 0.1);
assert_eq!(cfg.cfl_number, 0.4);
assert_eq!(cfg.max_substeps, 8);
}
#[test]
fn test_kinetic_energy() {
let mut p = Particle::new(0, Vec3::ZERO, 2.0);
p.velocity = Vec3::new(3.0, 4.0, 0.0);
assert!((p.kinetic_energy() - 25.0).abs() < 1e-6);
}
#[test]
fn test_poly6_at_origin() {
let w0 = w_poly6(0.0, 0.1);
assert!(w0 > 0.0, "Poly6(0) > 0 olmalı: {}", w0);
}
#[test]
fn test_poly6_at_boundary() {
let h = 0.1;
assert!(w_poly6(h * h, h).abs() < 1e-6);
}
#[test]
fn test_poly6_outside_radius() {
assert_eq!(w_poly6(0.1 * 0.1 * 1.01, 0.1), 0.0);
}
#[test]
fn test_poly6_monotonically_decreasing() {
let h = 0.1;
let (w1, w2, w3) = (w_poly6(0.001, h), w_poly6(0.005, h), w_poly6(0.009, h));
assert!(
w1 > w2 && w2 > w3,
"Monoton azalmalı: {} > {} > {}",
w1,
w2,
w3
);
}
#[test]
fn test_poly6_normalization_3d() {
let h = 0.1;
let n = 80;
let dv = (2.0 * h / n as f32).powi(3);
let mut integral = 0.0_f64;
for iz in 0..n {
for iy in 0..n {
for ix in 0..n {
let x = -h + (ix as f32 + 0.5) * 2.0 * h / n as f32;
let y = -h + (iy as f32 + 0.5) * 2.0 * h / n as f32;
let z = -h + (iz as f32 + 0.5) * 2.0 * h / n as f32;
integral += w_poly6(x * x + y * y + z * z, h) as f64 * dv as f64;
}
}
}
assert!(
(integral - 1.0).abs() < 0.05,
"Poly6 integral ≈ 1.0: {:.4}",
integral
);
}
#[test]
fn test_spiky_gradient_direction() {
let grad = grad_w_spiky(Vec3::new(0.05, 0.0, 0.0), 0.05, 0.1);
assert!(grad.x < 0.0, "Spiky gradyan negatif x yönünde olmalı");
assert!(grad.y.abs() < 1e-6);
}
#[test]
fn test_spiky_gradient_antisymmetry() {
let r = Vec3::new(0.03, 0.04, 0.0);
let r_len = r.length();
let sum = grad_w_spiky(r, r_len, 0.1) + grad_w_spiky(-r, r_len, 0.1);
assert!(sum.length() < 1e-6, "∇W(r) + ∇W(-r) ≈ 0: {:?}", sum);
}
#[test]
fn test_viscosity_laplacian_positive() {
let h = 0.1;
for i in 1..100 {
let r = i as f32 * h / 100.0;
assert!(laplacian_w_viscosity(r, h) >= 0.0, "Negatif r={}", r);
}
}
#[test]
fn test_cohesion_kernel_zero_outside() {
let h = 0.1;
assert_eq!(w_cohesion(h * 1.01, h), 0.0);
assert_eq!(w_cohesion(0.0, h), 0.0);
}
#[test]
fn test_make_system() {
let sys = make_system(10);
assert_eq!(sys.particles().len(), 10);
assert_eq!(sys.particles()[0].mass, 0.01);
}
#[test]
fn test_density_single_particle() {
let sys = make_system(1);
let d = sys.compute_density(0);
assert!(d > 0.0, "Tek parçacık yoğunluğu > 0: {}", d);
}
#[test]
fn test_density_increases_with_neighbors() {
let s1 = make_system(1);
let s2 = make_system(2);
assert!(
s2.compute_density(0) > s1.compute_density(0),
"Komşu eklenince yoğunluk artmalı"
);
}
#[test]
fn test_density_symmetric() {
let mut sys = FluidSystem::init(FluidConfig::default());
sys.add_particles(&[
Particle::new(0, Vec3::new(-0.03, 0.0, 0.0), 0.01),
Particle::new(1, Vec3::new(0.03, 0.0, 0.0), 0.01),
]);
let (d0, d1) = (sys.compute_density(0), sys.compute_density(1));
assert!((d0 - d1).abs() < 1e-6, "Simetrik: {} vs {}", d0, d1);
}
#[test]
fn test_density_outside_radius() {
let s1 = make_system(1);
let mut s2 = FluidSystem::init(FluidConfig::default());
s2.add_particles(&[
Particle::new(0, Vec3::ZERO, 0.01),
Particle::new(1, Vec3::new(0.15, 0.0, 0.0), 0.01), ]);
assert!((s1.compute_density(0) - s2.compute_density(0)).abs() < 1e-6);
}
#[test]
fn test_pressure_zero_at_rest() {
let sys = make_system(0);
assert_eq!(sys.compute_pressure(1000.0), 0.0);
}
#[test]
fn test_pressure_positive_above_rest() {
let sys = make_system(0);
assert!(sys.compute_pressure(1200.0) > 0.0);
}
#[test]
fn test_pressure_zero_below_rest() {
let sys = make_system(0);
assert_eq!(sys.compute_pressure(800.0), 0.0);
}
#[test]
fn test_pressure_force_pushes_apart() {
let mut sys = FluidSystem::init(FluidConfig::default());
sys.add_particles(&[
Particle::new(0, Vec3::new(0.0, 0.0, 0.0), 0.5),
Particle::new(1, Vec3::new(0.03, 0.0, 0.0), 0.5),
Particle::new(2, Vec3::new(-0.03, 0.0, 0.0), 0.5),
]);
for i in 0..3 {
let d = sys.compute_density(i);
let p = sys.compute_pressure(d);
sys.particles[i].density = d;
sys.particles[i].pressure = p;
}
let f1 = sys.compute_pressure_force(1);
assert!(
f1.x > 0.0 || f1.length() > 0.0,
"Basınç kuvveti itici olmalı: {:?}",
f1
);
}
#[test]
fn test_viscosity_force_direction() {
let mut sys = FluidSystem::init(FluidConfig::default());
let mut p0 = Particle::new(0, Vec3::ZERO, 0.01);
p0.velocity = Vec3::ZERO;
p0.density = 1000.0;
let mut p1 = Particle::new(1, Vec3::new(0.05, 0.0, 0.0), 0.01);
p1.velocity = Vec3::new(1.0, 0.0, 0.0);
p1.density = 1000.0;
sys.add_particles(&[p0, p1]);
let f = sys.compute_viscosity_force(0);
assert!(f.x > 0.0, "Viskozite hızlı yöne çekmeli: {:?}", f);
}
#[test]
fn test_cfl_adaptive_dt() {
let mut sys = make_system(2);
sys.particles[0].velocity = Vec3::new(10.0, 0.0, 0.0);
let dt = sys.compute_adaptive_dt(1.0 / 60.0);
let expected = 0.4 * 0.1 / 10.0; assert!(
(dt - expected).abs() < 1e-6,
"CFL dt: {}, beklenen: {}",
dt,
expected
);
}
#[test]
fn test_cfl_no_motion() {
let sys = make_system(5);
let dt = sys.compute_adaptive_dt(1.0 / 60.0);
assert!(
(dt - 1.0 / 60.0).abs() < 1e-6,
"Hareket yoksa tam dt: {}",
dt
);
}
#[test]
fn test_cfl_limits_timestep() {
let mut sys = make_system(0);
let mut fast = Particle::new(0, Vec3::ZERO, 0.01);
fast.velocity = Vec3::new(1000.0, 0.0, 0.0); sys.add_particles(&[fast]);
let requested_dt = 0.016; let safe_dt = sys.compute_adaptive_dt(requested_dt);
assert!(
safe_dt < requested_dt,
"CFL koşulu büyük hızda dt'yi kısmalı: safe_dt={}, requested={}",
safe_dt,
requested_dt
);
assert!(safe_dt > 0.0, "dt her zaman pozitif olmalı: {}", safe_dt);
let expected = 0.4 * 0.1 / 1000.0; assert!(
(safe_dt - expected).abs() < 1e-8,
"CFL dt = {}, beklenen ≈ {}",
safe_dt,
expected
);
}
#[test]
fn test_mass_is_conserved() {
let mut sys = make_system(100);
let total_mass_before: f64 = sys.particles().iter().map(|p| p.mass as f64).sum();
for _ in 0..60 {
sys.step(0.016);
}
let total_mass_after: f64 = sys.particles().iter().map(|p| p.mass as f64).sum();
assert!(
(total_mass_before - total_mass_after).abs() < 1e-6,
"60 adım sonra kütle değişmemeli: önce={}, sonra={}",
total_mass_before,
total_mass_after
);
}
#[test]
fn test_gravity_accelerates_particles() {
let mut sys = make_system(0);
let mut p = Particle::new(0, Vec3::new(0.0, 10.0, 0.0), 0.01);
p.velocity = Vec3::ZERO;
sys.add_particles(&[p]);
let y0 = sys.particles()[0].position.y;
sys.step(0.1);
let y1 = sys.particles()[0].position.y;
assert!(
y1 < y0,
"Parçacık yerçekimi ile aşağı düşmeli: y0={}, y1={}",
y0,
y1
);
let expected_fall = 9.81 * 0.1 * 0.1;
assert!(
(y0 - y1 - expected_fall).abs() < 0.02,
"Serbest düşüş Δy ≈ {}: gerçek Δy = {}",
expected_fall,
y0 - y1
);
}
#[test]
fn test_density_approximates_rest_density() {
let mut sys = make_system(200);
for _ in 0..30 {
sys.step(0.008);
}
let avg_density: f32 = (0..sys.particles().len())
.map(|i| sys.compute_density(i))
.sum::<f32>()
/ sys.particles().len() as f32;
assert!(
avg_density > 0.0,
"Ortalama yoğunluk pozitif olmalı: {}",
avg_density
);
}
#[test]
fn test_pressure_force_repels_particles() {
let mut sys = FluidSystem::init(FluidConfig::default());
sys.add_particles(&[
Particle::new(0, Vec3::new(0.0, 0.0, 0.0), 0.5),
Particle::new(1, Vec3::new(0.01, 0.0, 0.0), 0.5), ]);
for i in 0..2 {
let d = sys.compute_density(i);
let p = sys.compute_pressure(d);
sys.particles[i].density = d;
sys.particles[i].pressure = p;
}
let f1 = sys.compute_pressure_force(0);
let f2 = sys.compute_pressure_force(1);
assert!(f1.x < 0.0, "p1 sola itilmeli: f1.x = {}", f1.x);
assert!(f2.x > 0.0, "p2 sağa itilmeli: f2.x = {}", f2.x);
assert!(
(f1.x + f2.x).abs() < (f1.x.abs() + f2.x.abs()) * 0.01,
"Momentum korunumu: f1.x + f2.x = {} (≈ 0 olmalı)",
f1.x + f2.x
);
}
#[test]
fn test_viscosity_dampens_velocity_differences() {
let mut sys = FluidSystem::init(FluidConfig::default());
let mut p1 = Particle::new(0, Vec3::new(0.0, 0.0, 0.0), 0.01);
p1.velocity = Vec3::new(1.0, 0.0, 0.0);
p1.density = 1000.0;
let mut p2 = Particle::new(1, Vec3::new(0.05, 0.0, 0.0), 0.01);
p2.velocity = Vec3::ZERO;
p2.density = 1000.0;
sys.add_particles(&[p1, p2]);
let vel_diff_before = 1.0_f32; for _ in 0..20 {
sys.step(0.016);
}
let v1 = sys.particles()[0].velocity.x;
let v2 = sys.particles()[1].velocity.x;
let vel_diff_after = (v1 - v2).abs();
assert!(
vel_diff_after < vel_diff_before,
"Viskozite hız farkını azaltmalı: önce={}, sonra={}",
vel_diff_before,
vel_diff_after
);
}
#[test]
fn test_simulation_is_deterministic() {
let run_sim = || {
let mut sys = make_system(50);
for _ in 0..100 {
sys.step(0.016);
}
sys.particles()[0].position
};
let pos1 = run_sim();
let pos2 = run_sim();
assert_eq!(pos1.x, pos2.x, "Determinizm: x eşit olmalı");
assert_eq!(pos1.y, pos2.y, "Determinizm: y eşit olmalı");
assert_eq!(pos1.z, pos2.z, "Determinizm: z eşit olmalı");
}
#[test]
fn test_spatial_hash_finds_correct_neighbors() {
let mut sys = FluidSystem::init(FluidConfig::default());
sys.add_particles(&[
Particle::new(0, Vec3::new(0.0, 0.0, 0.0), 0.01), Particle::new(1, Vec3::new(0.08, 0.0, 0.0), 0.01), Particle::new(2, Vec3::new(0.5, 0.0, 0.0), 0.01), ]);
let neighbors = sys.get_neighbors(0); let found_p2 = neighbors.contains(&1);
let found_p3 = neighbors.contains(&2);
assert!(found_p2, "h=0.1 içindeki parçacık komşu olmalı");
assert!(!found_p3, "h=0.1 dışındaki parçacık komşu olmamalı");
}
#[test]
fn test_performance_10k_particles() {
let mut sys = make_system(10_000);
let start = std::time::Instant::now();
sys.step(0.016);
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
assert!(
elapsed_ms < 30_000.0, "10K parçacık makul sürede güncellenmeli: {:.1}ms",
elapsed_ms
);
println!(" ⏱ 10K parçacık step süresi: {:.1}ms", elapsed_ms);
}
#[test]
fn test_surface_tension_droplets_coalesce() {
let mut sys = FluidSystem::init(FluidConfig::default());
let mut p1 = Particle::new(0, Vec3::new(0.0, 0.0, 0.0), 0.01);
p1.velocity = Vec3::new(0.5, 0.0, 0.0);
let mut p2 = Particle::new(1, Vec3::new(0.08, 0.0, 0.0), 0.01);
p2.velocity = Vec3::new(-0.5, 0.0, 0.0);
sys.add_particles(&[p1, p2]);
let dist_before = (sys.particles()[0].position - sys.particles()[1].position).length();
for _ in 0..30 {
sys.step(0.01);
}
let dist_after = (sys.particles()[0].position - sys.particles()[1].position).length();
assert!(
dist_after < dist_before,
"Karşılıklı hızlar damlaları yaklaştırmalı: önce={}, sonra={}",
dist_before,
dist_after
);
}
#[test]
fn test_step_moves_particles() {
let mut sys = make_system(3);
let p0_before = sys.particles()[1].position;
sys.step(1.0 / 60.0);
let p0_after = sys.particles()[1].position;
let displacement = (p0_after - p0_before).length();
assert!(displacement > 0.0, "step() parçacıkları hareket ettirmeli");
}
#[test]
fn test_step_gravity() {
let mut sys = make_system(1);
sys.step(1.0 / 60.0);
assert!(
sys.particles()[0].velocity.y < 0.0,
"Yerçekimi aşağı çekmeli"
);
}
#[test]
fn test_energy_bounded_after_steps() {
let mut sys = make_system(5);
let initial_ke: f32 = sys.particles().iter().map(|p| p.kinetic_energy()).sum();
for _ in 0..10 {
sys.step(1.0 / 60.0);
}
let final_ke: f32 = sys.particles().iter().map(|p| p.kinetic_energy()).sum();
assert!(
final_ke < 1000.0,
"Enerji patlaması olmamalı: {} → {}",
initial_ke,
final_ke
);
}
#[test]
fn test_weber_spray_threshold() {
let cfg = FluidConfig::default();
let diameter = 0.001;
let we_slow = weber_number(cfg.rest_density, 0.5, diameter, cfg.surface_tension);
let we_fast = weber_number(cfg.rest_density, 5.0, diameter, cfg.surface_tension);
assert!(we_slow < 12.0, "Yavaş We < 12: {}", we_slow);
assert!(we_fast > 12.0, "Hızlı We > 12: {}", we_fast);
}
#[test]
fn test_gpu_particle_size() {
let size = std::mem::size_of::<crate::gpu_fluid::types::FluidParticle>();
assert_eq!(size, 64, "GPU FluidParticle {} byte, beklenen 64", size);
assert_eq!(size % 16, 0, "16-byte aligned olmalı");
}
#[test]
fn test_gpu_params_alignment() {
let size = std::mem::size_of::<crate::gpu_fluid::types::FluidParams>();
assert_eq!(size % 16, 0, "FluidParams ({} bytes) 16-byte aligned", size);
}
#[test]
fn test_gpu_collider_size() {
assert_eq!(
std::mem::size_of::<crate::gpu_fluid::types::FluidCollider>(),
48
);
}
#[test]
fn test_gpu_particle_hash_size() {
assert_eq!(
std::mem::size_of::<crate::gpu_fluid::types::ParticleHash>(),
8
);
}
}