//! 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::Extract, core::animation::eval::iterative::Iterative, core::components::rgba::Rgba, core::core_item::CoreItem, glam::{DVec3, 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;/// The cloth-plus-ball system's full state.#[derive(Clone)]struct ClothState { curr: Vec<DVec3>, prev: Vec<DVec3>, springs: Vec<(usize, usize, f64, f64)>, // (i, j, rest_len, stiffness) ball_y: f64, ball_mesh: MeshItem, cloth_indices: Vec<u32>,}impl ClothState { 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 { prev: curr.clone(), curr, springs, ball_y: BALL_START_Y, ball_mesh, cloth_indices, } } fn step(&mut self, dt: f64, global_secs: f64) { let dt2 = dt * dt; 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 by the global (unwarped) clock, // then held below the cloth. It is kinematic state: updated here in // `step` and projected by `extract`. self.ball_y = (BALL_START_Y - BALL_SPEED * global_secs).max(BALL_STOP_Y); let ball_center = dvec3(0.0, self.ball_y, 0.0); // Ball-cloth collision: push cloth particles out of the ball. for curr in self.curr.iter_mut().take(n) { let d = *curr - ball_center; let dist = d.length(); if dist < BALL_RADIUS && dist > 1e-9 { *curr += d / dist * (BALL_RADIUS - dist); } } }}impl Extract for ClothState { type Target = CoreItem; fn extract_into(&self, buf: &mut Vec<CoreItem>) { let points: Vec<DVec3> = self.curr.clone(); // Smooth normals: accumulate face normals, then normalize. let mut normals = vec![DVec3::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(), vertex_colors: vertex_colors.into(), vertex_normals: normals.into(), }; let ball = self .ball_mesh .clone() .transformed(Translation(dvec3(0.0, self.ball_y, 0.0))); cloth.extract_into(buf); ball.extract_into(buf); }}#[scene]#[wasm_demo_doc]#[output(dir = "./output/cloth_wrap")]fn cloth_wrap(r: &mut RanimScene) { let sim_secs = 7.0; 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(sim_secs)); r.play( Iterative::from_fn( ClothState::new(), move |state: &mut ClothState, alpha: f64, delta_alpha: f64| { state.step(sim_secs * delta_alpha, sim_secs * alpha); }, ) .with_steps((sim_secs * 60.0) as usize) .with_duration(sim_secs), ); r.insert_time_mark(sim_secs, TimeMark::Capture("preview.png".to_string()));}