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