use std::f32::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Vec3 {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Vec3 {
pub const fn new(x: f32, y: f32, z: f32) -> Self {
Self { x, y, z }
}
pub const ZERO: Self = Self::new(0.0, 0.0, 0.0);
pub const UP: Self = Self::new(0.0, 1.0, 0.0);
pub const RIGHT: Self = Self::new(1.0, 0.0, 0.0);
pub const FORWARD: Self = Self::new(0.0, 0.0, 1.0);
pub fn dot(self, o: Self) -> f32 {
self.x * o.x + self.y * o.y + self.z * o.z
}
pub fn cross(self, o: Self) -> Self {
Self {
x: self.y * o.z - self.z * o.y,
y: self.z * o.x - self.x * o.z,
z: self.x * o.y - self.y * o.x,
}
}
pub fn length_sq(self) -> f32 {
self.dot(self)
}
pub fn length(self) -> f32 {
self.length_sq().sqrt()
}
pub fn normalized(self) -> Self {
let l = self.length();
if l > 1e-8 { self / l } else { Self::ZERO }
}
pub fn lerp(self, o: Self, t: f32) -> Self {
self + (o - self) * t
}
}
impl std::ops::Add for Vec3 {
type Output = Self;
fn add(self, o: Self) -> Self {
Self::new(self.x + o.x, self.y + o.y, self.z + o.z)
}
}
impl std::ops::Sub for Vec3 {
type Output = Self;
fn sub(self, o: Self) -> Self {
Self::new(self.x - o.x, self.y - o.y, self.z - o.z)
}
}
impl std::ops::Mul<f32> for Vec3 {
type Output = Self;
fn mul(self, s: f32) -> Self {
Self::new(self.x * s, self.y * s, self.z * s)
}
}
impl std::ops::Div<f32> for Vec3 {
type Output = Self;
fn div(self, s: f32) -> Self {
Self::new(self.x / s, self.y / s, self.z / s)
}
}
impl std::ops::Neg for Vec3 {
type Output = Self;
fn neg(self) -> Self {
Self::new(-self.x, -self.y, -self.z)
}
}
impl std::ops::AddAssign for Vec3 {
fn add_assign(&mut self, o: Self) {
*self = *self + o;
}
}
impl std::ops::SubAssign for Vec3 {
fn sub_assign(&mut self, o: Self) {
*self = *self - o;
}
}
impl std::ops::MulAssign<f32> for Vec3 {
fn mul_assign(&mut self, s: f32) {
*self = *self * s;
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CollisionShape {
Sphere { radius: f32 },
Box { half_extents: Vec3 },
Capsule { radius: f32, half_height: f32 },
}
#[derive(Debug, Clone)]
pub struct RigidBody {
pub position: Vec3,
pub velocity: Vec3,
pub acceleration: Vec3,
pub orientation: Quat,
pub angular_velocity: Vec3,
pub mass: f32,
pub inv_mass: f32,
pub inertia: f32,
pub inv_inertia: f32,
pub restitution: f32, pub friction: f32,
pub shape: CollisionShape,
pub is_static: bool,
pub gravity_scale: f32,
}
impl RigidBody {
pub fn new(shape: CollisionShape, mass: f32) -> Self {
let inv_mass = if mass > 0.0 { 1.0 / mass } else { 0.0 };
let inertia = match shape {
CollisionShape::Sphere { radius } => 0.4 * mass * radius * radius,
CollisionShape::Box { half_extents } => {
let m = mass / 12.0;
m * (half_extents.y * half_extents.y + half_extents.z * half_extents.z)
+ m * (half_extents.x * half_extents.x + half_extents.z * half_extents.z)
+ m * (half_extents.x * half_extents.x + half_extents.y * half_extents.y)
}
_ => 0.4 * mass,
};
let inv_inertia = if inertia > 0.0 { 1.0 / inertia } else { 0.0 };
Self {
position: Vec3::ZERO,
velocity: Vec3::ZERO,
acceleration: Vec3::ZERO,
orientation: Quat::IDENTITY,
angular_velocity: Vec3::ZERO,
mass,
inv_mass,
inertia,
inv_inertia,
restitution: 0.5,
friction: 0.3,
shape,
is_static: mass <= 0.0,
gravity_scale: 1.0,
}
}
pub fn apply_force(&mut self, force: Vec3) {
if !self.is_static {
self.acceleration += force * self.inv_mass;
}
}
pub fn apply_impulse(&mut self, impulse: Vec3) {
if !self.is_static {
self.velocity += impulse * self.inv_mass;
}
}
pub fn integrate(&mut self, dt: f32) {
if self.is_static {
return;
}
self.velocity += self.acceleration * dt;
self.position += self.velocity * dt;
self.acceleration = Vec3::ZERO;
if self.angular_velocity.length_sq() > 1e-12 {
let angle = self.angular_velocity.length() * dt;
let axis = self.angular_velocity.normalized();
let dq = Quat::from_axis_angle(axis, angle);
self.orientation = (dq * self.orientation).normalized();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Quat {
pub x: f32,
pub y: f32,
pub z: f32,
pub w: f32,
}
impl Quat {
pub const IDENTITY: Self = Self {
x: 0.0,
y: 0.0,
z: 0.0,
w: 1.0,
};
pub fn from_axis_angle(axis: Vec3, angle: f32) -> Self {
let half = angle * 0.5;
let s = half.sin();
Self {
x: axis.x * s,
y: axis.y * s,
z: axis.z * s,
w: half.cos(),
}
}
pub fn normalized(self) -> Self {
let l = (self.x * self.x + self.y * self.y + self.z * self.z + self.w * self.w).sqrt();
if l > 1e-8 {
Self {
x: self.x / l,
y: self.y / l,
z: self.z / l,
w: self.w / l,
}
} else {
Self::IDENTITY
}
}
}
impl std::ops::Mul for Quat {
type Output = Self;
fn mul(self, o: Self) -> Self {
Self {
x: self.w * o.x + self.x * o.w + self.y * o.z - self.z * o.y,
y: self.w * o.y - self.x * o.z + self.y * o.w + self.z * o.x,
z: self.w * o.z + self.x * o.y - self.y * o.x + self.z * o.w,
w: self.w * o.w - self.x * o.x - self.y * o.y - self.z * o.z,
}
}
}
#[derive(Debug, Clone)]
pub struct RigidConstraint {
pub body_a: usize,
pub body_b: usize,
pub anchor_a: Vec3, pub anchor_b: Vec3,
pub compliance: f32, }
pub struct RigidBodyWorld {
pub bodies: Vec<RigidBody>,
pub constraints: Vec<RigidConstraint>,
pub gravity: Vec3,
pub iterations: usize,
}
impl Default for RigidBodyWorld {
fn default() -> Self {
Self::new()
}
}
impl RigidBodyWorld {
pub fn new() -> Self {
Self {
bodies: Vec::new(),
constraints: Vec::new(),
gravity: Vec3::new(0.0, -9.81, 0.0),
iterations: 8,
}
}
pub fn add_body(&mut self, body: RigidBody) -> usize {
let id = self.bodies.len();
self.bodies.push(body);
id
}
pub fn step(&mut self, dt: f32) {
let sub_dt = dt / self.iterations as f32;
for _ in 0..self.iterations {
for body in &mut self.bodies {
if !body.is_static {
body.acceleration += self.gravity * body.gravity_scale;
}
}
for body in &mut self.bodies {
body.integrate(sub_dt);
}
for c in &self.constraints {
let (a, b) = self.bodies.split_at_mut(std::cmp::max(c.body_a, c.body_b));
let (a, b) = if c.body_a < c.body_b {
(&mut a[c.body_a], &mut b[0])
} else {
(&mut b[0], &mut a[c.body_b])
};
let world_anchor_a = a.position + c.anchor_a;
let world_anchor_b = b.position + c.anchor_b;
let delta = world_anchor_b - world_anchor_a;
let dist = delta.length();
if dist < 1e-8 {
continue;
}
let correction = delta * (dist / (a.inv_mass + b.inv_mass + c.compliance));
if !a.is_static {
a.position += correction * a.inv_mass;
}
if !b.is_static {
b.position -= correction * b.inv_mass;
}
}
for body in &mut self.bodies {
let ground_y = 0.0;
match body.shape {
CollisionShape::Sphere { radius } if body.position.y - radius < ground_y => {
body.position.y = ground_y + radius;
if body.velocity.y < 0.0 {
body.velocity.y *= -body.restitution;
body.velocity.x *= 1.0 - body.friction;
body.velocity.z *= 1.0 - body.friction;
}
}
CollisionShape::Box { half_extents }
if body.position.y - half_extents.y < ground_y =>
{
body.position.y = ground_y + half_extents.y;
if body.velocity.y < 0.0 {
body.velocity.y *= -body.restitution;
}
}
_ => {}
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct SoftParticle {
pub position: Vec3,
pub prev_position: Vec3,
pub velocity: Vec3,
pub mass: f32,
pub inv_mass: f32,
pub pinned: bool,
}
impl SoftParticle {
pub fn new(position: Vec3, mass: f32) -> Self {
Self {
position,
prev_position: position,
velocity: Vec3::ZERO,
mass,
inv_mass: if mass > 0.0 { 1.0 / mass } else { 0.0 },
pinned: false,
}
}
}
#[derive(Debug, Clone)]
pub struct DistanceConstraint {
pub a: usize,
pub b: usize,
pub rest_length: f32,
pub stiffness: f32, }
#[derive(Debug, Clone)]
pub struct VolumeConstraint {
pub particles: Vec<usize>,
pub rest_volume: f32,
pub pressure: f32,
}
pub struct SoftBody {
pub particles: Vec<SoftParticle>,
pub distance_constraints: Vec<DistanceConstraint>,
pub volume_constraints: Vec<VolumeConstraint>,
pub gravity: Vec3,
pub damping: f32,
pub iterations: usize,
}
impl Default for SoftBody {
fn default() -> Self {
Self::new()
}
}
impl SoftBody {
pub fn new() -> Self {
Self {
particles: Vec::new(),
distance_constraints: Vec::new(),
volume_constraints: Vec::new(),
gravity: Vec3::new(0.0, -9.81, 0.0),
damping: 0.99,
iterations: 4,
}
}
pub fn create_grid(
origin: Vec3,
cells_x: usize,
cells_y: usize,
cell_size: f32,
mass: f32,
) -> Self {
let mut body = Self::new();
for y in 0..=cells_y {
for x in 0..=cells_x {
let pos = Vec3::new(
origin.x + x as f32 * cell_size,
origin.y + y as f32 * cell_size,
origin.z,
);
body.particles.push(SoftParticle::new(pos, mass));
}
}
let cols = cells_x + 1;
for y in 0..=cells_y {
for x in 0..=cells_x {
let idx = y * cols + x;
if x < cells_x {
body.distance_constraints.push(DistanceConstraint {
a: idx,
b: idx + 1,
rest_length: cell_size,
stiffness: 0.8,
});
}
if y < cells_y {
body.distance_constraints.push(DistanceConstraint {
a: idx,
b: idx + cols,
rest_length: cell_size,
stiffness: 0.8,
});
}
if x < cells_x && y < cells_y {
let diag = (cell_size * cell_size * 2.0).sqrt();
body.distance_constraints.push(DistanceConstraint {
a: idx,
b: idx + cols + 1,
rest_length: diag,
stiffness: 0.5,
});
body.distance_constraints.push(DistanceConstraint {
a: idx + 1,
b: idx + cols,
rest_length: diag,
stiffness: 0.5,
});
}
}
}
body
}
pub fn step(&mut self, dt: f32) {
let sub_dt = dt / self.iterations as f32;
for _ in 0..self.iterations {
for p in &mut self.particles {
if p.pinned {
continue;
}
let vel = (p.position - p.prev_position) * self.damping;
p.prev_position = p.position;
p.position += vel + self.gravity * sub_dt * sub_dt;
}
for c in &self.distance_constraints {
let (a, b) = self.particles.split_at_mut(std::cmp::max(c.a, c.b));
let (a, b) = if c.a < c.b {
(&mut a[c.a], &mut b[0])
} else {
(&mut b[0], &mut a[c.b])
};
let delta = b.position - a.position;
let dist = delta.length();
if dist < 1e-8 {
continue;
}
let diff = (dist - c.rest_length) / dist;
let correction = delta * diff * c.stiffness * 0.5;
if !a.pinned {
a.position += correction;
}
if !b.pinned {
b.position -= correction;
}
}
for p in &mut self.particles {
if p.pinned {
continue;
}
if p.position.y < 0.0 {
p.position.y = 0.0;
}
}
}
for p in &mut self.particles {
if !p.pinned {
p.velocity = (p.position - p.prev_position) / dt;
}
}
}
}
#[derive(Debug, Clone)]
pub struct ClothParticle {
pub position: Vec3,
pub prev_position: Vec3,
pub uv: (f32, f32),
pub mass: f32,
pub inv_mass: f32,
pub pinned: bool,
}
#[derive(Debug, Clone)]
pub struct ClothConstraint {
pub a: usize,
pub b: usize,
pub rest_length: f32,
}
pub struct Cloth {
pub particles: Vec<ClothParticle>,
pub constraints: Vec<ClothConstraint>,
pub cols: usize,
pub rows: usize,
pub gravity: Vec3,
pub damping: f32,
pub wind: Vec3,
pub iterations: usize,
}
impl Default for Cloth {
fn default() -> Self {
Self::new()
}
}
impl Cloth {
pub fn new() -> Self {
Self {
particles: Vec::new(),
constraints: Vec::new(),
cols: 0,
rows: 0,
gravity: Vec3::new(0.0, -9.81, 0.0),
damping: 0.98,
wind: Vec3::ZERO,
iterations: 5,
}
}
pub fn create_sheet(
origin: Vec3,
width: f32,
height: f32,
segments_x: usize,
segments_y: usize,
mass: f32,
) -> Self {
let mut cloth = Self::new();
cloth.cols = segments_x + 1;
cloth.rows = segments_y + 1;
let dx = width / segments_x as f32;
let dy = height / segments_y as f32;
for y in 0..=segments_y {
for x in 0..=segments_x {
let pos = Vec3::new(
origin.x + x as f32 * dx - width * 0.5,
origin.y,
origin.y + y as f32 * dy - height * 0.5,
);
cloth.particles.push(ClothParticle {
position: pos,
prev_position: pos,
uv: (x as f32 / segments_x as f32, y as f32 / segments_y as f32),
mass,
inv_mass: 1.0 / mass,
pinned: false,
});
}
}
for y in 0..=segments_y {
for x in 0..=segments_x {
let idx = y * cloth.cols + x;
if x < segments_x {
cloth.constraints.push(ClothConstraint {
a: idx,
b: idx + 1,
rest_length: dx,
});
}
if y < segments_y {
cloth.constraints.push(ClothConstraint {
a: idx,
b: idx + cloth.cols,
rest_length: dy,
});
}
if x < segments_x && y < segments_y {
let diag = (dx * dx + dy * dy).sqrt();
cloth.constraints.push(ClothConstraint {
a: idx,
b: idx + cloth.cols + 1,
rest_length: diag,
});
}
if x + 2 <= segments_x {
cloth.constraints.push(ClothConstraint {
a: idx,
b: idx + 2,
rest_length: dx * 2.0,
});
}
if y + 2 <= segments_y {
cloth.constraints.push(ClothConstraint {
a: idx,
b: idx + cloth.cols * 2,
rest_length: dy * 2.0,
});
}
}
}
cloth
}
pub fn pin_top(&mut self) {
for x in 0..self.cols {
self.particles[x].pinned = true;
self.particles[x].inv_mass = 0.0;
}
}
pub fn step(&mut self, dt: f32) {
let sub_dt = dt / self.iterations as f32;
for _ in 0..self.iterations {
for p in &mut self.particles {
if p.pinned {
continue;
}
let vel = (p.position - p.prev_position) * self.damping;
p.prev_position = p.position;
let force = self.gravity + self.wind;
p.position += vel + force * sub_dt * sub_dt;
}
for c in &self.constraints {
let (a, b) = self.particles.split_at_mut(std::cmp::max(c.a, c.b));
let (a, b) = if c.a < c.b {
(&mut a[c.a], &mut b[0])
} else {
(&mut b[0], &mut a[c.b])
};
let delta = b.position - a.position;
let dist = delta.length();
if dist < 1e-8 {
continue;
}
let diff = (dist - c.rest_length) / dist;
let correction = delta * diff * 0.5;
if !a.pinned {
a.position += correction;
}
if !b.pinned {
b.position -= correction;
}
}
for p in &mut self.particles {
if p.pinned {
continue;
}
if p.position.y < -2.0 {
p.position.y = -2.0;
}
}
}
}
pub fn compute_normals(&self) -> Vec<Vec3> {
let n = self.particles.len();
let mut normals = vec![Vec3::ZERO; n];
for y in 0..self.rows.saturating_sub(1) {
for x in 0..self.cols.saturating_sub(1) {
let i0 = y * self.cols + x;
let i1 = i0 + 1;
let i2 = i0 + self.cols;
let i3 = i2 + 1;
if i3 >= n {
continue;
}
let v0 = self.particles[i0].position;
let v1 = self.particles[i1].position;
let v2 = self.particles[i2].position;
let n1 = (v1 - v0).cross(v2 - v0).normalized();
normals[i0] += n1;
normals[i1] += n1;
normals[i2] += n1;
let v3 = self.particles[i3].position;
let n2 = (v2 - v1).cross(v3 - v1).normalized();
normals[i1] += n2;
normals[i2] += n2;
normals[i3] += n2;
}
}
normals.iter_mut().for_each(|n| {
let l = n.length();
if l > 1e-8 {
*n = *n / l;
} else {
*n = Vec3::UP;
}
});
normals
}
}
#[derive(Debug, Clone)]
pub struct FluidParticle {
pub position: Vec3,
pub velocity: Vec3,
pub density: f32,
pub pressure: f32,
}
pub struct FluidSimulation {
pub particles: Vec<FluidParticle>,
pub rest_density: f32,
pub gas_constant: f32,
pub viscosity: f32,
pub particle_mass: f32,
pub smoothing_radius: f32,
pub gravity: Vec3,
cell_size: f32,
grid: std::collections::HashMap<(i32, i32, i32), Vec<usize>>,
}
impl Default for FluidSimulation {
fn default() -> Self {
Self::new()
}
}
impl FluidSimulation {
pub fn new() -> Self {
Self {
particles: Vec::new(),
rest_density: 1000.0,
gas_constant: 2000.0,
viscosity: 0.1,
particle_mass: 1.0,
smoothing_radius: 0.1,
gravity: Vec3::new(0.0, -9.81, 0.0),
cell_size: 0.1,
grid: std::collections::HashMap::new(),
}
}
pub fn add_particle(&mut self, position: Vec3) {
self.particles.push(FluidParticle {
position,
velocity: Vec3::ZERO,
density: 0.0,
pressure: 0.0,
});
}
fn hash_position(&self, pos: Vec3) -> (i32, i32, i32) {
(
(pos.x / self.cell_size).floor() as i32,
(pos.y / self.cell_size).floor() as i32,
(pos.z / self.cell_size).floor() as i32,
)
}
fn build_grid(&mut self) {
self.grid.clear();
for (i, p) in self.particles.iter().enumerate() {
let key = self.hash_position(p.position);
self.grid.entry(key).or_default().push(i);
}
}
fn neighbors(&self, pos: Vec3) -> Vec<usize> {
let h = self.hash_position(pos);
let mut result = Vec::new();
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
if let Some(indices) = self.grid.get(&(h.0 + dx, h.1 + dy, h.2 + dz)) {
result.extend(indices);
}
}
}
}
result
}
fn poly6(&self, r_sq: f32) -> f32 {
let h = self.smoothing_radius;
let h2 = h * h;
if r_sq >= h2 {
return 0.0;
}
let diff = h2 - r_sq;
let coeff = 315.0 / (64.0 * PI * h.powi(9));
coeff * diff * diff * diff
}
fn spiky_grad(&self, r: f32) -> f32 {
let h = self.smoothing_radius;
if r >= h || r < 1e-8 {
return 0.0;
}
let coeff = -45.0 / (PI * h.powi(6));
coeff * (h - r) * (h - r)
}
fn viscosity_lap(&self, r: f32) -> f32 {
let h = self.smoothing_radius;
if r >= h {
return 0.0;
}
let coeff = 45.0 / (PI * h.powi(6));
coeff * (h - r)
}
pub fn step(&mut self, dt: f32) {
self.build_grid();
let n = self.particles.len();
let mut densities = vec![0.0f32; n];
for i in 0..n {
let pos = self.particles[i].position;
let neighbors = self.neighbors(pos);
let mut density = 0.0;
for &j in &neighbors {
let r_sq = (self.particles[j].position - pos).length_sq();
density += self.particle_mass * self.poly6(r_sq);
}
densities[i] = density.max(self.rest_density * 0.01);
self.particles[i].pressure = self.gas_constant * (densities[i] - self.rest_density);
}
for i in 0..n {
let pos = self.particles[i].position;
let neighbors = self.neighbors(pos);
let mut pressure_force = Vec3::ZERO;
let mut viscosity_force = Vec3::ZERO;
for &j in &neighbors {
if i == j {
continue;
}
let diff = self.particles[i].position - self.particles[j].position;
let r = diff.length();
if r < 1e-8 || r >= self.smoothing_radius {
continue;
}
let dir = diff / r;
let pressure_term = -self.particle_mass
* (self.particles[i].pressure + self.particles[j].pressure)
/ (2.0 * densities[j])
* self.spiky_grad(r);
pressure_force += dir * pressure_term;
let visc_scalar =
self.viscosity * self.particle_mass / densities[j] * self.viscosity_lap(r);
viscosity_force +=
(self.particles[j].velocity - self.particles[i].velocity) * visc_scalar;
}
let total_force = pressure_force + viscosity_force + self.gravity * densities[i];
let acceleration = total_force / densities[i];
self.particles[i].velocity += acceleration * dt;
let vel = self.particles[i].velocity;
self.particles[i].position += vel * dt;
let pos = &mut self.particles[i].position;
for axis in [&mut pos.x, &mut pos.y, &mut pos.z] {
*axis = (*axis).clamp(0.0, 5.0);
}
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct SmokeCell {
pub density: f32,
pub temperature: f32,
pub fuel: f32,
pub velocity: Vec3,
}
pub struct SmokeSimulation {
pub grid: Vec<SmokeCell>,
pub size_x: usize,
pub size_y: usize,
pub size_z: usize,
pub ambient_temp: f32,
pub cooling_rate: f32,
pub buoyancy: f32,
pub dissipation: f32,
}
impl SmokeSimulation {
pub fn new(size_x: usize, size_y: usize, size_z: usize) -> Self {
let n = size_x * size_y * size_z;
Self {
grid: vec![SmokeCell::default(); n],
size_x,
size_y,
size_z,
ambient_temp: 0.0,
cooling_rate: 0.01,
buoyancy: 0.5,
dissipation: 0.99,
}
}
fn idx(&self, x: usize, y: usize, z: usize) -> usize {
x + y * self.size_x + z * self.size_x * self.size_y
}
pub fn add_source(&mut self, x: usize, y: usize, z: usize, density: f32, temp: f32) {
let i = self.idx(x, y, z);
self.grid[i].density += density;
self.grid[i].temperature = self.grid[i].temperature.max(temp);
self.grid[i].fuel = 1.0;
}
pub fn step(&mut self, dt: f32) {
let nx = self.size_x;
let ny = self.size_y;
let nz = self.size_z;
for z in 0..nz {
for y in 0..ny {
for x in 0..nx {
let i = self.idx(x, y, z);
let cell = &mut self.grid[i];
if cell.fuel > 0.0 && cell.temperature > 0.5 {
cell.fuel -= dt * 0.5;
cell.temperature += dt * 2.0;
cell.density += dt * 0.1;
}
cell.temperature -= self.cooling_rate * dt;
cell.temperature = cell.temperature.max(self.ambient_temp);
let buoyancy_force =
self.buoyancy * (cell.temperature - self.ambient_temp) - cell.density * 0.1;
cell.velocity.y += buoyancy_force * dt;
cell.density *= self.dissipation;
}
}
}
let mut new_grid = self.grid.clone();
for z in 1..nz - 1 {
for y in 1..ny - 1 {
for x in 1..nx - 1 {
let i = self.idx(x, y, z);
let vel = &self.grid[i].velocity;
let fx = (x as f32 - vel.x * dt).clamp(0.5, (nx - 1) as f32 - 0.5);
let fy = (y as f32 - vel.y * dt).clamp(0.5, (ny - 1) as f32 - 0.5);
let fz = (z as f32 - vel.z * dt).clamp(0.5, (nz - 1) as f32 - 0.5);
let x0 = fx.floor() as usize;
let y0 = fy.floor() as usize;
let z0 = fz.floor() as usize;
let x1 = (x0 + 1).min(nx - 1);
let y1 = (y0 + 1).min(ny - 1);
let z1 = (z0 + 1).min(nz - 1);
let tx = fx - x0 as f32;
let ty = fy - y0 as f32;
let tz = fz - z0 as f32;
let c000 = self.grid[self.idx(x0, y0, z0)];
let c100 = self.grid[self.idx(x1, y0, z0)];
let c010 = self.grid[self.idx(x0, y1, z0)];
let c110 = self.grid[self.idx(x1, y1, z0)];
let c001 = self.grid[self.idx(x0, y0, z1)];
let c101 = self.grid[self.idx(x1, y0, z1)];
let c011 = self.grid[self.idx(x0, y1, z1)];
let c111 = self.grid[self.idx(x1, y1, z1)];
let d00 = c000.density * (1.0 - tx) + c100.density * tx;
let d10 = c010.density * (1.0 - tx) + c110.density * tx;
let d01 = c001.density * (1.0 - tx) + c101.density * tx;
let d11 = c011.density * (1.0 - tx) + c111.density * tx;
let d0 = d00 * (1.0 - ty) + d10 * ty;
let d1 = d01 * (1.0 - ty) + d11 * ty;
new_grid[i].density = d0 * (1.0 - tz) + d1 * tz;
let t00 = c000.temperature * (1.0 - tx) + c100.temperature * tx;
let t10 = c010.temperature * (1.0 - tx) + c110.temperature * tx;
let t01 = c001.temperature * (1.0 - tx) + c101.temperature * tx;
let t11 = c011.temperature * (1.0 - tx) + c111.temperature * tx;
let t0 = t00 * (1.0 - ty) + t10 * ty;
let t1 = t01 * (1.0 - ty) + t11 * ty;
new_grid[i].temperature = t0 * (1.0 - tz) + t1 * tz;
}
}
}
self.grid = new_grid;
}
pub fn emission_color(&self, x: usize, y: usize, z: usize) -> [f32; 4] {
let cell = &self.grid[self.idx(x, y, z)];
let t = cell.temperature.clamp(0.0, 3.0);
let r = (t * 0.8).min(1.0);
let g = ((t - 0.5) * 0.6).clamp(0.0, 1.0);
let b = ((t - 1.5) * 0.4).clamp(0.0, 1.0);
[r, g, b, cell.density.min(1.0)]
}
}
#[derive(Debug, Clone)]
pub struct GerstnerWave {
pub direction: Vec3,
pub amplitude: f32,
pub wavelength: f32,
pub speed: f32,
pub steepness: f32, }
impl GerstnerWave {
pub fn new(direction: Vec3, amplitude: f32, wavelength: f32, steepness: f32) -> Self {
let g = 9.81;
let k = 2.0 * PI / wavelength;
let speed = (g / k).sqrt();
Self {
direction: direction.normalized(),
amplitude,
wavelength,
speed,
steepness: steepness.clamp(0.0, 1.0),
}
}
pub fn evaluate(&self, pos: Vec3, time: f32) -> Vec3 {
let k = 2.0 * PI / self.wavelength;
let c = self.speed;
let d_dot_p = self.direction.x * pos.x + self.direction.z * pos.z;
let phase = k * (d_dot_p - c * time);
let cos_phase = phase.cos();
let sin_phase = phase.sin();
Vec3::new(
self.steepness * self.amplitude * self.direction.x * cos_phase,
self.amplitude * sin_phase,
self.steepness * self.amplitude * self.direction.z * cos_phase,
)
}
pub fn normal(&self, pos: Vec3, time: f32) -> Vec3 {
let k = 2.0 * PI / self.wavelength;
let c = self.speed;
let d_dot_p = self.direction.x * pos.x + self.direction.z * pos.z;
let phase = k * (d_dot_p - c * time);
let cos_phase = phase.cos();
let wa = self.amplitude * k;
let dx = -self.direction.x * wa * self.steepness * cos_phase;
let dy = wa * phase.cos();
let dz = -self.direction.z * wa * self.steepness * cos_phase;
Vec3::new(dx, dy, dz).normalized()
}
}
pub struct OceanWaves {
pub waves: Vec<GerstnerWave>,
pub time: f32,
}
impl Default for OceanWaves {
fn default() -> Self {
Self::new()
}
}
impl OceanWaves {
pub fn new() -> Self {
Self {
waves: Vec::new(),
time: 0.0,
}
}
pub fn create_phillips(wind_speed: f32, wind_dir: Vec3, wave_count: usize) -> Self {
let mut ocean = Self::new();
let dir = wind_dir.normalized();
let perp = Vec3::new(-dir.z, 0.0, dir.x);
for i in 0..wave_count {
let t = i as f32 / wave_count as f32;
let wavelength = 0.5 + t * 20.0;
let spread = (t * 0.8).min(0.8);
let angle = (t - 0.5) * spread * PI;
let wave_dir = Vec3::new(
dir.x * angle.cos() - perp.x * angle.sin(),
0.0,
dir.z * angle.cos() - perp.z * angle.sin(),
)
.normalized();
let k = 2.0 * PI / wavelength;
let g = 9.81;
let a = 0.0001;
let l = wind_speed * wind_speed / g;
let phillips = a * (-1.0 / (k * l * k * l)).exp() / (k * k * k * k);
let amplitude = phillips.sqrt() * 0.5;
let steepness = 0.3 + t * 0.2;
ocean.waves.push(GerstnerWave::new(
wave_dir, amplitude, wavelength, steepness,
));
}
ocean
}
pub fn step(&mut self, dt: f32) {
self.time += dt;
}
pub fn displacement(&self, pos: Vec3) -> Vec3 {
let mut result = Vec3::ZERO;
for wave in &self.waves {
result += wave.evaluate(pos, self.time);
}
result
}
pub fn normal(&self, pos: Vec3) -> Vec3 {
let mut result = Vec3::UP;
for wave in &self.waves {
result += wave.normal(pos, self.time);
}
result.normalized()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vec3_basic() {
let a = Vec3::new(1.0, 2.0, 3.0);
let b = Vec3::new(4.0, 5.0, 6.0);
assert_eq!(a + b, Vec3::new(5.0, 7.0, 9.0));
assert_eq!(a - b, Vec3::new(-3.0, -3.0, -3.0));
assert_eq!(a * 2.0, Vec3::new(2.0, 4.0, 6.0));
assert!((a.dot(b) - 32.0).abs() < 1e-6);
}
#[test]
fn test_rigid_body_fall() {
let mut world = RigidBodyWorld::new();
let mut body = RigidBody::new(CollisionShape::Sphere { radius: 0.5 }, 1.0);
body.position.y = 1.0;
world.add_body(body);
world.step(0.016);
assert!(world.bodies[0].position.y < 1.0);
}
#[test]
fn test_soft_body_grid() {
let mut body = SoftBody::create_grid(Vec3::new(0.0, 1.0, 0.0), 4, 4, 0.25, 1.0);
body.step(0.016);
assert!(body.particles[0].position.y <= 1.0);
}
#[test]
fn test_cloth_sheet() {
let mut cloth = Cloth::create_sheet(Vec3::new(0.0, 2.0, 0.0), 2.0, 2.0, 8, 8, 0.1);
cloth.pin_top();
for _ in 0..30 {
cloth.step(0.016);
}
let bottom_right = (cloth.rows - 1) * cloth.cols + (cloth.cols - 1);
assert!(cloth.particles[bottom_right].position.y < 2.0);
}
#[test]
fn test_fluid_particles() {
let mut fluid = FluidSimulation::new();
for i in 0..10 {
fluid.add_particle(Vec3::new(
2.5 + (i % 3) as f32 * 0.05,
2.0 + (i / 3) as f32 * 0.05,
2.5,
));
}
fluid.step(0.016);
assert!(fluid.particles[0].position.y < 2.0);
}
#[test]
fn test_smoke_rise() {
let mut smoke = SmokeSimulation::new(16, 16, 16);
smoke.buoyancy = 50.0; smoke.add_source(8, 2, 8, 1.0, 2.0);
for _ in 0..60 {
smoke.step(0.016);
}
let color_at_3 = smoke.emission_color(8, 3, 8);
let color_at_2 = smoke.emission_color(8, 2, 8);
assert!(
color_at_3[3] > 0.0 || color_at_2[3] > 0.0,
"smoke not present at y=2 or y=3: y2={:?}, y3={:?}",
color_at_2,
color_at_3
);
}
#[test]
fn test_ocean_waves() {
let mut ocean = OceanWaves::create_phillips(10.0, Vec3::FORWARD, 8);
ocean.step(0.016);
let disp = ocean.displacement(Vec3::new(1.0, 0.0, 1.0));
assert!(disp.length_sq() > 0.0);
}
}