1use super::*;
23use motion_core::{Point, Rect};
24
25const BOUNDS_EXTENT: f64 = 1e7;
28
29const BOUNDS_MARGIN: f64 = 1e-3;
31
32#[derive(Clone, Copy, Debug)]
34pub struct CollisionResponse {
35 pub restitution: f32,
37 pub friction: f32,
39 pub kill_on_hit: bool,
41}
42
43impl Default for CollisionResponse {
44 fn default() -> Self {
45 Self {
46 restitution: 1.0,
47 friction: 0.0,
48 kill_on_hit: false,
49 }
50 }
51}
52
53impl CollisionResponse {
54 pub(super) fn validate(self) -> Result<(), ValidationError> {
55 if !self.restitution.is_finite()
56 || !self.friction.is_finite()
57 || !(0.0..=1.0).contains(&self.restitution)
58 || !(0.0..=1.0).contains(&self.friction)
59 {
60 return Err(invalid(
61 "collision",
62 "Restitution/friction must be finite in 0..=1",
63 ));
64 }
65 Ok(())
66 }
67}
68
69fn nearest(point: Point, start: Point, end: Point) -> (f64, [f64; 2]) {
70 let delta = [end.x - start.x, end.y - start.y];
71 let square = delta[0] * delta[0] + delta[1] * delta[1];
72 let progress = if square == 0.0 {
73 0.0
74 } else {
75 ((point.x - start.x) * delta[0] + (point.y - start.y) * delta[1]) / square
76 }
77 .clamp(0.0, 1.0);
78 let difference = [
79 point.x - start.x - delta[0] * progress,
80 point.y - start.y - delta[1] * progress,
81 ];
82 let distance = libm::hypot(difference[0], difference[1]);
83 let normal = if distance > 1e-12 {
84 difference.map(|value| value / distance)
85 } else {
86 let length = libm::hypot(delta[0], delta[1]).max(1e-12);
87 [-delta[1] / length, delta[0] / length]
88 };
89 (distance, normal)
90}
91
92fn inside_polygon(point: Point, vertices: &[Point]) -> bool {
93 let mut inside = false;
94 for index in 0..vertices.len() {
95 let first = vertices[index];
96 let second = vertices[(index + 1) % vertices.len()];
97 if (first.y > point.y) != (second.y > point.y)
98 && point.x < (second.x - first.x) * (point.y - first.y) / (second.y - first.y) + first.x
99 {
100 inside = !inside;
101 }
102 }
103 inside
104}
105
106fn swept(
107 particle: &mut Particle,
108 step: f32,
109 response: CollisionResponse,
110 bounds: Option<Rect>,
111 distance: impl Fn(Point) -> (f64, [f64; 2]),
112) -> Result<bool, ValidationError> {
113 swept_from(particle, step, response, bounds, distance, 0.0)
114}
115
116fn swept_from(
118 particle: &mut Particle,
119 step: f32,
120 response: CollisionResponse,
121 bounds: Option<Rect>,
122 distance: impl Fn(Point) -> (f64, [f64; 2]),
123 mut progress: f64,
124) -> Result<bool, ValidationError> {
125 particle.validate()?;
126 response.validate()?;
127 if !step.is_finite() || step <= 0.0 {
128 return Err(invalid(
129 "collision",
130 "Contact step must be positive and finite",
131 ));
132 }
133 let start = particle.position.map(f64::from);
134 let velocity = particle.velocity.map(f64::from);
135 let travel = libm::hypot(velocity[0], velocity[1]) * f64::from(step);
136 let radius = f64::from(particle.size);
138 if let Some(bounds) = bounds {
139 let end = [
143 start[0] + velocity[0] * f64::from(step),
144 start[1] + velocity[1] * f64::from(step),
145 ];
146 let margin = radius + travel / 32.0 + BOUNDS_MARGIN;
147 if start
148 .iter()
149 .chain(&end)
150 .all(|value| value.abs() <= BOUNDS_EXTENT)
151 && (start[0].min(end[0]) - margin > bounds.right
152 || start[0].max(end[0]) + margin < bounds.left
153 || start[1].min(end[1]) - margin > bounds.bottom
154 || start[1].max(end[1]) + margin < bounds.top)
155 {
156 return Ok(false);
157 }
158 }
159 for _ in 0..64 {
160 let point = Point::new(
161 start[0] + velocity[0] * f64::from(step) * progress,
162 start[1] + velocity[1] * f64::from(step) * progress,
163 );
164 let (signed, normal) = distance(point);
165 if !signed.is_finite() || normal.iter().any(|value| !value.is_finite()) {
166 return Err(invalid("collision", "Nonfinite contact geometry"));
167 }
168 if signed <= radius + 1e-6 {
169 let contact = [
170 point.x + normal[0] * (radius - signed + 1e-5),
171 point.y + normal[1] * (radius - signed + 1e-5),
172 ];
173 let incoming = velocity[0] * normal[0] + velocity[1] * normal[1];
174 let normal_speed = if incoming < 0.0 {
175 -incoming * f64::from(response.restitution)
176 } else {
177 incoming
178 };
179 let tangent = [
180 velocity[0] - incoming * normal[0],
181 velocity[1] - incoming * normal[1],
182 ];
183 particle.velocity = std::array::from_fn(|axis| {
184 (normal[axis] * normal_speed + tangent[axis] * (1.0 - f64::from(response.friction)))
185 as f32
186 });
187 particle.position = std::array::from_fn(|axis| {
188 (contact[axis] - f64::from(particle.velocity[axis]) * f64::from(step) * progress)
189 as f32
190 });
191 if response.kill_on_hit {
192 particle.lifetime = particle.age.max(f32::MIN_POSITIVE);
193 particle.position = contact.map(|value| value as f32);
194 }
195 particle.validate()?;
196 return Ok(true);
197 }
198 if progress >= 1.0 || travel == 0.0 {
199 return Ok(false);
200 }
201 progress = (progress + ((signed - radius) / travel).max(1e-7)).min(1.0);
202 }
203 Err(limit("Swept collision iteration allowance exhausted"))
204}
205
206#[derive(Clone, Copy, Debug)]
208pub struct CircleCollider {
209 pub center: Point,
211 pub radius: f64,
213 pub response: CollisionResponse,
215}
216
217impl CircleCollider {
218 fn contact(&self, particle: &mut Particle, step: f32) -> Result<bool, ValidationError> {
219 if !self.radius.is_finite()
220 || self.radius < 0.0
221 || !self.center.x.is_finite()
222 || !self.center.y.is_finite()
223 {
224 return Err(invalid("circle", "Invalid collider circle"));
225 }
226 swept(particle, step, self.response, None, |point| {
227 let delta = [point.x - self.center.x, point.y - self.center.y];
228 let length = libm::hypot(delta[0], delta[1]);
229 (
230 length - self.radius,
231 if length == 0.0 {
232 [1.0, 0.0]
233 } else {
234 delta.map(|value| value / length)
235 },
236 )
237 })
238 }
239}
240
241#[derive(Clone, Copy, Debug)]
243pub struct SegmentCollider {
244 pub start: Point,
246 pub end: Point,
248 pub response: CollisionResponse,
250}
251
252impl SegmentCollider {
253 fn contact(&self, particle: &mut Particle, step: f32) -> Result<bool, ValidationError> {
254 if [self.start.x, self.start.y, self.end.x, self.end.y]
255 .iter()
256 .any(|value| !value.is_finite())
257 || self.start == self.end
258 {
259 return Err(invalid("segment", "Require finite distinct endpoints"));
260 }
261 particle.validate()?;
262 self.response.validate()?;
263 if !step.is_finite() || step <= 0.0 {
264 return Err(invalid(
265 "collision",
266 "Contact step must be positive and finite",
267 ));
268 }
269 let origin = Point::new(
270 f64::from(particle.position[0]),
271 f64::from(particle.position[1]),
272 );
273 let motion = particle
274 .velocity
275 .map(|value| f64::from(value) * f64::from(step));
276 let radius = f64::from(particle.size);
277 let mut hit = if nearest(origin, self.start, self.end).0 <= radius + 1e-6 {
278 Some(0.0)
279 } else {
280 None
281 };
282 let delta = [self.end.x - self.start.x, self.end.y - self.start.y];
283 let length = libm::hypot(delta[0], delta[1]);
284 let axis = delta.map(|value| value / length);
285 let normal = [-axis[1], axis[0]];
286 let relative = [origin.x - self.start.x, origin.y - self.start.y];
287 let side = relative[0] * normal[0] + relative[1] * normal[1];
288 let crossing = motion[0] * normal[0] + motion[1] * normal[1];
289 let mut admit = |progress: f64| {
290 if (0.0..=1.0).contains(&progress) {
291 hit = Some(hit.map_or(progress, |prior: f64| prior.min(progress)));
292 }
293 };
294 if crossing != 0.0 {
295 for edge in [-radius, radius] {
296 let progress = (edge - side) / crossing;
297 let along = (relative[0] + motion[0] * progress) * axis[0]
298 + (relative[1] + motion[1] * progress) * axis[1];
299 if (0.0..=length).contains(&along) {
300 admit(progress);
301 }
302 }
303 }
304 let square = motion[0] * motion[0] + motion[1] * motion[1];
305 if square > 0.0 {
306 for end in [self.start, self.end] {
307 let offset = [origin.x - end.x, origin.y - end.y];
308 let projection = offset[0] * motion[0] + offset[1] * motion[1];
309 let constant = offset[0] * offset[0] + offset[1] * offset[1] - radius * radius;
310 let discriminant = projection * projection - square * constant;
311 if discriminant >= 0.0 {
312 admit((-projection - libm::sqrt(discriminant)) / square);
313 }
314 }
315 }
316 let Some(progress) = hit else {
317 return Ok(false);
318 };
319 swept_from(
320 particle,
321 step,
322 self.response,
323 None,
324 |point| nearest(point, self.start, self.end),
325 progress,
326 )
327 }
328}
329
330#[derive(Clone, Debug)]
332pub struct PolygonCollider {
333 vertices: Arc<[Point]>,
334 response: CollisionResponse,
335 bounds: Option<Rect>,
337}
338
339impl PolygonCollider {
340 pub fn new(
342 vertices: Arc<[Point]>,
343 response: CollisionResponse,
344 ) -> Result<Self, ValidationError> {
345 response.validate()?;
346 if !(3..=256).contains(&vertices.len())
347 || vertices
348 .iter()
349 .any(|point| !point.x.is_finite() || !point.y.is_finite())
350 {
351 return Err(invalid("polygon", "Require 3..=256 finite vertices"));
352 }
353 let cross = |first: Point, second: Point, third: Point| {
354 (second.x - first.x) * (third.y - first.y) - (second.y - first.y) * (third.x - first.x)
355 };
356 let area = (0..vertices.len())
357 .map(|index| {
358 vertices[index].x * vertices[(index + 1) % vertices.len()].y
359 - vertices[(index + 1) % vertices.len()].x * vertices[index].y
360 })
361 .sum::<f64>();
362 if !area.is_finite() || area.abs() < 1e-12 {
363 return Err(invalid("polygon", "Degenerate polygon"));
364 }
365 for first in 0..vertices.len() {
366 if vertices[first] == vertices[(first + 1) % vertices.len()] {
367 return Err(invalid("polygon", "Duplicate adjacent vertices"));
368 }
369 for second in first + 2..vertices.len() {
370 if first == 0 && second == vertices.len() - 1 {
371 continue;
372 }
373 let ends = [
374 vertices[first],
375 vertices[(first + 1) % vertices.len()],
376 vertices[second],
377 vertices[(second + 1) % vertices.len()],
378 ];
379 let overlap = ends[0].x.min(ends[1].x) <= ends[2].x.max(ends[3].x)
380 && ends[2].x.min(ends[3].x) <= ends[0].x.max(ends[1].x)
381 && ends[0].y.min(ends[1].y) <= ends[2].y.max(ends[3].y)
382 && ends[2].y.min(ends[3].y) <= ends[0].y.max(ends[1].y);
383 if overlap
384 && cross(ends[0], ends[1], ends[2]) * cross(ends[0], ends[1], ends[3]) <= 0.0
385 && cross(ends[2], ends[3], ends[0]) * cross(ends[2], ends[3], ends[1]) <= 0.0
386 {
387 return Err(invalid("polygon", "Polygon edges intersect"));
388 }
389 }
390 }
391 let bounds = vertices
392 .iter()
393 .all(|point| point.x.abs() <= BOUNDS_EXTENT && point.y.abs() <= BOUNDS_EXTENT)
394 .then(|| {
395 vertices.iter().skip(1).fold(
396 Rect::new(vertices[0].x, vertices[0].y, vertices[0].x, vertices[0].y),
397 |bounds, point| {
398 Rect::new(
399 bounds.left.min(point.x),
400 bounds.top.min(point.y),
401 bounds.right.max(point.x),
402 bounds.bottom.max(point.y),
403 )
404 },
405 )
406 });
407 Ok(Self {
408 vertices,
409 response,
410 bounds,
411 })
412 }
413
414 fn contact(&self, particle: &mut Particle, step: f32) -> Result<bool, ValidationError> {
415 swept(particle, step, self.response, self.bounds, |point| {
416 let (distance, normal) = (0..self.vertices.len())
417 .map(|index| {
418 nearest(
419 point,
420 self.vertices[index],
421 self.vertices[(index + 1) % self.vertices.len()],
422 )
423 })
424 .min_by(|first, second| first.0.total_cmp(&second.0))
425 .unwrap_or((0.0, [1.0, 0.0]));
426 if inside_polygon(point, &self.vertices) {
427 (-distance, normal.map(|value| -value))
428 } else {
429 (distance, normal)
430 }
431 })
432 }
433}
434
435macro_rules! collider {
436 ($name:ident, $work:expr) => {
437 impl ParticleModifier for $name {
438 fn apply(
439 &self,
440 particle: &mut Particle,
441 step: f32,
442 _: Time,
443 _: &mut Random,
444 ) -> Result<(), ValidationError> {
445 self.contact(particle, step).map(|_| ())
446 }
447 fn apply_event(
448 &self,
449 particle: &mut Particle,
450 step: f32,
451 _: Time,
452 _: &mut Random,
453 ) -> Result<bool, ValidationError> {
454 self.contact(particle, step)
455 }
456 fn work(&self) -> u64 {
457 $work(self)
458 }
459 }
460 };
461}
462collider!(CircleCollider, |_: &CircleCollider| 64);
463collider!(SegmentCollider, |_: &SegmentCollider| 64);
464collider!(
465 PolygonCollider,
466 |collider: &PolygonCollider| collider.vertices.len() as u64 * 128
467);
468
469#[derive(Clone, Debug)]
472pub struct SignedDistanceGrid {
473 bounds: Rect,
474 dimensions: [u32; 2],
475 values: Arc<[f32]>,
476 surface: Arc<crate::VectorSurface>,
477 boundary: Arc<[(Point, Point)]>,
478}
479
480impl SignedDistanceGrid {
481 #[track_caller]
484 pub fn cached<Key: Eq + std::hash::Hash + Send + Sync + 'static>(
485 context: &motion_core::SceneContext<'_>,
486 key: Key,
487 surface: Arc<crate::VectorSurface>,
488 bounds: Rect,
489 dimensions: [u32; 2],
490 limits: SimulationLimits,
491 ) -> Result<Arc<Self>, ValidationError> {
492 let identity = Arc::as_ptr(&surface) as usize;
493 let geometry = [
494 bounds.left.to_bits(),
495 bounds.top.to_bits(),
496 bounds.right.to_bits(),
497 bounds.bottom.to_bits(),
498 ];
499 context
500 .cache_with_cost(
501 (
502 key,
503 identity,
504 geometry,
505 dimensions,
506 limits.max_bytes,
507 limits.max_work,
508 ),
509 || {
510 let result =
511 Self::new(surface.clone(), bounds, dimensions, limits).map(Arc::new);
512 (surface, result)
513 },
514 |(_, result)| result.as_ref().map_or(0, |grid| grid.bytes() as usize),
515 )
516 .1
517 .clone()
518 }
519
520 pub fn new(
522 surface: Arc<crate::VectorSurface>,
523 bounds: Rect,
524 dimensions: [u32; 2],
525 limits: SimulationLimits,
526 ) -> Result<Self, ValidationError> {
527 motion_core::drawing::validate_rect(bounds, "particles.sdf", "bounds")?;
528 let [width, height] = dimensions;
529 let count = u64::from(width) * u64::from(height);
530 if width < 2
531 || height < 2
532 || width > 2048
533 || height > 2048
534 || bounds.width() <= 0.0
535 || bounds.height() <= 0.0
536 || count * 4 + surface.triangles().len() as u64 * 512 > limits.max_bytes
537 {
538 return Err(limit("SDF resolution/byte allowance exhausted"));
539 }
540 let contains = |point: Point| {
541 surface
542 .triangles()
543 .iter()
544 .any(|triangle| inside_polygon(point, &triangle.vertices))
545 };
546 let mut boundary = Vec::new();
547 let mut work = 0u64;
548 for triangle in surface.triangles() {
549 for index in 0..3 {
550 let start = triangle.vertices[index];
551 let end = triangle.vertices[(index + 1) % 3];
552 let delta = [end.x - start.x, end.y - start.y];
553 let length = libm::hypot(delta[0], delta[1]);
554 if length == 0.0 {
555 continue;
556 }
557 let margin = surface.tolerance().min(length * 0.001).max(1e-9);
558 let middle = Point::new((start.x + end.x) * 0.5, (start.y + end.y) * 0.5);
559 work += surface.triangles().len() as u64 * 6;
560 if work > limits.max_work {
561 return Err(limit("SDF boundary work exhausted"));
562 }
563 if contains(Point::new(
564 middle.x - delta[1] / length * margin,
565 middle.y + delta[0] / length * margin,
566 )) != contains(Point::new(
567 middle.x + delta[1] / length * margin,
568 middle.y - delta[0] / length * margin,
569 )) {
570 boundary.push((start, end));
571 }
572 }
573 }
574 if boundary.is_empty() {
575 return Err(invalid("sdf", "Surface has no resolved boundary"));
576 }
577 let mut values = Vec::new();
578 values
579 .try_reserve_exact(count as usize)
580 .map_err(|_| limit("SDF grid allocation failed"))?;
581 for row in 0..height {
582 for column in 0..width {
583 work += surface.triangles().len() as u64 * 3 + boundary.len() as u64;
584 if work > limits.max_work {
585 return Err(limit("SDF grid work exhausted"));
586 }
587 let point = Point::new(
588 bounds.left + f64::from(column) / f64::from(width - 1) * bounds.width(),
589 bounds.top + f64::from(row) / f64::from(height - 1) * bounds.height(),
590 );
591 let distance = boundary
592 .iter()
593 .map(|(start, end)| nearest(point, *start, *end).0)
594 .fold(f64::INFINITY, f64::min);
595 values.push((distance * if contains(point) { -1.0 } else { 1.0 }) as f32);
596 }
597 }
598 Ok(Self {
599 bounds,
600 dimensions,
601 values: values.into(),
602 surface,
603 boundary: boundary.into(),
604 })
605 }
606
607 pub fn bytes(&self) -> u64 {
609 self.values.len() as u64 * 4
610 + self.boundary.len() as u64 * 32
611 + self.surface.triangles().len() as u64
612 * std::mem::size_of::<crate::SurfaceTriangle>() as u64
613 }
614
615 pub fn sample(&self, point: Point) -> (f64, [f64; 2]) {
617 let cell = [
618 self.bounds.width() / f64::from(self.dimensions[0] - 1),
619 self.bounds.height() / f64::from(self.dimensions[1] - 1),
620 ];
621 let coordinate = [
622 (point.x - self.bounds.left) / cell[0],
623 (point.y - self.bounds.top) / cell[1],
624 ];
625 let outside = coordinate[0] < 0.0
626 || coordinate[1] < 0.0
627 || coordinate[0] > f64::from(self.dimensions[0] - 1)
628 || coordinate[1] > f64::from(self.dimensions[1] - 1);
629 let position = [
630 coordinate[0].clamp(0.0, f64::from(self.dimensions[0] - 1)),
631 coordinate[1].clamp(0.0, f64::from(self.dimensions[1] - 1)),
632 ];
633 let low = position.map(|value| libm::floor(value) as u32);
634 let high = [
635 (low[0] + 1).min(self.dimensions[0] - 1),
636 (low[1] + 1).min(self.dimensions[1] - 1),
637 ];
638 let value =
639 |column, row| f64::from(self.values[(row * self.dimensions[0] + column) as usize]);
640 let values = [
641 value(low[0], low[1]),
642 value(high[0], low[1]),
643 value(low[0], high[1]),
644 value(high[0], high[1]),
645 ];
646 let fraction = [
647 position[0] - f64::from(low[0]),
648 position[1] - f64::from(low[1]),
649 ];
650 let distance = (values[0] * (1.0 - fraction[0]) + values[1] * fraction[0])
651 * (1.0 - fraction[1])
652 + (values[2] * (1.0 - fraction[0]) + values[3] * fraction[0]) * fraction[1];
653 if outside || distance.abs() <= libm::hypot(cell[0], cell[1]) * 2.0 {
654 let (distance, normal) = self
655 .boundary
656 .iter()
657 .map(|(start, end)| nearest(point, *start, *end))
658 .min_by(|first, second| first.0.total_cmp(&second.0))
659 .unwrap_or((0.0, [1.0, 0.0]));
660 if self
661 .surface
662 .triangles()
663 .iter()
664 .any(|triangle| inside_polygon(point, &triangle.vertices))
665 {
666 return (-distance, normal.map(|value| -value));
667 }
668 return (distance, normal);
669 }
670 let gradient = [
671 ((values[1] - values[0]) * (1.0 - fraction[1]) + (values[3] - values[2]) * fraction[1])
672 / cell[0],
673 ((values[2] - values[0]) * (1.0 - fraction[0]) + (values[3] - values[1]) * fraction[0])
674 / cell[1],
675 ];
676 let length = libm::hypot(gradient[0], gradient[1]);
677 (
678 distance,
679 if length == 0.0 {
680 [1.0, 0.0]
681 } else {
682 gradient.map(|value| value / length)
683 },
684 )
685 }
686}
687
688#[derive(Clone, Debug)]
690pub struct SdfCollider {
691 pub grid: Arc<SignedDistanceGrid>,
693 pub response: CollisionResponse,
695}
696
697impl SdfCollider {
698 fn contact(&self, particle: &mut Particle, step: f32) -> Result<bool, ValidationError> {
699 let cell = (self.grid.bounds.width() / f64::from(self.grid.dimensions[0] - 1))
700 .min(self.grid.bounds.height() / f64::from(self.grid.dimensions[1] - 1));
701 if f64::from(libm::hypotf(particle.velocity[0], particle.velocity[1]) * step) >= cell {
702 return Err(invalid(
703 "sdf",
704 "Velocity times step must be smaller than a grid cell",
705 ));
706 }
707 swept(particle, step, self.response, None, |point| {
708 self.grid.sample(point)
709 })
710 }
711}
712collider!(
713 SdfCollider,
714 |collider: &SdfCollider| (collider.grid.boundary.len() as u64
715 + collider.grid.surface.triangles().len() as u64 * 3
716 + 8)
717 * 64
718);