1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
//! Cloth-Fluid Coupling
//!
//! Two-way interaction between cloth particles and fluid particles:
//!
//! - **Fluid-to-Cloth**: Drag and buoyancy forces on cloth particles that
//! are near fluid particles.
//! - **Cloth-to-Fluid**: Cloth surface acts as a boundary that repels
//! nearby fluid particles.
//!
//! All computations use deterministic 128-bit fixed-point arithmetic.
use crate::math::{Fix128, Vec3Fix};
// ============================================================================
// Cloth-Fluid Coupling Configuration
// ============================================================================
/// Configuration for cloth-fluid interaction.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ClothFluidCoupling {
/// Drag coefficient applied to cloth particles submerged in fluid.
/// Higher values cause stronger velocity damping.
pub drag_coefficient: Fix128,
/// Buoyancy factor controlling upward force on submerged cloth.
pub buoyancy_factor: Fix128,
/// Surface tension factor at the fluid-cloth interface.
pub surface_tension: Fix128,
}
impl Default for ClothFluidCoupling {
fn default() -> Self {
Self {
drag_coefficient: Fix128::from_ratio(1, 2),
buoyancy_factor: Fix128::from_ratio(1, 10),
surface_tension: Fix128::from_ratio(1, 100),
}
}
}
// ============================================================================
// Fluid-to-Cloth Forces
// ============================================================================
/// Apply drag and buoyancy forces from fluid particles to cloth particles.
///
/// For each cloth particle, finds nearby fluid particles and applies:
/// - **Drag**: Force proportional to the relative velocity between cloth
/// and fluid, scaled by the number of nearby fluid particles.
/// - **Buoyancy**: Upward force proportional to the local fluid density
/// (approximated by nearby fluid particle count).
///
/// Cloth velocities are modified in place.
pub fn apply_fluid_forces_to_cloth(
coupling: &ClothFluidCoupling,
cloth_positions: &[Vec3Fix],
cloth_velocities: &mut [Vec3Fix],
fluid_positions: &[Vec3Fix],
fluid_velocities: &[Vec3Fix],
fluid_density: Fix128,
dt: Fix128,
) {
if dt.is_zero() || fluid_positions.is_empty() || cloth_positions.is_empty() {
return;
}
// Interaction radius: larger radius catches more fluid neighbors
let interaction_radius = Fix128::from_ratio(1, 2);
let radius_sq = interaction_radius * interaction_radius;
let buoyancy_dir = Vec3Fix::new(Fix128::ZERO, Fix128::ONE, Fix128::ZERO);
for ci in 0..cloth_positions.len() {
let cp = cloth_positions[ci];
let cv = cloth_velocities[ci];
let mut avg_fluid_vel = Vec3Fix::ZERO;
let mut neighbor_count = Fix128::ZERO;
// Find fluid particles within interaction radius
for fi in 0..fluid_positions.len() {
let diff = fluid_positions[fi] - cp;
let dist_sq = diff.dot(diff);
if dist_sq < radius_sq {
avg_fluid_vel = avg_fluid_vel + fluid_velocities[fi];
neighbor_count = neighbor_count + Fix128::ONE;
}
}
if neighbor_count.is_zero() {
continue;
}
// Average fluid velocity near this cloth particle
avg_fluid_vel = avg_fluid_vel / neighbor_count;
// Drag force: F_drag = -C_d * (v_cloth - v_fluid) * density_factor
let relative_vel = cv - avg_fluid_vel;
let density_factor = neighbor_count * fluid_density;
let drag_force = relative_vel * (coupling.drag_coefficient * density_factor);
// Buoyancy force: F_buoy = buoyancy_factor * neighbor_count * up
let buoyancy_force = buoyancy_dir * (coupling.buoyancy_factor * neighbor_count);
// Surface tension: pulls cloth toward local fluid center
let tension_force = avg_fluid_vel * coupling.surface_tension;
// Apply forces as velocity change (F * dt)
cloth_velocities[ci] = cv - drag_force * dt + buoyancy_force * dt + tension_force * dt;
}
}
// ============================================================================
// Cloth-to-Fluid Boundary
// ============================================================================
/// Apply cloth boundary forces to fluid particles.
///
/// Each cloth particle acts as a boundary that repels nearby fluid particles
/// along the cloth surface normal direction. This prevents fluid from
/// passing through the cloth.
///
/// `cloth_normals` should contain per-particle surface normals.
pub fn apply_cloth_boundary_to_fluid(
cloth_positions: &[Vec3Fix],
cloth_normals: &[Vec3Fix],
fluid_positions: &[Vec3Fix],
fluid_velocities: &mut [Vec3Fix],
repulsion_strength: Fix128,
) {
if cloth_positions.is_empty() || fluid_positions.is_empty() {
return;
}
let repulsion_radius = Fix128::from_ratio(1, 4);
let radius_sq = repulsion_radius * repulsion_radius;
for fi in 0..fluid_positions.len() {
let fp = fluid_positions[fi];
for ci in 0..cloth_positions.len() {
let cp = cloth_positions[ci];
let normal = if ci < cloth_normals.len() {
cloth_normals[ci]
} else {
Vec3Fix::UNIT_Y
};
let diff = fp - cp;
let dist_sq = diff.dot(diff);
if dist_sq >= radius_sq || dist_sq.is_zero() {
continue;
}
// Project onto cloth normal to determine which side
let proj = diff.dot(normal);
// Repulsion: push fluid along normal, strength inversely proportional to distance
let dist = dist_sq.sqrt();
let inv_dist = Fix128::ONE / dist;
let overlap = repulsion_radius - dist;
let force_mag = repulsion_strength * overlap * inv_dist;
if proj.is_negative() {
// Fluid is on the back side: push away along -normal
fluid_velocities[fi] = fluid_velocities[fi] - normal * force_mag;
} else {
// Fluid is on the front side: push away along +normal
fluid_velocities[fi] = fluid_velocities[fi] + normal * force_mag;
}
}
}
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn test_default_coupling() {
let coupling = ClothFluidCoupling::default();
assert!(!coupling.drag_coefficient.is_zero());
assert!(!coupling.buoyancy_factor.is_zero());
}
#[test]
fn test_no_fluid_no_effect() {
let coupling = ClothFluidCoupling::default();
let cloth_pos = vec![Vec3Fix::ZERO];
let mut cloth_vel = vec![Vec3Fix::from_int(1, 0, 0)];
let original_vel = cloth_vel[0];
apply_fluid_forces_to_cloth(
&coupling,
&cloth_pos,
&mut cloth_vel,
&[],
&[],
Fix128::ONE,
Fix128::from_ratio(1, 60),
);
assert_eq!(cloth_vel[0].x.hi, original_vel.x.hi);
}
#[test]
fn test_no_cloth_no_effect() {
let coupling = ClothFluidCoupling::default();
apply_fluid_forces_to_cloth(
&coupling,
&[],
&mut [],
&[Vec3Fix::ZERO],
&[Vec3Fix::ZERO],
Fix128::ONE,
Fix128::from_ratio(1, 60),
);
// Should not panic
}
#[test]
fn test_drag_slows_cloth() {
let coupling = ClothFluidCoupling {
drag_coefficient: Fix128::ONE,
buoyancy_factor: Fix128::ZERO,
surface_tension: Fix128::ZERO,
};
let cloth_pos = vec![Vec3Fix::ZERO];
// Cloth moving fast in +X, fluid stationary
let mut cloth_vel = vec![Vec3Fix::from_int(10, 0, 0)];
let fluid_pos = vec![Vec3Fix::ZERO]; // Right on top
let fluid_vel = vec![Vec3Fix::ZERO];
apply_fluid_forces_to_cloth(
&coupling,
&cloth_pos,
&mut cloth_vel,
&fluid_pos,
&fluid_vel,
Fix128::ONE,
Fix128::from_ratio(1, 60),
);
// Drag should reduce velocity in +X direction
assert!(cloth_vel[0].x < Fix128::from_int(10));
}
#[test]
fn test_buoyancy_adds_upward_force() {
let coupling = ClothFluidCoupling {
drag_coefficient: Fix128::ZERO,
buoyancy_factor: Fix128::ONE,
surface_tension: Fix128::ZERO,
};
let cloth_pos = vec![Vec3Fix::ZERO];
let mut cloth_vel = vec![Vec3Fix::ZERO];
let fluid_pos = vec![Vec3Fix::ZERO];
let fluid_vel = vec![Vec3Fix::ZERO];
apply_fluid_forces_to_cloth(
&coupling,
&cloth_pos,
&mut cloth_vel,
&fluid_pos,
&fluid_vel,
Fix128::ONE,
Fix128::from_ratio(1, 10),
);
// Buoyancy should add upward (positive Y) velocity
assert!(cloth_vel[0].y > Fix128::ZERO);
}
#[test]
fn test_boundary_repulsion() {
let cloth_pos = vec![Vec3Fix::ZERO];
let cloth_normals = vec![Vec3Fix::UNIT_Y];
// Fluid particle slightly above cloth
let fluid_pos = vec![Vec3Fix::new(
Fix128::ZERO,
Fix128::from_ratio(1, 10),
Fix128::ZERO,
)];
let mut fluid_vel = vec![Vec3Fix::new(
Fix128::ZERO,
Fix128::from_int(-5),
Fix128::ZERO,
)];
apply_cloth_boundary_to_fluid(
&cloth_pos,
&cloth_normals,
&fluid_pos,
&mut fluid_vel,
Fix128::from_int(10),
);
// Fluid should be pushed upward (repelled from cloth)
assert!(fluid_vel[0].y > Fix128::from_int(-5));
}
#[test]
fn test_boundary_no_effect_far_away() {
let cloth_pos = vec![Vec3Fix::ZERO];
let cloth_normals = vec![Vec3Fix::UNIT_Y];
// Fluid particle far from cloth
let fluid_pos = vec![Vec3Fix::from_int(100, 100, 100)];
let mut fluid_vel = vec![Vec3Fix::from_int(1, -1, 0)];
let original_vel = fluid_vel[0];
apply_cloth_boundary_to_fluid(
&cloth_pos,
&cloth_normals,
&fluid_pos,
&mut fluid_vel,
Fix128::ONE,
);
assert_eq!(fluid_vel[0].x.hi, original_vel.x.hi);
assert_eq!(fluid_vel[0].y.hi, original_vel.y.hi);
}
#[test]
fn test_zero_dt_no_effect() {
let coupling = ClothFluidCoupling::default();
let cloth_pos = vec![Vec3Fix::ZERO];
let mut cloth_vel = vec![Vec3Fix::from_int(5, 0, 0)];
let original = cloth_vel[0];
apply_fluid_forces_to_cloth(
&coupling,
&cloth_pos,
&mut cloth_vel,
&[Vec3Fix::ZERO],
&[Vec3Fix::ZERO],
Fix128::ONE,
Fix128::ZERO,
);
assert_eq!(cloth_vel[0].x.hi, original.x.hi);
}
}