Skip to main content

box2d_rust/geometry/
point_ray.rs

1// Point-in-shape tests and ray casts from geometry.c.
2// SPDX-FileCopyrightText: 2023 Erin Catto
3// SPDX-License-Identifier: MIT
4
5use super::is_valid_ray;
6use crate::collision::{Capsule, CastOutput, Circle, Polygon, RayCastInput, Segment};
7use crate::distance::{
8    make_proxy, shape_cast, shape_distance, DistanceInput, ShapeCastPairInput, SimplexCache,
9};
10use crate::math_functions::{
11    add, clamp_float, cross, distance_squared, dot, get_length_and_normalize, length_squared, lerp,
12    mul_add, mul_sub, mul_sv, neg, normalize, right_perp, sub, Vec2, TRANSFORM_IDENTITY,
13};
14
15/// Test a point for overlap with a circle in local space. (b2PointInCircle)
16pub fn point_in_circle(shape: &Circle, point: Vec2) -> bool {
17    let center = shape.center;
18    distance_squared(point, center) <= shape.radius * shape.radius
19}
20
21/// Test a point for overlap with a capsule in local space. (b2PointInCapsule)
22pub fn point_in_capsule(shape: &Capsule, point: Vec2) -> bool {
23    let rr = shape.radius * shape.radius;
24    let p1 = shape.center1;
25    let p2 = shape.center2;
26
27    let d = sub(p2, p1);
28    let dd = dot(d, d);
29    if dd == 0.0 {
30        // Capsule is really a circle
31        return distance_squared(point, p1) <= rr;
32    }
33
34    // Get closest point on capsule segment
35    // c = p1 + t * d
36    // dot(point - c, d) = 0
37    // dot(point - p1 - t * d, d) = 0
38    // t = dot(point - p1, d) / dot(d, d)
39    let mut t = dot(sub(point, p1), d) / dd;
40    t = clamp_float(t, 0.0, 1.0);
41    let c = mul_add(p1, t, d);
42
43    // Is query point within radius around closest point?
44    distance_squared(point, c) <= rr
45}
46
47/// Test a point for overlap with a convex polygon in local space.
48/// (b2PointInPolygon)
49pub fn point_in_polygon(shape: &Polygon, point: Vec2) -> bool {
50    let input = DistanceInput {
51        proxy_a: make_proxy(&shape.vertices[..shape.count as usize], 0.0),
52        proxy_b: make_proxy(&[point], 0.0),
53        transform: TRANSFORM_IDENTITY,
54        use_radii: false,
55    };
56
57    let mut cache = SimplexCache::default();
58    let output = shape_distance(&input, &mut cache, None);
59
60    output.distance <= shape.radius
61}
62
63/// Ray cast versus circle shape in local space. (b2RayCastCircle)
64// Precision Improvements for Ray / Sphere Intersection - Ray Tracing Gems 2019
65// http://www.codercorner.com/blog/?p=321
66pub fn ray_cast_circle(shape: &Circle, input: &RayCastInput) -> CastOutput {
67    debug_assert!(is_valid_ray(input));
68
69    let p = shape.center;
70
71    let mut output = CastOutput::default();
72
73    // Shift ray so circle center is the origin
74    let s = sub(input.origin, p);
75
76    let r = shape.radius;
77    let rr = r * r;
78
79    let mut length = 0.0;
80    let d = get_length_and_normalize(&mut length, input.translation);
81    if length == 0.0 {
82        // zero length ray
83
84        if length_squared(s) < rr {
85            // initial overlap
86            output.point = input.origin;
87            output.hit = true;
88        }
89
90        return output;
91    }
92
93    // Find closest point on ray to origin
94
95    // solve: dot(s + t * d, d) = 0
96    let t = -dot(s, d);
97
98    // c is the closest point on the line to the origin
99    let c = mul_add(s, t, d);
100
101    let cc = dot(c, c);
102
103    if cc > rr {
104        // closest point is outside the circle
105        return output;
106    }
107
108    // Pythagoras
109    let h = (rr - cc).sqrt();
110
111    let fraction = t - h;
112
113    if fraction < 0.0 || input.max_fraction * length < fraction {
114        // intersection is point outside the range of the ray segment
115
116        if length_squared(s) < rr {
117            // initial overlap
118            output.point = input.origin;
119            output.hit = true;
120        }
121
122        return output;
123    }
124
125    // hit point relative to center
126    let hit_point = mul_add(s, fraction, d);
127
128    output.fraction = fraction / length;
129    output.normal = normalize(hit_point);
130    output.point = mul_add(p, shape.radius, output.normal);
131    output.hit = true;
132
133    output
134}
135
136/// Ray cast versus capsule shape in local space. (b2RayCastCapsule)
137pub fn ray_cast_capsule(shape: &Capsule, input: &RayCastInput) -> CastOutput {
138    debug_assert!(is_valid_ray(input));
139
140    let mut output = CastOutput::default();
141
142    let v1 = shape.center1;
143    let v2 = shape.center2;
144
145    let e = sub(v2, v1);
146
147    let mut capsule_length = 0.0;
148    let a = get_length_and_normalize(&mut capsule_length, e);
149
150    if capsule_length < f32::EPSILON {
151        // Capsule is really a circle
152        let circle = Circle {
153            center: v1,
154            radius: shape.radius,
155        };
156        return ray_cast_circle(&circle, input);
157    }
158
159    let p1 = input.origin;
160    let d = input.translation;
161
162    // Ray from capsule start to ray start
163    let q = sub(p1, v1);
164    let qa = dot(q, a);
165
166    // Vector to ray start that is perpendicular to capsule axis
167    let qp = mul_add(q, -qa, a);
168
169    let radius = shape.radius;
170
171    // Does the ray start within the infinite length capsule?
172    if dot(qp, qp) < radius * radius {
173        if qa < 0.0 {
174            // start point behind capsule segment
175            let circle = Circle {
176                center: v1,
177                radius: shape.radius,
178            };
179            return ray_cast_circle(&circle, input);
180        }
181
182        if qa > capsule_length {
183            // start point ahead of capsule segment
184            let circle = Circle {
185                center: v2,
186                radius: shape.radius,
187            };
188            return ray_cast_circle(&circle, input);
189        }
190
191        // ray starts inside capsule -> no hit
192        output.point = input.origin;
193        output.hit = true;
194        return output;
195    }
196
197    // Perpendicular to capsule axis, pointing right
198    let mut n = Vec2 { x: a.y, y: -a.x };
199
200    let mut ray_length = 0.0;
201    let u = get_length_and_normalize(&mut ray_length, d);
202
203    // Intersect ray with infinite length capsule
204    // v1 + radius * n + s1 * a = p1 + s2 * u
205    // v1 - radius * n + s1 * a = p1 + s2 * u
206
207    // s1 * a - s2 * u = b
208    // b = q - radius * ap
209    // or
210    // b = q + radius * ap
211
212    // Cramer's rule [a -u]
213    let den = -a.x * u.y + u.x * a.y;
214    if -f32::EPSILON < den && den < f32::EPSILON {
215        // Ray is parallel to capsule and outside infinite length capsule
216        return output;
217    }
218
219    let b1 = mul_sub(q, radius, n);
220    let b2 = mul_add(q, radius, n);
221
222    let inv_den = 1.0 / den;
223
224    // Cramer's rule [a b1]
225    let s21 = (a.x * b1.y - b1.x * a.y) * inv_den;
226
227    // Cramer's rule [a b2]
228    let s22 = (a.x * b2.y - b2.x * a.y) * inv_den;
229
230    let s2;
231    let b;
232    if s21 < s22 {
233        s2 = s21;
234        b = b1;
235    } else {
236        s2 = s22;
237        b = b2;
238        n = neg(n);
239    }
240
241    if s2 < 0.0 || input.max_fraction * ray_length < s2 {
242        return output;
243    }
244
245    // Cramer's rule [b -u]
246    let s1 = (-b.x * u.y + u.x * b.y) * inv_den;
247
248    if s1 < 0.0 {
249        // ray passes behind capsule segment
250        let circle = Circle {
251            center: v1,
252            radius: shape.radius,
253        };
254        ray_cast_circle(&circle, input)
255    } else if capsule_length < s1 {
256        // ray passes ahead of capsule segment
257        let circle = Circle {
258            center: v2,
259            radius: shape.radius,
260        };
261        ray_cast_circle(&circle, input)
262    } else {
263        // ray hits capsule side
264        output.fraction = s2 / ray_length;
265        output.point = add(lerp(v1, v2, s1 / capsule_length), mul_sv(shape.radius, n));
266        output.normal = n;
267        output.hit = true;
268        output
269    }
270}
271
272/// Ray cast versus segment shape in local space. Optionally treat the segment
273/// as one-sided with hits from the left side being treated as a miss.
274/// (b2RayCastSegment)
275// Ray vs line segment
276pub fn ray_cast_segment(shape: &Segment, input: &RayCastInput, one_sided: bool) -> CastOutput {
277    if one_sided {
278        // Skip left-side collision
279        let offset = cross(
280            sub(input.origin, shape.point1),
281            sub(shape.point2, shape.point1),
282        );
283        if offset < 0.0 {
284            return CastOutput::default();
285        }
286    }
287
288    // Put the ray into the edge's frame of reference.
289    let p1 = input.origin;
290    let d = input.translation;
291
292    let v1 = shape.point1;
293    let v2 = shape.point2;
294    let e = sub(v2, v1);
295
296    let mut output = CastOutput::default();
297
298    let mut length = 0.0;
299    let e_unit = get_length_and_normalize(&mut length, e);
300    if length == 0.0 {
301        return output;
302    }
303
304    // Normal points to the right, looking from v1 towards v2
305    let mut normal = right_perp(e_unit);
306
307    // Intersect ray with infinite segment using normal
308    // Similar to intersecting a ray with an infinite plane
309    // p = p1 + t * d
310    // dot(normal, p - v1) = 0
311    // dot(normal, p1 - v1) + t * dot(normal, d) = 0
312    let numerator = dot(normal, sub(v1, p1));
313    let denominator = dot(normal, d);
314
315    if denominator == 0.0 {
316        // parallel
317        return output;
318    }
319
320    let t = numerator / denominator;
321    if t < 0.0 || input.max_fraction < t {
322        // out of ray range
323        return output;
324    }
325
326    // Intersection point on infinite segment
327    let p = mul_add(p1, t, d);
328
329    // Compute position of p along segment
330    // p = v1 + s * e
331    // s = dot(p - v1, e) / dot(e, e)
332
333    let s = dot(sub(p, v1), e_unit);
334    if s < 0.0 || length < s {
335        // out of segment range
336        return output;
337    }
338
339    if numerator > 0.0 {
340        normal = neg(normal);
341    }
342
343    output.fraction = t;
344    output.point = p;
345    output.normal = normal;
346    output.hit = true;
347
348    output
349}
350
351/// Ray cast versus polygon shape in local space. (b2RayCastPolygon)
352pub fn ray_cast_polygon(shape: &Polygon, input: &RayCastInput) -> CastOutput {
353    debug_assert!(is_valid_ray(input));
354
355    if shape.radius == 0.0 {
356        // Shift all math to first vertex since the polygon may be far
357        // from the origin.
358        let base = shape.vertices[0];
359
360        let p1 = sub(input.origin, base);
361        let d = input.translation;
362
363        let (mut lower, mut upper) = (0.0f32, input.max_fraction);
364
365        let mut index = -1i32;
366
367        let mut output = CastOutput::default();
368
369        for edge_index in 0..shape.count as usize {
370            // p = p1 + a * d
371            // dot(normal, p - v) = 0
372            // dot(normal, p1 - v) + a * dot(normal, d) = 0
373            let vertex = sub(shape.vertices[edge_index], base);
374            let numerator = dot(shape.normals[edge_index], sub(vertex, p1));
375            let denominator = dot(shape.normals[edge_index], d);
376
377            if denominator == 0.0 {
378                // Parallel and runs outside edge
379                if numerator < 0.0 {
380                    return output;
381                }
382            } else {
383                // Note: we want this predicate without division:
384                // lower < numerator / denominator, where denominator < 0
385                // Since denominator < 0, we have to flip the inequality:
386                // lower < numerator / denominator <==> denominator * lower > numerator.
387                if denominator < 0.0 && numerator < lower * denominator {
388                    // Increase lower.
389                    // The segment enters this half-space.
390                    lower = numerator / denominator;
391                    index = edge_index as i32;
392                } else if denominator > 0.0 && numerator < upper * denominator {
393                    // Decrease upper.
394                    // The segment exits this half-space.
395                    upper = numerator / denominator;
396                }
397            }
398
399            if upper < lower {
400                // Ray misses
401                return output;
402            }
403        }
404
405        debug_assert!(0.0 <= lower && lower <= input.max_fraction);
406
407        if index >= 0 {
408            output.fraction = lower;
409            output.normal = shape.normals[index as usize];
410            output.point = mul_add(input.origin, lower, d);
411            output.hit = true;
412        } else {
413            // initial overlap
414            output.point = input.origin;
415            output.hit = true;
416        }
417
418        return output;
419    }
420
421    let cast_input = ShapeCastPairInput {
422        proxy_a: make_proxy(&shape.vertices[..shape.count as usize], shape.radius),
423        proxy_b: make_proxy(&[input.origin], 0.0),
424        transform: TRANSFORM_IDENTITY,
425        translation_b: input.translation,
426        max_fraction: input.max_fraction,
427        can_encroach: false,
428    };
429    shape_cast(&cast_input)
430}