phys_raycast/shapes/
sphere.rs

1// Copyright (C) 2020-2025 phys-raycast authors. All Rights Reserved.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use phys_geom::math::{Real, *};
16use phys_geom::shape::Sphere;
17
18use crate::{Raycast, RaycastHitResult};
19
20impl Raycast for Sphere {
21    fn raycast(
22        &self,
23        local_ray: phys_geom::Ray,
24        max_distance: Real,
25        discard_inside_hit: bool,
26    ) -> Option<RaycastHitResult> {
27        let radius = self.radius();
28        let center_to_origin = local_ray.origin - Point3::origin();
29
30        // Move ray origin closer to the sphere, to avoid numerical problems.
31        let offset = (-center_to_origin.dot(&local_ray.direction.into_inner()) - radius).max(0.0);
32        let center_to_translated_origin =
33            center_to_origin + local_ray.direction.into_inner() * offset;
34        let discr_half_b = center_to_translated_origin.dot(&local_ray.direction.into_inner());
35        let discr_c = center_to_translated_origin.norm_squared() - radius * radius;
36
37        // Ray is outside and pointing away
38        if discr_half_b > 0.0 && discr_c > 0.0 {
39            return None;
40        }
41
42        let discr = discr_half_b * discr_half_b - discr_c;
43        if discr < 0.0 {
44            None
45        } else {
46            let mut distance = -discr_half_b - discr.sqrt();
47
48            // If the origin is inside the sphere.
49            if distance < -offset {
50                if discard_inside_hit {
51                    return None;
52                }
53                Some(RaycastHitResult {
54                    distance: 0.0,
55                    normal: -local_ray.direction,
56                })
57            } else {
58                distance += offset;
59                if distance <= max_distance {
60                    let hit_point = center_to_origin + local_ray.direction.into_inner() * distance;
61                    Some(RaycastHitResult {
62                        distance,
63                        normal: UnitVec3::new_normalize(hit_point),
64                    })
65                } else {
66                    None
67                }
68            }
69        }
70    }
71}
72
73#[cfg(test)]
74mod raycast_sphere_tests {
75
76    use super::*;
77
78    #[test]
79    fn test_raycast() {
80        let sphere = Sphere::new(1.0);
81
82        // Direct hit from outside
83        {
84            let ray = phys_geom::Ray::new(
85                Point3::new(-2.0, 0.0, 0.0),
86                UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
87            );
88            assert_eq!(
89                sphere.raycast(ray, 5.0, false),
90                Some(RaycastHitResult {
91                    distance: 1.0,
92                    normal: UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0)),
93                })
94            );
95
96            // Max distance is too short
97            assert_eq!(
98                sphere.raycast(ray, 1.0, false),
99                Some(RaycastHitResult {
100                    distance: 1.0,
101                    normal: UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0)),
102                })
103            );
104
105            assert_eq!(sphere.raycast(ray, 0.5, false), None);
106        }
107
108        // Ray misses
109        {
110            let ray = phys_geom::Ray::new(
111                Point3::new(-2.0, 2.0, 0.0),
112                UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
113            );
114            assert_eq!(sphere.raycast(ray, 10.0, false), None);
115        }
116
117        // Ray starts inside sphere
118        {
119            let ray = phys_geom::Ray::new(
120                Point3::new(0.0, 0.0, 0.0),
121                UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
122            );
123            assert_eq!(
124                sphere.raycast(ray, 5.0, false),
125                Some(RaycastHitResult {
126                    distance: 0.0,
127                    normal: UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0)),
128                })
129            );
130        }
131
132        // Ray starts inside but discard inside hits
133        {
134            let ray = phys_geom::Ray::new(
135                Point3::new(0.0, 0.0, 0.0),
136                UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
137            );
138            assert_eq!(sphere.raycast(ray, 5.0, true), None);
139        }
140    }
141
142    #[test]
143    fn test_raycast_inner() {
144        let sphere = Sphere::new(1.0);
145
146        let ray = phys_geom::Ray::new(
147            Point3::new(0.5, 0.0, 0.0),
148            UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0)),
149        );
150        assert_eq!(
151            sphere.raycast(ray, 5.0, false),
152            Some(RaycastHitResult {
153                distance: 0.0,
154                normal: UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
155            })
156        );
157
158        assert_eq!(sphere.raycast(ray, 5.0, true), None);
159    }
160
161    #[test]
162    fn test_raycast_from_far_origin() {
163        let sphere = Sphere::new(1.0);
164
165        let ray = phys_geom::Ray::new(
166            Point3::new(-100001.0, 0.0, 0.0),
167            UnitVec3::new_normalize(Vec3::new(1.0, 0.0, 0.0)),
168        );
169        assert_eq!(
170            sphere.raycast(ray, 100002.0, false),
171            Some(RaycastHitResult {
172                distance: 100000.0,
173                normal: UnitVec3::new_normalize(Vec3::new(-1.0, 0.0, 0.0)),
174            })
175        );
176    }
177}