1use crate::choice_enum;
6use crate::components::*;
7use crate::hierarchy;
8use crate::mesh::{MeshData, Vertex};
9use crate::noise::{Noise, NoiseKind};
10use crate::procedural::signature;
11use crate::reflect::Field;
12use glam::{Mat4, Vec3};
13use kvas_ecs::{Entity, World};
14use serde::{Deserialize, Serialize};
15use std::sync::Arc;
16
17choice_enum!(
18 ClothPin { Free, TopEdge, TopCorners, LeftEdge } default TopEdge);
20
21#[derive(Clone, Serialize, Deserialize)]
24#[serde(default)]
25pub struct Cloth {
26 pub width: f32,
28 pub height: f32,
30 pub segments: i32,
32 pub pin: ClothPin,
34 pub stiffness: f32,
36 pub damping: f32,
38 pub gravity_scale: f32,
40 pub wind: Vec3,
42 pub gust: f32,
44 pub drag: f32,
46 pub tear: f32,
48 pub collide_ground: bool,
50 pub ground_y: f32,
52 pub collide_objects: bool,
54 #[serde(skip)]
55 #[doc(hidden)]
56 pub state: Option<State>,
57 #[serde(skip)]
58 #[doc(hidden)]
59 pub sig: u64,
60}
61
62impl Default for Cloth {
63 fn default() -> Self {
64 Self {
65 width: 3.0,
66 height: 2.0,
67 segments: 24,
68 pin: ClothPin::TopEdge,
69 stiffness: 0.8,
70 damping: 0.02,
71 gravity_scale: 1.0,
72 wind: Vec3::new(2.5, 0.0, 1.2),
73 gust: 1.0,
74 drag: 0.8,
75 tear: 0.0,
76 collide_ground: true,
77 ground_y: 0.0,
78 collide_objects: true,
79 state: None,
80 sig: 0,
81 }
82 }
83}
84
85impl Component for Cloth {
86 fn fields(&mut self) -> Vec<Field<'_>> {
87 vec![
88 Field::f32("width", &mut self.width),
89 Field::f32("height", &mut self.height),
90 Field::i32("segments", &mut self.segments),
91 Field::choice("pin", &mut self.pin),
92 Field::f32("stiffness", &mut self.stiffness),
93 Field::f32("damping", &mut self.damping),
94 Field::f32("gravity_scale", &mut self.gravity_scale),
95 Field::vec3("wind", &mut self.wind),
96 Field::f32("gust", &mut self.gust),
97 Field::f32("drag", &mut self.drag),
98 Field::f32("tear", &mut self.tear),
99 Field::bool("collide_ground", &mut self.collide_ground),
100 Field::f32("ground_y", &mut self.ground_y),
101 Field::bool("collide_objects", &mut self.collide_objects),
102 ]
103 }
104}
105
106impl Cloth {
107 pub fn integrity(&self) -> f32 {
109 self.state.as_ref().map_or(1.0, |s| s.links.iter().filter(|l| l.alive).count() as f32 / s.links.len().max(1) as f32)
110 }
111
112 pub fn particles(&self) -> &[Vec3] {
114 self.state.as_ref().map_or(&[], |s| &s.pos)
115 }
116}
117
118#[derive(Clone)]
119struct Link {
120 a: usize,
121 b: usize,
122 rest: f32,
123 alive: bool,
124}
125
126#[doc(hidden)]
128#[derive(Clone)]
129pub struct State {
130 nx: usize,
131 ny: usize,
132 local: Vec<Vec3>,
134 pos: Vec<Vec3>,
135 prev: Vec<Vec3>,
136 pinned: Vec<bool>,
137 links: Vec<Link>,
138 tris: Vec<([usize; 3], [usize; 3])>,
140 version: u64,
141}
142
143enum Obstacle {
144 Sphere { center: Vec3, radius: f32 },
145 Box { to_local: Mat4, to_world: Mat4, half: Vec3 },
146}
147
148impl State {
149 fn new(c: &Cloth, model: Mat4) -> State {
150 let nx = c.segments.clamp(2, 80) as usize;
151 let ny = ((nx as f32 * c.height / c.width.max(0.01)).round() as usize).clamp(2, 80);
152 let idx = |i: usize, j: usize| j * (nx + 1) + i;
153 let mut local = Vec::new();
154 let mut pinned = Vec::new();
155 for j in 0..=ny {
156 for i in 0..=nx {
157 local.push(Vec3::new((i as f32 / nx as f32 - 0.5) * c.width, -(j as f32 / ny as f32) * c.height, 0.0));
158 pinned.push(match c.pin {
159 ClothPin::Free => false,
160 ClothPin::TopEdge => j == 0,
161 ClothPin::TopCorners => j == 0 && (i == 0 || i == nx),
162 ClothPin::LeftEdge => i == 0,
163 });
164 }
165 }
166 let pos: Vec<Vec3> = local.iter().map(|&p| model.transform_point3(p)).collect();
167 let mut links = Vec::new();
168 let link = |a: usize, b: usize, links: &mut Vec<Link>| {
169 links.push(Link { a, b, rest: pos[a].distance(pos[b]), alive: true });
170 links.len() - 1
171 };
172 let mut tris = Vec::new();
173 let (mut h, mut v) = (vec![0usize; (nx + 1) * (ny + 1)], vec![0usize; (nx + 1) * (ny + 1)]);
174 for j in 0..=ny {
175 for i in 0..=nx {
176 if i < nx {
177 h[idx(i, j)] = link(idx(i, j), idx(i + 1, j), &mut links);
178 }
179 if j < ny {
180 v[idx(i, j)] = link(idx(i, j), idx(i, j + 1), &mut links);
181 }
182 }
183 }
184 for j in 0..ny {
185 for i in 0..nx {
186 let diag = link(idx(i + 1, j), idx(i, j + 1), &mut links); link(idx(i, j), idx(i + 1, j + 1), &mut links); let (a, b, cc, d) = (idx(i, j), idx(i + 1, j), idx(i, j + 1), idx(i + 1, j + 1));
189 tris.push(([a, b, cc], [h[a], v[a], diag]));
190 tris.push(([b, d, cc], [v[b], h[cc], diag]));
191 }
192 }
193 for j in 0..=ny {
194 for i in 0..=nx {
195 if i + 2 <= nx {
196 link(idx(i, j), idx(i + 2, j), &mut links);
197 }
198 if j + 2 <= ny {
199 link(idx(i, j), idx(i, j + 2), &mut links);
200 }
201 }
202 }
203 State { nx, ny, prev: pos.clone(), pos, local, pinned, links, tris, version: 0 }
204 }
205
206 fn tri_alive(&self, t: usize) -> bool {
207 self.tris[t].1.iter().all(|&l| self.links[l].alive)
208 }
209
210 fn step(&mut self, c: &Cloth, obstacles: &[Obstacle], model: Mat4, time: f32, dt: f32) {
211 let gust_noise = Noise::new(NoiseKind::Perlin).with_seed(11).with_octaves(2);
212 let iterations = 3 + (c.stiffness.clamp(0.0, 1.0) * 9.0) as usize;
213 let gravity = Vec3::new(0.0, -9.81 * c.gravity_scale, 0.0);
214 let n = self.pos.len();
215
216 let mut force = vec![Vec3::ZERO; n];
218 for t in 0..self.tris.len() {
219 if !self.tri_alive(t) {
220 continue;
221 }
222 let [a, b, cc] = self.tris[t].0;
223 let (pa, pb, pc) = (self.pos[a], self.pos[b], self.pos[cc]);
224 let cross = (pb - pa).cross(pc - pa);
225 let area2 = cross.length();
226 if area2 < 1e-9 {
227 continue;
228 }
229 let normal = cross / area2;
230 let centroid = (pa + pb + pc) / 3.0;
231 let gust = 1.0 + c.gust * gust_noise.sample3(centroid.x * 0.35, centroid.z * 0.35 + centroid.y * 0.2, time * 0.9);
232 let velocity = ((pa - self.prev[a]) + (pb - self.prev[b]) + (pc - self.prev[cc])) / (3.0 * dt.max(1e-4));
233 let relative = c.wind * gust - velocity;
234 let push = normal * normal.dot(relative) * c.drag * 1.5;
235 for p in [a, b, cc] {
236 force[p] += push / 3.0;
237 }
238 }
239
240 for (i, force) in force.iter().enumerate().take(n) {
241 if self.pinned[i] {
242 self.pos[i] = model.transform_point3(self.local[i]);
243 self.prev[i] = self.pos[i];
244 continue;
245 }
246 let velocity = (self.pos[i] - self.prev[i]) * (1.0 - c.damping.clamp(0.0, 0.5));
247 self.prev[i] = self.pos[i];
248 self.pos[i] += velocity + (gravity + *force) * dt * dt;
249 }
250
251 for _ in 0..iterations {
252 for l in 0..self.links.len() {
253 let Link { a, b, rest, alive } = self.links[l];
254 if !alive {
255 continue;
256 }
257 let delta = self.pos[b] - self.pos[a];
258 let dist = delta.length();
259 if dist < 1e-7 {
260 continue;
261 }
262 if c.tear > 1.0 && dist > rest * c.tear {
263 self.links[l].alive = false;
264 continue;
265 }
266 let correction = delta * ((dist - rest) / dist);
267 match (self.pinned[a], self.pinned[b]) {
268 (false, false) => {
269 self.pos[a] += correction * 0.5;
270 self.pos[b] -= correction * 0.5;
271 }
272 (true, false) => self.pos[b] -= correction,
273 (false, true) => self.pos[a] += correction,
274 (true, true) => {}
275 }
276 }
277 self.collide(c, obstacles);
278 }
279 }
280
281 fn collide(&mut self, c: &Cloth, obstacles: &[Obstacle]) {
282 const THICKNESS: f32 = 0.02;
283 for i in 0..self.pos.len() {
284 if self.pinned[i] {
285 continue;
286 }
287 let mut p = self.pos[i];
288 if c.collide_ground && p.y < c.ground_y + THICKNESS {
289 p.y = c.ground_y + THICKNESS;
290 self.prev[i].x += (p.x - self.prev[i].x) * 0.25;
292 self.prev[i].z += (p.z - self.prev[i].z) * 0.25;
293 }
294 for o in obstacles {
295 match o {
296 Obstacle::Sphere { center, radius } => {
297 let d = p - *center;
298 let r = radius + THICKNESS;
299 let len = d.length();
300 if len < r && len > 1e-6 {
301 let normal = d / len;
302 p = *center + normal * r;
303 self.prev[i] = contact_friction(p, self.prev[i], normal);
304 }
305 }
306 Obstacle::Box { to_local, to_world, half } => {
307 let l = to_local.transform_point3(p);
308 let margin = Vec3::splat(THICKNESS) / to_world.to_scale_rotation_translation().0.max(Vec3::splat(1e-3));
309 let h = *half + margin;
310 if l.abs().cmple(h).all() {
311 let depth = h - l.abs();
312 let mut out = l;
313 if depth.x <= depth.y && depth.x <= depth.z {
314 out.x = h.x * l.x.signum();
315 } else if depth.y <= depth.z {
316 out.y = h.y * l.y.signum();
317 } else {
318 out.z = h.z * l.z.signum();
319 }
320 p = to_world.transform_point3(out);
321 let axis = (out - l).normalize_or_zero();
322 self.prev[i] = contact_friction(p, self.prev[i], to_world.transform_vector3(axis).normalize_or_zero());
323 }
324 }
325 }
326 }
327 self.pos[i] = p;
328 }
329 }
330
331 fn mesh(&self, to_local: Mat4) -> MeshData {
333 let n = self.pos.len();
334 let local: Vec<Vec3> = self.pos.iter().map(|&p| to_local.transform_point3(p)).collect();
335 let mut normals = vec![Vec3::ZERO; n];
336 let mut live = Vec::new();
337 for (t, (verts, _)) in self.tris.iter().enumerate() {
338 if !self.tri_alive(t) {
339 continue;
340 }
341 let [a, b, c] = *verts;
342 let face = (local[b] - local[a]).cross(local[c] - local[a]);
343 for v in [a, b, c] {
344 normals[v] += face;
345 }
346 live.push(*verts);
347 }
348 let mut m = MeshData::default();
349 for side in [1.0f32, -1.0] {
350 for i in 0..n {
351 let (x, y) = (i % (self.nx + 1), i / (self.nx + 1));
352 m.vertices.push(Vertex {
353 pos: local[i].into(),
354 normal: (normals[i].normalize_or_zero() * side).into(),
355 uv: [x as f32 / self.nx as f32, y as f32 / self.ny as f32],
356 });
357 }
358 }
359 for [a, b, c] in live {
360 m.indices.extend([a as u32, b as u32, c as u32]);
361 let o = n as u32;
362 m.indices.extend([c as u32 + o, b as u32 + o, a as u32 + o]);
363 }
364 m
365 }
366}
367
368fn contact_friction(pos: Vec3, prev: Vec3, normal: Vec3) -> Vec3 {
371 let v = pos - prev;
372 let tangential = v - normal * v.dot(normal);
373 pos - tangential * 0.3
374}
375
376fn gather_obstacles(world: &World, skip: Entity) -> Vec<Obstacle> {
377 let mut out = Vec::new();
378 for (e, col) in world.iter::<Collider>() {
379 if e == skip || col.is_trigger {
380 continue;
381 }
382 let Some(g) = world.get::<GlobalTransform>(e).map(|g| g.0) else { continue };
383 if !g.is_finite() || g.determinant() == 0.0 { continue; }
384 match col.shape {
385 ShapeKind::Sphere => {
386 let (scale, _, pos) = g.to_scale_rotation_translation();
387 out.push(Obstacle::Sphere { center: pos, radius: col.size.x.abs() * 0.5 * scale.abs().max_element() });
388 }
389 ShapeKind::Box => out.push(Obstacle::Box { to_local: g.inverse(), to_world: g, half: col.size.abs() * 0.5 }),
390 _ => {}
391 }
392 }
393 out
394}
395
396pub fn update(world: &mut World, time: f32, dt: f32, simulate: bool) {
401 if world.count::<Cloth>() == 0 { return; }
402 hierarchy::update_globals(world);
403 let dt = if dt.is_finite() { dt.clamp(0.0, 0.1) } else { 0.0 };
404 let time = if time.is_finite() { time } else { 0.0 };
405 for e in world.entities_with::<Cloth>() {
406 let model = world.get::<GlobalTransform>(e).map_or_else(|| hierarchy::global_matrix(world, e), |g| g.0);
407 if !model.is_finite() || model.determinant() == 0.0 { continue; }
408 let obstacles =
409 if simulate && world.get::<Cloth>(e).is_some_and(|c| c.collide_objects) { gather_obstacles(world, e) } else { Vec::new() };
410 let cloth = world.get_mut::<Cloth>(e).unwrap();
411 let sig = signature(&(cloth.width, cloth.height, cloth.segments, cloth.pin));
413 if cloth.state.is_none() || cloth.sig != sig {
414 cloth.state = Some(State::new(cloth, model));
415 cloth.sig = sig;
416 }
417 let mut state = cloth.state.take().unwrap();
418 if simulate && dt > 0.0 {
419 let substeps = 2;
420 for _ in 0..substeps {
421 state.step(cloth, &obstacles, model, time, dt / substeps as f32);
422 }
423 } else if !simulate {
424 state.pos = state.local.iter().map(|&p| model.transform_point3(p)).collect();
426 state.prev = state.pos.clone();
427 }
428 state.version = state.version.wrapping_add(1);
429 let mesh = state.mesh(model.inverse());
430 let version = state.version;
431 cloth.state = Some(state);
432 world.insert(e, DynamicMesh { data: Arc::new(mesh), version });
433 if !world.has::<MeshRenderer>(e) {
434 world.insert(e, MeshRenderer { color: [0.75, 0.2, 0.2, 1.0], roughness: 0.9, ..Default::default() });
435 }
436 }
437}
438
439#[cfg(test)]
440mod tests {
441 use super::*;
442 use crate::scene::Scene;
443
444 fn cloth_scene(cloth: Cloth, at: Vec3) -> (Scene, Entity) {
445 let mut scene = Scene::default();
446 let e = hierarchy::spawn_named(&mut scene.world, "Cloth");
447 scene.world.insert(e, Transform::at(at));
448 scene.world.insert(e, cloth);
449 hierarchy::update_globals(&mut scene.world);
450 (scene, e)
451 }
452
453 fn run(scene: &mut Scene, seconds: f32) {
454 for i in 0..(seconds * 60.0) as usize {
455 update(&mut scene.world, i as f32 / 60.0, 1.0 / 60.0, true);
456 }
457 }
458
459 #[test]
460 fn pinned_cloth_hangs_and_keeps_its_length() {
461 let (mut scene, e) = cloth_scene(Cloth { wind: Vec3::ZERO, ..Default::default() }, Vec3::new(0.0, 5.0, 0.0));
462 run(&mut scene, 4.0);
463 let c = scene.world.get::<Cloth>(e).unwrap();
464 let ys: Vec<f32> = c.particles().iter().map(|p| p.y).collect();
465 let (top, bottom) = (ys.iter().cloned().fold(f32::MIN, f32::max), ys.iter().cloned().fold(f32::MAX, f32::min));
466 assert!((top - 5.0).abs() < 0.01, "pinned row stays put: {top}");
467 assert!(bottom < 3.3 && bottom > 2.6, "hangs about its own height below the pin: {bottom}");
468 assert_eq!(c.integrity(), 1.0);
469 }
470
471 #[test]
472 fn free_cloth_falls_onto_the_ground_and_stays_above_it() {
473 let (mut scene, e) = cloth_scene(Cloth { pin: ClothPin::Free, wind: Vec3::ZERO, ..Default::default() }, Vec3::new(0.0, 3.0, 0.0));
474 run(&mut scene, 5.0);
475 let min = scene.world.get::<Cloth>(e).unwrap().particles().iter().map(|p| p.y).fold(f32::MAX, f32::min);
476 assert!(min >= 0.0 && min < 0.2, "cloth rests on the ground: {min}");
477 }
478
479 #[test]
480 fn cloth_drapes_over_a_sphere() {
481 let (mut scene, e) = cloth_scene(
482 Cloth { pin: ClothPin::Free, wind: Vec3::ZERO, collide_ground: false, ..Default::default() },
483 Vec3::new(0.0, 3.0, 0.0),
484 );
485 let ball = hierarchy::spawn_named(&mut scene.world, "Ball");
486 scene.world.insert(ball, Transform::at(Vec3::new(0.0, 1.0, 1.0)));
487 scene.world.insert(ball, Collider { shape: ShapeKind::Sphere, size: Vec3::splat(1.0), ..Default::default() });
488 hierarchy::update_globals(&mut scene.world);
489 scene.world.get_mut::<Transform>(e).unwrap().rotation = Vec3::new(-90.0, 0.0, 0.0);
491 hierarchy::update_globals(&mut scene.world);
492 run(&mut scene, 4.0);
493 let c = scene.world.get::<Cloth>(e).unwrap();
494 let center = Vec3::new(0.0, 1.0, 1.0);
495 assert!(c.particles().iter().all(|p| p.distance(center) > 0.49), "no particle inside the ball");
496 assert!(c.particles().iter().any(|p| p.y > 1.45), "some cloth is held up by the ball");
497 }
498
499 #[test]
500 fn strong_wind_tears_weak_cloth() {
501 let tough = Cloth { wind: Vec3::new(0.0, 0.0, 40.0), gust: 0.0, tear: 0.0, ..Default::default() };
502 let weak = Cloth { tear: 1.08, ..tough.clone() };
503 let ((mut a, ea), (mut b, eb)) = (cloth_scene(tough, Vec3::new(0.0, 5.0, 0.0)), cloth_scene(weak, Vec3::new(0.0, 5.0, 0.0)));
504 run(&mut a, 3.0);
505 run(&mut b, 3.0);
506 assert_eq!(a.world.get::<Cloth>(ea).unwrap().integrity(), 1.0);
507 assert!(b.world.get::<Cloth>(eb).unwrap().integrity() < 1.0, "torn cloth loses links");
508 }
509
510 #[test]
511 fn wind_pushes_hanging_cloth_downwind() {
512 let (mut scene, e) =
513 cloth_scene(Cloth { wind: Vec3::new(0.0, 0.0, 6.0), gust: 0.0, ..Default::default() }, Vec3::new(0.0, 5.0, 0.0));
514 run(&mut scene, 4.0);
515 let mean_z = scene.world.get::<Cloth>(e).unwrap().particles().iter().map(|p| p.z).sum::<f32>()
516 / scene.world.get::<Cloth>(e).unwrap().particles().len() as f32;
517 assert!(mean_z > 0.3, "cloth swings with the wind: {mean_z}");
518 }
519
520 #[test]
521 fn changing_wind_at_run_time_does_not_restart_the_simulation() {
522 let (mut scene, e) = cloth_scene(Cloth { pin: ClothPin::Free, wind: Vec3::ZERO, ..Default::default() }, Vec3::new(0.0, 6.0, 0.0));
523 for i in 0..60 {
524 scene.world.get_mut::<Cloth>(e).unwrap().wind = Vec3::new(i as f32 * 0.01, 0.0, 0.0); update(&mut scene.world, i as f32 / 60.0, 1.0 / 60.0, true);
526 }
527 let top = scene.world.get::<Cloth>(e).unwrap().particles().iter().map(|p| p.y).fold(f32::MIN, f32::max);
528 assert!(top < 5.0, "cloth keeps falling instead of resetting every frame: {top}");
529 }
530
531 #[test]
532 fn mesh_is_double_sided_and_skips_torn_triangles() {
533 let (mut scene, e) = cloth_scene(Cloth { segments: 4, ..Default::default() }, Vec3::ZERO);
534 update(&mut scene.world, 0.0, 0.0, false);
535 let full = scene.world.get::<DynamicMesh>(e).unwrap().data.indices.len();
536 assert_eq!(full, 4 * 3 * 2 * 3 * 2, "4x3 quads, 2 triangles each, two sides");
537 }
538}