Skip to content

Commit bd127d0

Browse files
committed
Add sphere type and ray-sphere intersection test
1 parent 6c6edb2 commit bd127d0

File tree

2 files changed

+161
-8
lines changed

2 files changed

+161
-8
lines changed

core/src/geom/prim.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@
33
//! Includes vertices, polygons, planes, rays, and more.
44
55
use alloc::vec::Vec;
6-
use core::fmt::Debug;
6+
use core::fmt::{self, Debug, Formatter};
77

88
use crate::math::{
99
Affine, Lerp, Linear, Mat4, Parametric, Point, Point2, Point3, Vec2, Vec3,
10-
Vector, space::Real, vec2, vec3,
10+
Vector, pt3, space::Real, vec2, vec3,
1111
};
12-
1312
use crate::render::Model;
1413

1514
/// Vertex with a position and arbitrary other attributes.
@@ -42,6 +41,8 @@ pub type Plane3<B = ()> = Plane<Vector<[f32; 4], Real<3, B>>>;
4241
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
4342
pub struct Ray<T: Affine>(pub T, pub T::Diff);
4443

44+
pub type Ray3<B = ()> = Ray<Point3<B>>;
45+
4546
/// A curve composed of a chain of line segments.
4647
///
4748
/// The polyline is represented as a list of points, or vertices, with each
@@ -60,6 +61,9 @@ pub struct Polygon<T>(pub Vec<T>);
6061
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
6162
pub struct Edge<T>(pub T, pub T);
6263

64+
#[derive(Copy, Clone, PartialEq)]
65+
pub struct Sphere<B = ()>(pub Point3<B>, pub f32);
66+
6367
/// A surface normal in 3D.
6468
// TODO Use distinct type rather than alias
6569
pub type Normal3 = Vec3;
@@ -748,6 +752,22 @@ impl<B> Default for Plane3<B> {
748752
}
749753
}
750754

755+
impl<B: Debug + Default> Debug for Sphere<B> {
756+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
757+
f.debug_tuple("Sphere")
758+
.field(&self.0)
759+
.field(&self.1)
760+
.finish()
761+
}
762+
}
763+
764+
impl<B> Default for Sphere<B> {
765+
/// Returns a unit sphere, with the center at the origin and radius 1.
766+
fn default() -> Self {
767+
Self(pt3(0.0, 0.0, 0.0), 1.0)
768+
}
769+
}
770+
751771
#[cfg(test)]
752772
mod tests {
753773
use crate::assert_approx_eq;

geom/src/isect.rs

Lines changed: 138 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use core::fmt::Debug;
22

3+
#[cfg(feature = "std")] // TODO separate fp feature for geom
4+
use retrofire_core::geom::Sphere;
35
use retrofire_core::{
4-
geom::{Plane3, Ray},
6+
geom::{Plane3, Ray, Ray3},
57
math::{ApproxEq, Point3, vec3},
68
render::scene::BBox,
79
};
@@ -18,8 +20,10 @@ pub trait Intersect<T> {
1820
fn intersect(&self, other: &T) -> Self::Result;
1921
}
2022

21-
impl<B> Intersect<Plane3<B>> for Ray<Point3<B>> {
22-
type Result = Option<(f32, Point3<B>)>;
23+
type RayIntersect3<B> = Option<(f32, Point3<B>)>;
24+
25+
impl<B> Intersect<Plane3<B>> for Ray3<B> {
26+
type Result = RayIntersect3<B>;
2327

2428
/// Returns the unique intersection point of `self` and a plane,
2529
/// or `None` if they do not intersect.
@@ -52,8 +56,8 @@ impl<B> Intersect<Plane3<B>> for Ray<Point3<B>> {
5256
}
5357
}
5458

55-
impl<B: Debug + Default> Intersect<BBox<B>> for Ray<Point3<B>> {
56-
type Result = Option<(f32, Point3<B>)>; // Only closest for now
59+
impl<B: Debug + Default> Intersect<BBox<B>> for Ray3<B> {
60+
type Result = RayIntersect3<B>; // Only closest for now
5761

5862
/// Returns the nearest intersection point of `self` and a box,
5963
/// or `None` if they do not intersect.
@@ -120,6 +124,93 @@ impl<B: Debug + Default> Intersect<BBox<B>> for Ray<Point3<B>> {
120124
}
121125
}
122126

127+
#[cfg(feature = "std")]
128+
impl<B> Intersect<Sphere<B>> for Ray3<B> {
129+
type Result = RayIntersect3<B>; // Only closest for now
130+
131+
/// Returns the intersection point of `self` and a sphere closest to the
132+
/// origin of `self`, or `None` if they do not intersect.
133+
///
134+
/// # Examples
135+
/// ```
136+
/// ```
137+
fn intersect(&self, &Sphere(center, r): &Sphere<B>) -> Self::Result {
138+
let &Ray(orig, dir) = self;
139+
140+
// > r, no intersection
141+
// If |C - C'| = r, one -"-
142+
// < r, two -"-
143+
// _______
144+
// / \
145+
// / \
146+
// | C--r--|
147+
// \ | /
148+
// \___|___/
149+
// _|
150+
// O--------+-C'----> d
151+
//
152+
// Find point P = (x, y, z) given sphere (C, r) and ray (O, d)
153+
//
154+
// Sphere equation:
155+
// (x - c_x)² + (y - c_y)² + (z - c_z)² = r²
156+
// or in vector form
157+
// (P - C) · (P - C) = r²
158+
//
159+
// Ray equation:
160+
// P = O + t·d
161+
//
162+
// Intersection:
163+
//
164+
// Substitute ray equation to sphere equation:
165+
// (o + t·d - c) · (o + t·d - c) = r²
166+
//
167+
// Multiply out
168+
// o (o + td - c) + t·d (o + td - c) - c (o + td - c) = r²
169+
//
170+
// Distribute
171+
// o·o + o·td - o·c + o·td + td·td - td·c - c·o - c·td + c·c = r²
172+
//
173+
// Reorder
174+
// td·td + o·td + o·td - td·c + o·o - o·c - c·o + c·c - r² = 0
175+
//
176+
// Factor out t's
177+
// (d·d) t² + (o·d + o·d - c·d) t + o·o - 2o·c + c·c - r² = 0
178+
//
179+
// Solve quadratic equation:
180+
// (d·d) t² + 2(o - c)·d t + (o - c)² - r² = 0
181+
//
182+
// t = (-b ± √(b² - 4ac)) / 2a
183+
184+
let c_to_o = orig - center;
185+
let a = dir.len_sqr(); // >= 0
186+
let b = 2.0 * c_to_o.dot(&dir);
187+
let c = c_to_o.len_sqr() - r * r;
188+
189+
let discriminant = b * b - 4.0 * a * c;
190+
if discriminant < 0.0 {
191+
// the line of the ray does not hit the sphere
192+
return None;
193+
}
194+
195+
use retrofire_core::math::float::f32;
196+
let sqrt = f32::sqrt(discriminant);
197+
// sqrt >= 0.0, thus t0 <= t1 always
198+
let (t0, t1) = (-b - sqrt, -b + sqrt);
199+
let t = if t0 >= 0.0 {
200+
// ray hits both points
201+
t0
202+
} else if t1 >= 0.0 {
203+
// ray origin is inside sphere
204+
t1
205+
} else {
206+
// sphere is behind ray
207+
return None;
208+
};
209+
let t = t / (2.0 * a);
210+
Some((t, orig + t * dir))
211+
}
212+
}
213+
123214
#[cfg(test)]
124215
mod tests {
125216
use retrofire_core::math::{Linear, Vec3, pt3};
@@ -299,4 +390,46 @@ mod tests {
299390
assert_eq!(ray.intersect(&empty), None);
300391
}
301392
}
393+
394+
// TODO until sqrt has a fallback
395+
#[cfg(feature = "std")]
396+
mod ray_sphere {
397+
use super::*;
398+
399+
const SPHERE: Sphere = Sphere(pt3(0.0, 0.0, 1.0), 2.0);
400+
401+
#[test]
402+
fn ray_passes_through_sphere() {
403+
let ray: Ray3 = Ray(pt3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 2.0));
404+
assert_eq!(
405+
ray.intersect(&SPHERE),
406+
Some((1.0, pt3(0.0, 0.0, -1.0)))
407+
);
408+
}
409+
#[test]
410+
fn ray_tangent_to_sphere() {
411+
let ray: Ray3 = Ray(pt3(0.0, 2.0, -3.0), vec3(0.0, 0.0, 2.0));
412+
assert_eq!(ray.intersect(&SPHERE), Some((2.0, pt3(0.0, 2.0, 1.0))));
413+
}
414+
415+
#[test]
416+
fn ray_origin_inside_sphere() {
417+
let ray: Ray3 = Ray(pt3(0.0, 0.0, 0.0), vec3(0.0, 0.0, 2.0));
418+
assert_eq!(ray.intersect(&SPHERE), Some((1.5, pt3(0.0, 0.0, 3.0))));
419+
420+
let ray: Ray3 = Ray(pt3(0.0, 0.0, 2.0), vec3(0.0, 0.0, 2.0));
421+
assert_eq!(ray.intersect(&SPHERE), Some((0.5, pt3(0.0, 0.0, 3.0))));
422+
}
423+
424+
fn sphere_behind_ray() {
425+
let ray: Ray3 = Ray(pt3(0.0, 0.0, -3.0), vec3(0.0, 0.0, -1.0));
426+
assert_eq!(ray.intersect(&SPHERE), None);
427+
}
428+
429+
#[test]
430+
fn ray_misses_sphere() {
431+
let ray: Ray3 = Ray(pt3(0.0, 0.0, -3.0), vec3(0.0, 1.0, 1.0));
432+
assert_eq!(ray.intersect(&SPHERE), None);
433+
}
434+
}
302435
}

0 commit comments

Comments
 (0)