//! Zero-gravity cloth simulation: a free-floating spring net wrapped by a ball.//!//! Iterative-only: spring forces, self-collision and ball-cloth collision have//! no closed form; all state is carried across frames by `step`.//!//! The cloth is unpinned and weightless, so it floats perfectly flat (no//! self-weight sag). A ball is driven straight down through it: the cloth//! visibly wraps around the ball and is dragged down, then settles draped over//! it. The cloth is rendered as a shaded `MeshItem` surface (smooth normals//! each frame); the ball is a static sphere mesh moved by its transform.use ranim::{ color::palettes::manim, core::{ animation::{Eval, SegmentTime}, components::rgba::Rgba, }, glam::{DVec3, Mat4, Vec3, dvec3}, items::mesh::{MeshItem, Sphere, Surface}, prelude::*,};const ROWS: usize = 16;const COLS: usize = 16;const SPACING: f64 = 0.22;const CLOTH_Y: f64 = 2.2;const DAMPING: f64 = 0.99;const K_STRUCTURAL: f64 = 600.0;const K_SHEAR: f64 = 420.0;const K_BEND: f64 = 220.0;const REPULSION_CUTOFF: f64 = 0.35 * SPACING;const REPULSION_K: f64 = 300.0;const BALL_RADIUS: f64 = 0.65;/// The ball is driven straight down from `BALL_START_Y` at this speed (units/s).const BALL_SPEED: f64 = 1.2;const BALL_START_Y: f64 = 3.0;/// Where the ball stops, just below the cloth, letting the cloth settle draped over it.const BALL_STOP_Y: f64 = 1.4;const TOTAL_SECS: f64 = 7.0;/// A free-floating zero-gravity spring-net cloth and a driven ball.struct ClothWrap { curr: Vec<DVec3>, prev: Vec<DVec3>, springs: Vec<(usize, usize, f64, f64)>, // (i, j, rest_len, stiffness) initial: Vec<DVec3>, ball_mesh: MeshItem, cloth_indices: Vec<u32>,}impl ClothWrap { fn new() -> Self { let width = (COLS - 1) as f64 * SPACING; let mut curr = Vec::with_capacity(ROWS * COLS); for r in 0..ROWS { for c in 0..COLS { curr.push(dvec3( -width / 2.0 + c as f64 * SPACING, CLOTH_Y, -width / 2.0 + r as f64 * SPACING, )); } } let mut springs = Vec::new(); for r in 0..ROWS { for c in 0..COLS { let i = r * COLS + c; if c + 1 < COLS { springs.push((i, i + 1, SPACING, K_STRUCTURAL)); } if r + 1 < ROWS { springs.push((i, i + COLS, SPACING, K_STRUCTURAL)); } if c + 1 < COLS && r + 1 < ROWS { springs.push((i, i + COLS + 1, SPACING * 2.0f64.sqrt(), K_SHEAR)); } if c + 2 < COLS { springs.push((i, i + 2, 2.0 * SPACING, K_BEND)); } if r + 2 < ROWS { springs.push((i, i + 2 * COLS, 2.0 * SPACING, K_BEND)); } } } // Grid quads -> two triangles each, wound for +Y normals on the flat cloth. let mut cloth_indices = Vec::with_capacity(6 * (ROWS - 1) * (COLS - 1)); for r in 0..ROWS - 1 { for c in 0..COLS - 1 { let a = (r * COLS + c) as u32; let b = (r * COLS + c + 1) as u32; let d = ((r + 1) * COLS + c) as u32; let e = ((r + 1) * COLS + c + 1) as u32; cloth_indices.extend_from_slice(&[a, e, b, a, d, e]); } } let ball_mesh = MeshItem::from( Surface::from( Sphere::new(BALL_RADIUS) .with_resolution((20, 12)) .with_fill_color(manim::RED_C), ) .with_smooth_normals(), ); Self { initial: curr.clone(), prev: curr.clone(), curr, springs, ball_mesh, cloth_indices, } }}impl Eval for ClothWrap { type Output = Vec<MeshItem>; fn reset(&mut self) { self.curr = self.initial.clone(); self.prev = self.initial.clone(); } fn step(&mut self, time: &SegmentTime) { let dt2 = time.local_delta_secs * time.local_delta_secs; let n = self.curr.len(); // Spring forces (zero gravity: the cloth floats freely and stays flat). let mut ax = vec![0.0f64; n]; let mut ay = vec![0.0f64; n]; let mut az = vec![0.0f64; n]; for &(i, j, rest, k) in &self.springs { let d = self.curr[j] - self.curr[i]; let dist = d.length(); if dist < 1e-9 { continue; } let f = k * (dist - rest) / dist; ax[i] += f * d.x; ay[i] += f * d.y; az[i] += f * d.z; ax[j] -= f * d.x; ay[j] -= f * d.y; az[j] -= f * d.z; } // Self-collision repulsion (keeps the wrapped cloth from folding onto itself). for i in 0..n { for j in (i + 1)..n { let d = self.curr[j] - self.curr[i]; let dist = d.length(); if dist < REPULSION_CUTOFF && dist > 1e-9 { let f = REPULSION_K * (REPULSION_CUTOFF - dist) / dist; ax[i] -= f * d.x; ay[i] -= f * d.y; az[i] -= f * d.z; ax[j] += f * d.x; ay[j] += f * d.y; az[j] += f * d.z; } } } // Cloth Verlet integration (no gravity). for i in 0..n { let vx = (self.curr[i].x - self.prev[i].x) * DAMPING; let vy = (self.curr[i].y - self.prev[i].y) * DAMPING; let vz = (self.curr[i].z - self.prev[i].z) * DAMPING; self.prev[i] = self.curr[i]; self.curr[i].x += vx + ax[i] * dt2; self.curr[i].y += vy + ay[i] * dt2; self.curr[i].z += vz + az[i] * dt2; } // Ball position: driven straight down, then held below the cloth. let ball_y = (BALL_START_Y - BALL_SPEED * time.global_secs).max(BALL_STOP_Y); let ball_center = dvec3(0.0, ball_y, 0.0); // Ball-cloth collision: push cloth particles out of the ball. for i in 0..n { let d = self.curr[i] - ball_center; let dist = d.length(); if dist < BALL_RADIUS && dist > 1e-9 { self.curr[i] += d / dist * (BALL_RADIUS - dist); } } } fn sample(&self, time: &SegmentTime) -> Vec<MeshItem> { let points: Vec<Vec3> = self.curr.iter().map(|p| p.as_vec3()).collect(); // Smooth normals: accumulate face normals, then normalize. let mut normals = vec![Vec3::ZERO; points.len()]; for [a, b, c] in self.cloth_indices.as_chunks::<3>().0 { let (a, b, c) = (*a as usize, *b as usize, *c as usize); let face = (points[b] - points[a]).cross(points[c] - points[a]); normals[a] += face; normals[b] += face; normals[c] += face; } for n in &mut normals { *n = n.normalize_or_zero(); } let vertex_colors = vec![Rgba::from(manim::WHITE.with_alpha(1.0)); points.len()]; let cloth = MeshItem { points: points.into(), triangle_indices: self.cloth_indices.clone(), transform: Mat4::IDENTITY, vertex_colors: vertex_colors.into(), vertex_normals: normals.into(), }; let ball_y = (BALL_START_Y - BALL_SPEED * time.global_secs).max(BALL_STOP_Y); let mut ball = self.ball_mesh.clone(); ball.transform = Mat4::from_translation(dvec3(0.0, ball_y, 0.0).as_vec3()); vec![cloth, ball] }}#[scene]#[output(dir = "./output/cloth_wrap")]fn cloth_wrap(r: &mut RanimScene) { let mut camera = CameraFrame::default(); // Oblique downward view (~40° below horizontal, slightly asymmetric azimuth). camera.pos = dvec3(-4.0, 5.0, 2.5); camera.facing = (dvec3(0.0, 1.0, 0.0) - camera.pos).normalize(); camera.up = DVec3::Y; camera.perspective_blend = 1.0; camera.fovy = 45.0f64.to_radians(); r.play(camera.show().with_duration(TOTAL_SECS)); r.play(ClothWrap::new().with_duration(TOTAL_SECS));}