1use std::collections::HashSet;
10
11#[derive(Debug, Clone)]
15pub struct ClothParticle {
16 pub position: [f32; 3],
18 pub prev_position: [f32; 3],
20 pub mass: f32,
22 pub pinned: bool,
24}
25
26impl ClothParticle {
27 pub fn new(position: [f32; 3], mass: f32) -> Self {
29 Self {
30 position,
31 prev_position: position,
32 mass,
33 pinned: false,
34 }
35 }
36
37 pub fn pinned(mut self) -> Self {
39 self.pinned = true;
40 self
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum SpringKind {
49 Structural,
51 Shear,
53 Bending,
55}
56
57#[derive(Debug, Clone)]
61pub struct Spring {
62 pub a: usize,
64 pub b: usize,
66 pub rest_length: f32,
68 pub stiffness: f32,
70 pub kind: SpringKind,
72}
73
74pub struct ClothSim {
78 pub particles: Vec<ClothParticle>,
80 pub springs: Vec<Spring>,
82 pub gravity: [f32; 3],
84 pub damping: f32,
86}
87
88#[inline]
91fn dist3(a: [f32; 3], b: [f32; 3]) -> f32 {
92 let dx = b[0] - a[0];
93 let dy = b[1] - a[1];
94 let dz = b[2] - a[2];
95 (dx * dx + dy * dy + dz * dz).sqrt()
96}
97
98#[inline]
100fn edge_key(i: usize, j: usize) -> (usize, usize) {
101 if i < j {
102 (i, j)
103 } else {
104 (j, i)
105 }
106}
107
108impl ClothSim {
111 pub fn from_mesh(positions: &[[f32; 3]], indices: &[u32], stiffness: f32) -> Self {
117 let particles: Vec<ClothParticle> = positions
119 .iter()
120 .map(|&p| ClothParticle::new(p, 1.0))
121 .collect();
122
123 let n_tris = indices.len() / 3;
124
125 let mut edge_set: HashSet<(usize, usize)> = HashSet::new();
127 let mut springs: Vec<Spring> = Vec::new();
128
129 macro_rules! add_spring {
131 ($a:expr, $b:expr, $kind:expr) => {{
132 let key = edge_key($a, $b);
133 if edge_set.insert(key) {
134 let rest_length = dist3(positions[$a], positions[$b]);
135 if rest_length > 1e-10 {
136 springs.push(Spring {
137 a: $a,
138 b: $b,
139 rest_length,
140 stiffness,
141 kind: $kind,
142 });
143 }
144 }
145 }};
146 }
147
148 for tri in 0..n_tris {
150 let i0 = indices[tri * 3] as usize;
151 let i1 = indices[tri * 3 + 1] as usize;
152 let i2 = indices[tri * 3 + 2] as usize;
153 add_spring!(i0, i1, SpringKind::Structural);
154 add_spring!(i1, i2, SpringKind::Structural);
155 add_spring!(i2, i0, SpringKind::Structural);
156 }
157
158 use std::collections::HashMap;
161 let mut edge_tris: HashMap<(usize, usize), Vec<[usize; 3]>> = HashMap::new();
162 for tri in 0..n_tris {
163 let i0 = indices[tri * 3] as usize;
164 let i1 = indices[tri * 3 + 1] as usize;
165 let i2 = indices[tri * 3 + 2] as usize;
166 let face = [i0, i1, i2];
167 for &(ea, eb) in &[(i0, i1), (i1, i2), (i2, i0)] {
168 edge_tris.entry(edge_key(ea, eb)).or_default().push(face);
169 }
170 }
171
172 for tris_for_edge in edge_tris.values() {
173 if tris_for_edge.len() == 2 {
174 let t0 = tris_for_edge[0];
175 let t1 = tris_for_edge[1];
176 let opp0 = t0.iter().find(|&&v| !t1.contains(&v)).copied();
178 let opp1 = t1.iter().find(|&&v| !t0.contains(&v)).copied();
179 if let (Some(a), Some(b)) = (opp0, opp1) {
180 add_spring!(a, b, SpringKind::Bending);
181 }
182 }
183 }
184
185 Self {
186 particles,
187 springs,
188 gravity: [0.0, -9.81, 0.0],
189 damping: 0.99,
190 }
191 }
192
193 pub fn pin_by_y(mut self, y_threshold: f32) -> Self {
195 for p in &mut self.particles {
196 if p.position[1] >= y_threshold {
197 p.pinned = true;
198 }
199 }
200 self
201 }
202
203 pub fn step(&mut self, dt: f32, sub_steps: usize) {
205 let sub_dt = dt / sub_steps.max(1) as f32;
206
207 for _ in 0..sub_steps {
208 for p in &mut self.particles {
210 if p.pinned {
211 continue;
212 }
213 let vx = p.position[0] - p.prev_position[0];
214 let vy = p.position[1] - p.prev_position[1];
215 let vz = p.position[2] - p.prev_position[2];
216
217 let new_x = p.position[0] + vx * self.damping + self.gravity[0] * sub_dt * sub_dt;
218 let new_y = p.position[1] + vy * self.damping + self.gravity[1] * sub_dt * sub_dt;
219 let new_z = p.position[2] + vz * self.damping + self.gravity[2] * sub_dt * sub_dt;
220
221 p.prev_position = p.position;
222 p.position = [new_x, new_y, new_z];
223 }
224
225 self.satisfy_springs();
227 self.apply_floor_constraint();
228 }
229 }
230
231 pub fn positions(&self) -> Vec<[f32; 3]> {
233 self.particles.iter().map(|p| p.position).collect()
234 }
235
236 fn apply_floor_constraint(&mut self) {
238 for p in &mut self.particles {
239 if p.position[1] < 0.0 {
240 p.position[1] = 0.0;
241 }
242 }
243 }
244
245 fn satisfy_springs(&mut self) {
247 let springs: Vec<Spring> = self.springs.clone();
249
250 for s in &springs {
251 let pa = self.particles[s.a].position;
252 let pb = self.particles[s.b].position;
253
254 let dx = pb[0] - pa[0];
255 let dy = pb[1] - pa[1];
256 let dz = pb[2] - pa[2];
257 let dist = (dx * dx + dy * dy + dz * dz).sqrt();
258
259 if dist < 1e-10 {
260 continue;
261 }
262
263 let factor = (1.0 - s.rest_length / dist) * s.stiffness;
264 let cx = dx * factor;
265 let cy = dy * factor;
266 let cz = dz * factor;
267
268 if !self.particles[s.a].pinned {
269 self.particles[s.a].position[0] += cx * 0.5;
270 self.particles[s.a].position[1] += cy * 0.5;
271 self.particles[s.a].position[2] += cz * 0.5;
272 }
273 if !self.particles[s.b].pinned {
274 self.particles[s.b].position[0] -= cx * 0.5;
275 self.particles[s.b].position[1] -= cy * 0.5;
276 self.particles[s.b].position[2] -= cz * 0.5;
277 }
278 }
279 }
280}
281
282#[cfg(test)]
285mod tests {
286 use super::*;
287
288 fn quad_cloth() -> ClothSim {
289 let positions = vec![
291 [0.0f32, 1.0, 0.0],
292 [1.0, 1.0, 0.0],
293 [0.0, 0.0, 0.0],
294 [1.0, 0.0, 0.0],
295 ];
296 let indices = vec![0u32, 1, 2, 1, 3, 2];
297 ClothSim::from_mesh(&positions, &indices, 0.8)
298 }
299
300 #[test]
301 fn cloth_from_mesh_particle_count() {
302 assert_eq!(quad_cloth().particles.len(), 4);
303 }
304
305 #[test]
306 fn cloth_from_mesh_has_springs() {
307 assert!(!quad_cloth().springs.is_empty());
308 }
309
310 #[test]
311 fn cloth_step_moves_particles() {
312 let mut sim = quad_cloth();
313 let before: Vec<[f32; 3]> = sim.positions();
314 sim.step(0.016, 4);
315 let after: Vec<[f32; 3]> = sim.positions();
316 let moved = before.iter().zip(after.iter()).any(|(b, a)| b != a);
317 assert!(moved, "at least one particle should move under gravity");
318 }
319
320 #[test]
321 fn cloth_pinned_particles_dont_move() {
322 let positions = vec![
324 [0.0f32, 1.0, 0.0],
325 [1.0, 1.0, 0.0],
326 [0.0, 0.0, 0.0],
327 [1.0, 0.0, 0.0],
328 ];
329 let indices = vec![0u32, 1, 2, 1, 3, 2];
330 let mut sim = ClothSim::from_mesh(&positions, &indices, 0.8).pin_by_y(1.0);
331
332 let pinned_before: Vec<[f32; 3]> = sim
333 .particles
334 .iter()
335 .filter(|p| p.pinned)
336 .map(|p| p.position)
337 .collect();
338
339 sim.step(0.016, 8);
340
341 let pinned_after: Vec<[f32; 3]> = sim
342 .particles
343 .iter()
344 .filter(|p| p.pinned)
345 .map(|p| p.position)
346 .collect();
347
348 assert_eq!(
349 pinned_before, pinned_after,
350 "pinned particles must not move"
351 );
352 }
353
354 #[test]
355 fn cloth_positions_returns_vec() {
356 let sim = quad_cloth();
357 assert_eq!(sim.positions().len(), sim.particles.len());
358 }
359
360 #[test]
361 fn cloth_spring_rest_length_positive() {
362 let sim = quad_cloth();
363 for s in &sim.springs {
364 assert!(
365 s.rest_length > 0.0,
366 "spring ({},{}) has rest_length <= 0",
367 s.a,
368 s.b
369 );
370 }
371 }
372}