232 lines
7.3 KiB
Rust
232 lines
7.3 KiB
Rust
use nalgebra::{Matrix3, Vector3};
|
|
use ordered_float::NotNan;
|
|
|
|
use crate::scene_data::{Cylinder, Sphere};
|
|
|
|
/// A normalized parametric Ray of the form (origin + direction * time)
|
|
///
|
|
/// That means at any time t: f64, the point represented by origin + direction *
|
|
/// time occurs on the ray.
|
|
#[derive(Debug)]
|
|
pub struct Ray {
|
|
origin: Vector3<f64>,
|
|
direction: Vector3<f64>,
|
|
}
|
|
|
|
impl Ray {
|
|
/// Construct a ray from endpoints
|
|
pub fn from_endpoints(start: Vector3<f64>, end: Vector3<f64>) -> Self {
|
|
let delta = (end - start).normalize();
|
|
Ray {
|
|
origin: start,
|
|
direction: delta,
|
|
}
|
|
}
|
|
|
|
/// Evaluate the ray at a certain point in time, yielding a point
|
|
pub fn eval(&self, time: f64) -> Vector3<f64> {
|
|
self.origin + self.direction * time
|
|
}
|
|
|
|
/// Given a sphere, returns the first time at which this ray intersects the
|
|
/// sphere.
|
|
///
|
|
/// If there is no intersection point, returns None.
|
|
pub fn intersects_sphere_at(&self, sphere: &Sphere) -> Option<f64> {
|
|
let a = self.direction.x.powi(2)
|
|
+ self.direction.y.powi(2)
|
|
+ self.direction.z.powi(2);
|
|
let b = 2.0
|
|
* (self.direction.x * (self.origin.x - sphere.center.x)
|
|
+ self.direction.y * (self.origin.y - sphere.center.y)
|
|
+ self.direction.z * (self.origin.z - sphere.center.z));
|
|
let c = (self.origin.x - sphere.center.x).powi(2)
|
|
+ (self.origin.y - sphere.center.y).powi(2)
|
|
+ (self.origin.z - sphere.center.z).powi(2)
|
|
- sphere.radius.powi(2);
|
|
let discriminant = b * b - 4.0 * a * c;
|
|
|
|
match discriminant {
|
|
// Discriminant < 0, means the equation has no solutions.
|
|
d if d < 0.0 => return None,
|
|
|
|
// Discriminant == 0
|
|
d if d == 0.0 => {
|
|
return Some(-b / (2.0 * a));
|
|
}
|
|
|
|
d if d > 0.0 => {
|
|
let solution_1 = (-b + discriminant.sqrt()) / (2.0 * a);
|
|
let solution_2 = (-b - discriminant.sqrt()) / (2.0 * a);
|
|
|
|
return Some(solution_1.min(solution_2));
|
|
}
|
|
|
|
// Probably hit some NaN or Infinity value due to faulty inputs...
|
|
_ => unreachable!("Invalid determinant value: {discriminant}"),
|
|
}
|
|
}
|
|
|
|
/// Given a cylinder, returns the first time at which this ray intersects the
|
|
/// cylinder.
|
|
///
|
|
/// If there is no intersection point, returns None.
|
|
pub fn intersects_cylinder_at(&self, cylinder: &Cylinder) -> Option<f64> {
|
|
// Determine rotation matrix for turning the cylinder upright along the
|
|
// Z-axis
|
|
let target_direction = Vector3::new(0.0, 0.0, 1.0);
|
|
let rotation_matrix =
|
|
compute_rotation_matrix(cylinder.direction, target_direction);
|
|
|
|
// Transform all parameters according to this rotation matrix
|
|
let rotated_cylinder_center = rotation_matrix * cylinder.center;
|
|
let rotated_ray_origin = rotation_matrix * self.origin;
|
|
let rotated_ray_direction = rotation_matrix * self.direction;
|
|
|
|
// Now that we know the cylinder is upright, we can start checking against
|
|
// the formula:
|
|
//
|
|
// (ox + t*rx - cx)^2 + (oy + t*ry - cy)^2 = r^2
|
|
//
|
|
// where o{xy} is the ray origin, r{xy} is the ray direction, and c{xy} is
|
|
// the cylinder center. The z will be taken care of after the fact. To
|
|
// solve, we must put it into the form At^2 + Bt + c = 0. The variables
|
|
// are:
|
|
//
|
|
// A: rx^2 + ry^2
|
|
// B: 2(rx(ox - cx) + ry(oy - cy))
|
|
// C: (cx - ox)^2 + (cy - oy)^2 - r^2
|
|
let (a, b, c) = {
|
|
let o = rotated_ray_origin;
|
|
let r = rotated_ray_direction;
|
|
let c = rotated_cylinder_center;
|
|
|
|
(
|
|
r.x.powi(2) + r.y.powi(2),
|
|
2.0 * (r.x * (o.x - c.x) + r.y * (o.y - c.y)),
|
|
(c.x - o.x).powi(2) + (c.y - o.y).powi(2) - cylinder.radius.powi(2),
|
|
)
|
|
};
|
|
|
|
let discriminant = b * b - 4.0 * a * c;
|
|
|
|
let mut solutions = match discriminant {
|
|
// Discriminant < 0, means the equation has no solutions.
|
|
d if d < 0.0 => vec![],
|
|
|
|
// Discriminant == 0
|
|
d if d == 0.0 => vec![-b / 2.0 * a],
|
|
|
|
// Discriminant > 0, 2 solutions available.
|
|
d if d > 0.0 => {
|
|
vec![
|
|
(-b + discriminant.sqrt()) / (2.0 * a),
|
|
(-b - discriminant.sqrt()) / (2.0 * a),
|
|
]
|
|
}
|
|
|
|
// Probably hit some NaN or Infinity value due to faulty inputs...
|
|
_ => unreachable!("Invalid determinant value: {discriminant}"),
|
|
};
|
|
|
|
// We also need to add solutions for the two ends of the cylinder, which
|
|
// uses a similar method except backwards: check intersection points
|
|
// with the correct z-plane and then see if the points are within the
|
|
// circle.
|
|
//
|
|
// Luckily, this means we only need to care about one dimension at first,
|
|
// and don't need to perform the quadratic equation method above.
|
|
//
|
|
// oz + t * rz = cz +- (len / 2)
|
|
// t = (oz + cz +- (len / 2)) / rz
|
|
let possible_z_intersections = {
|
|
let o = rotated_ray_origin;
|
|
let r = rotated_ray_direction;
|
|
let c = rotated_cylinder_center;
|
|
|
|
vec![
|
|
(o.z + c.z + cylinder.length / 2.0) / r.z,
|
|
(o.z + c.z - cylinder.length / 2.0) / r.z,
|
|
]
|
|
};
|
|
// Filter out all the solutions where the z does not lie in the circle
|
|
solutions.extend(possible_z_intersections.into_iter().filter(|t| {
|
|
let ray_point = self.origin + self.direction * (*t);
|
|
ray_point.x.powi(2) + ray_point.y.powi(2) <= cylinder.radius.powi(2)
|
|
}));
|
|
|
|
// Filter out solutions that don't have a valid Z position.
|
|
let solutions = solutions
|
|
.into_iter()
|
|
.filter(|t| {
|
|
let ray_point = self.origin + self.direction * (*t);
|
|
|
|
let rotated_ray_point = rotation_matrix * ray_point;
|
|
let z = rotated_ray_point.z - rotated_cylinder_center.z;
|
|
|
|
// Check to see if z is between -len/2 and len/2
|
|
z.abs() < cylinder.length / 2.0
|
|
})
|
|
.filter_map(|t| NotNan::new(t).ok());
|
|
|
|
// Return the minimum solution
|
|
solutions.min().map(|t| t.into_inner())
|
|
}
|
|
}
|
|
|
|
/// Calculate the rotation matrix between the 2 given vectors
|
|
/// Based on the method here: https://math.stackexchange.com/a/897677
|
|
fn compute_rotation_matrix(a: Vector3<f64>, b: Vector3<f64>) -> Matrix3<f64> {
|
|
let cos_t = a.dot(&b);
|
|
let sin_t = a.cross(&b).norm();
|
|
let G = Matrix3::new(cos_t, -sin_t, 0.0, sin_t, cos_t, 0.0, 0.0, 0.0, 1.0);
|
|
|
|
// Basis
|
|
let u = a;
|
|
let v = (b - a.dot(&b) * a).normalize();
|
|
let w = b.cross(&a);
|
|
let F = Matrix3::from_columns(&[u, v, w]).try_inverse().unwrap();
|
|
|
|
F.try_inverse().unwrap() * G * F
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use nalgebra::Vector3;
|
|
|
|
use crate::scene_data::Sphere;
|
|
|
|
use super::Ray;
|
|
|
|
#[test]
|
|
fn practice_problem_slide_154() {
|
|
let ray = Ray {
|
|
origin: Vector3::new(0.0, 0.0, 0.0),
|
|
direction: Vector3::new(0.0, 0.0, -1.0),
|
|
};
|
|
let sphere = Sphere {
|
|
center: Vector3::new(0.0, 0.0, -10.0),
|
|
radius: 4.0,
|
|
};
|
|
|
|
let point = ray.intersects_sphere_at(&sphere).map(|t| ray.eval(t));
|
|
|
|
// the intersection point in this case is (0, 0, -6)
|
|
assert_eq!(point, Some(Vector3::new(0.0, 0.0, -6.0)));
|
|
}
|
|
|
|
#[test]
|
|
fn practice_problem_slide_158() {
|
|
let ray = Ray {
|
|
origin: Vector3::new(0.0, 0.0, 0.0),
|
|
direction: Vector3::new(0.0, 0.5, -1.0),
|
|
};
|
|
let sphere = Sphere {
|
|
center: Vector3::new(0.0, 0.0, -10.0),
|
|
radius: 4.0,
|
|
};
|
|
|
|
// oops! In this case, the ray does not intersect the sphere.
|
|
assert_eq!(ray.intersects_sphere_at(&sphere), None);
|
|
}
|
|
}
|