Files
rust-raytracer/src/primitive.rs
2023-11-16 17:14:24 -05:00

754 lines
23 KiB
Rust

use crate::ray::Ray;
use crate::{EPSILON, INFINITY};
use nalgebra::{distance, Matrix4, Point3, Vector3};
use roots::{find_roots_quadratic, Roots};
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::sync::Arc;
// MATERIAL -----------------------------------------------------------------
pub struct Material {
pub kd: Vector3<f32>,
pub ks: Vector3<f32>,
pub shininess: f32,
}
impl Material {
pub fn new(kd: Vector3<f32>, ks: Vector3<f32>, shininess: f32) -> Self {
Material { kd, ks, shininess }
}
pub fn magenta() -> Self {
let kd = Vector3::new(1.0, 0.0, 1.0);
let ks = Vector3::new(0.0, 1.0, 1.0);
let shininess = 0.5;
Material { kd, ks, shininess }
}
}
// INTERSECTION -----------------------------------------------------------------
pub struct Intersection {
// Information about an intersection
pub point: Point3<f32>,
pub normal: Vector3<f32>,
pub incidence: Vector3<f32>,
pub material: Arc<Material>,
pub distance: f32,
}
impl Intersection {
pub fn new(
point: Point3<f32>,
normal: Vector3<f32>,
incidence: Vector3<f32>,
material: Arc<Material>,
t: f32,
) -> Self {
Intersection {
point,
normal,
incidence,
material,
distance: t,
}
}
}
// BOUNDING BOX -----------------------------------------------------------------
struct BoundingBox {
bln: Point3<f32>,
trf: Point3<f32>,
}
impl BoundingBox {
fn new(bln: Point3<f32>, trf: Point3<f32>) -> Self {
BoundingBox { bln, trf }
}
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>> {
let t1 = (self.bln - ray.a).component_div(&ray.b);
let t2 = (self.trf - ray.a).component_div(&ray.b);
let tmin = t1.inf(&t2).min();
let tmax = t1.sup(&t2).max();
if tmax >= tmin {
Some(ray.at_t(tmin))
} else {
None
}
}
}
// PRIMITIVE TRAIT -----------------------------------------------------------------
pub trait Primitive {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection>;
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>>;
fn get_material(&self) -> Arc<Material>;
}
// SPHERE -----------------------------------------------------------------
pub struct Sphere {
position: Point3<f32>,
radius: f32,
bounding_box: BoundingBox,
material: Arc<Material>,
}
impl Sphere {
fn new(position: Point3<f32>, radius: f32, material: Arc<Material>) -> Self {
let radius_vec = Vector3::new(radius, radius, radius);
let bln = position - radius_vec;
let trf = position + radius_vec;
let bounding_box = BoundingBox::new(bln, trf);
Sphere {
position,
radius,
bounding_box,
material,
}
}
pub fn unit(material: Arc<Material>) -> Self {
Sphere::new(Point3::new(0.0, 0.0, 0.0), 1.0, material)
}
}
impl Primitive for Sphere {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
let pos = &ray.a;
let dir = &ray.b;
let l = pos - self.position;
let a = dir.dot(dir);
let b = 2.0 * l.dot(dir);
let c = l.dot(&l) - self.radius * self.radius;
let t = match find_roots_quadratic(a, b, c) {
Roots::No(_) => return None,
Roots::One([x1]) => x1,
Roots::Two([x1, x2]) => {
if x1 <= 0.0 && x2 <= 0.0 {
return None;
} else {
if x1.abs() < x2.abs() {
x1
} else {
x2
}
}
}
_ => return None,
};
let intersect = ray.at_t(t);
let normal = (intersect - self.position).normalize();
Some(Intersection {
point: intersect,
normal,
incidence: ray.b,
material: Arc::clone(&self.material),
distance: t,
})
}
fn get_material(&self) -> Arc<Material> {
Arc::clone(&self.material)
}
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>> {
return self.bounding_box.intersect_bounding_box(ray);
}
}
// CIRCLE -----------------------------------------------------------------
struct Circle {
position: Point3<f32>,
radius: f32,
normal: Vector3<f32>,
material: Arc<Material>,
bounding_box: BoundingBox,
}
impl Circle {
fn new(
position: Point3<f32>,
radius: f32,
normal: Vector3<f32>,
material: Arc<Material>,
) -> Self {
let radius_vec = Vector3::new(radius, radius, radius);
let bln = position - radius_vec;
let trf = position + radius_vec;
let bounding_box = BoundingBox::new(bln, trf);
Circle {
position,
radius,
normal: normal.normalize(),
material,
bounding_box,
}
}
pub fn unit(material: Arc<Material>) -> Self {
let position = Point3::new(0.0, 0.0, 0.0);
let normal = Vector3::new(0.0, 1.0, 0.0);
let radius = 1.0;
let material = material;
let bln = Point3::new(-radius, 0.0, -EPSILON);
let trf = Point3::new(radius, 0.0, EPSILON);
let bounding_box = BoundingBox { bln, trf };
Circle {
position,
normal,
radius,
material,
bounding_box,
}
}
}
impl Primitive for Circle {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
let constant = self.position.coords.dot(&self.normal);
let denominator = ray.b.dot(&self.normal);
let t = (constant - ray.a.coords.dot(&self.normal)) / denominator;
if t > INFINITY {
return None;
};
let intersect = ray.at_t(t);
let distance = distance(&intersect, &self.position);
match distance > self.radius {
true => return None,
false => {
return Some(Intersection {
point: intersect,
normal: self.normal,
incidence: ray.b,
material: Arc::clone(&self.material),
distance: t,
})
}
}
}
fn get_material(&self) -> Arc<Material> {
Arc::clone(&self.material)
}
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>> {
self.bounding_box.intersect_bounding_box(ray)
}
}
// CYLINDER -----------------------------------------------------------------
struct Cylinder {
radius: f32,
base: f32,
height: f32,
base_circle: Circle,
height_circle: Circle,
material: Arc<Material>,
}
impl Cylinder {}
impl Primitive for Cylinder {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
todo!()
}
fn get_material(&self) -> Arc<Material> {
todo!()
}
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>> {
todo!()
}
}
// CONE -----------------------------------------------------------------
pub struct Cone {
radius: f32,
base: f32,
height: f32,
circle: Circle,
material: Arc<Material>,
bounding_box: BoundingBox,
}
impl Cone {
pub fn new(radius: f32, height: f32, base: f32, material: Arc<Material>) -> Self {
let circle = Circle::new(
Point3::new(0.0, base, 0.0),
radius,
Vector3::new(0.0, 1.0, 0.0),
Arc::clone(&material),
);
let bln = Point3::new(-radius, base, -radius);
let trf = Point3::new(radius, base + height, radius);
Cone {
radius,
base,
height,
circle,
material,
bounding_box: BoundingBox { bln, trf },
}
}
pub fn unit(material: Arc<Material>) -> Self {
Cone::new(1.0, 2.0, -1.0, material)
}
pub fn get_normal(&self, intersect: Point3<f32>) -> Vector3<f32> {
let r = self.radius;
let h = self.height;
let (x, y, z) = (intersect.x, intersect.y, intersect.z);
let normal = Vector3::new(2.0 * x, 2.0 * r * r * (h - y), 2.0 * z).normalize();
normal
}
}
impl Primitive for Cone {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
let point = &ray.a;
let dir = &ray.b;
let (r, h) = (self.radius, self.height);
let (a1, a2, a3) = (point.x, point.y, point.z);
let (b1, b2, b3) = (dir.x, dir.y, dir.z);
let r2 = r * r;
let a = b1 * b1 + b3 * b3 - r2 * (b2 * b2);
let b = 2.0 * (a1 * b1 + a3 * b3 + r2 * (h * b2 - a2 * b2));
let c = a1 * a1 + a3 * a3 + r2 * (2.0 * h * a2 - h * h - a2 * a2);
let t = match find_roots_quadratic(a, b, c) {
Roots::No(_) => None,
Roots::One([x1]) => Some(x1),
Roots::Two([x1, x2]) => {
if x1 <= 0.0 && x2 <= 0.0 {
None
} else {
if x1.abs() < x2.abs() {
Some(x1)
} else {
Some(x2)
}
}
}
_ => None,
};
let cone_intersect = match t {
None => None,
Some(t) => {
let intersect = ray.at_t(t);
match intersect.y >= self.base && intersect.y <= self.height {
true => Some(Intersection {
point: intersect,
normal: self.get_normal(intersect),
material: Arc::clone(&self.material),
incidence: ray.b,
distance: t,
}),
false => None,
}
}
};
let circle_intersect = self.circle.intersect_ray(ray);
match (cone_intersect, circle_intersect) {
(None, None) => None,
(Some(cone_intersect), None) => Some(cone_intersect),
(None, Some(circle_intersect)) => Some(circle_intersect),
(Some(cone_intersect), Some(circle_intersect)) => {
let circle_distance = distance(&ray.a, &circle_intersect.point);
let cone_distance = distance(&ray.a, &cone_intersect.point);
match cone_distance < circle_distance {
true => Some(cone_intersect),
false => Some(circle_intersect),
}
}
}
}
fn get_material(&self) -> Arc<Material> {
Arc::clone(&self.material)
}
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>> {
self.bounding_box.intersect_bounding_box(ray)
}
}
// RECTANGLE -----------------------------------------------------------------
struct Rectangle {
position: Point3<f32>,
normal: Vector3<f32>,
width_direction: Vector3<f32>,
material: Arc<Material>,
width: f32,
height: f32,
bounding_box: BoundingBox,
}
impl Rectangle {
fn new(
position: Point3<f32>,
normal: Vector3<f32>,
width_direction: Vector3<f32>,
width: f32,
height: f32,
material: Arc<Material>,
) -> Self {
let normal = normal.normalize();
let width_direction = width_direction.normalize();
let height_direction = width_direction.cross(&normal);
let bln = position - width / 2.0 * width_direction - height / 2.0 * height_direction;
let trf = position + width / 2.0 * width_direction + height / 2.0 * height_direction;
Rectangle {
position,
normal: normal.normalize(),
width_direction: width_direction.normalize(),
width,
height,
material,
bounding_box: BoundingBox { bln, trf },
}
}
pub fn unit(material: Arc<Material>) -> Self {
Rectangle::new(
Point3::new(0.0, 0.0, 0.0),
Vector3::new(0.0, 1.0, 0.0),
Vector3::new(1.0, 0.0, 0.0),
2.0,
2.0,
material,
)
}
}
impl Primitive for Rectangle {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
let constant = self.position.coords.dot(&self.normal);
let denominator = ray.b.dot(&self.normal);
let t = (constant - ray.a.coords.dot(&self.normal)) / denominator;
if t > INFINITY {
return None;
}
let intersect = ray.at_t(t);
let height_direction = self.width_direction.cross(&self.normal);
let (w2, h2) = (self.width / 2.0, self.height / 2.0);
let r1 = w2 * self.width_direction;
let r2 = h2 * height_direction;
let pi = intersect - self.position;
let pi_dot_r1 = pi.dot(&r1);
let pi_dot_r2 = pi.dot(&r2);
if pi_dot_r1 >= -w2 && pi_dot_r1 <= w2 && pi_dot_r2 >= -h2 && pi_dot_r2 <= h2 {
return Some(Intersection {
point: intersect,
normal: self.normal,
incidence: ray.b,
material: Arc::clone(&self.material),
distance: t,
});
}
None
}
fn get_material(&self) -> Arc<Material> {
Arc::clone(&self.material)
}
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>> {
self.bounding_box.intersect_bounding_box(ray)
}
}
// BOX -----------------------------------------------------------------
pub struct Box {
width: f32,
height: f32,
depth: f32,
material: Arc<Material>,
bounding_box: BoundingBox,
}
impl Box {
fn new(width: f32, height: f32, depth: f32, material: Arc<Material>) -> Self {
let trf = Point3::new(width / 2.0, height / 2.0, depth / 2.0);
let bln = Point3::new(-width / 2.0, -height / 2.0, -depth / 2.0);
Box {
width,
height,
depth,
material,
bounding_box: BoundingBox { bln, trf },
}
}
pub fn unit(material: Arc<Material>) -> Self {
Box::new(2.0, 2.0, 2.0, material)
}
}
impl Primitive for Box {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
// Compute the minimum and maximum t-values for each axis of the bounding box
let t1 = (self.bounding_box.bln - ray.a).component_div(&ray.b);
let t2 = (self.bounding_box.trf - ray.a).component_div(&ray.b);
// Find the largest minimum t-value and the smallest maximum t-value among the axes
let tmin = t1.inf(&t2).max();
let tmax = t1.sup(&t2).min();
// Check if there's an intersection between tmin and tmax
if tmax >= tmin {
// The ray intersects the box, and tmin is the entry point, tmax is the exit point
let intersect = ray.at_t(tmin);
// Check if the intersection is outside the box
if intersect.x < -self.width / 2.0
|| intersect.x > self.width / 2.0
|| intersect.y < -self.height / 2.0
|| intersect.y > self.height / 2.0
|| intersect.z < -self.depth / 2.0
|| intersect.z > self.depth / 2.0
{
return None; // Intersection is outside the box
}
//Get normal of intersection point
let normal = if tmin == t1.x {
Vector3::new(-1.0, 0.0, 0.0)
} else if tmin == t1.y {
Vector3::new(0.0, -1.0, 0.0)
} else if tmin == t1.z {
Vector3::new(0.0, 0.0, -1.0)
} else if tmin == t2.x {
Vector3::new(1.0, 0.0, 0.0)
} else if tmin == t2.y {
Vector3::new(0.0, 1.0, 0.0)
} else {
Vector3::new(0.0, 0.0, 1.0)
};
Some(Intersection {
point: intersect,
normal,
incidence: ray.b,
material: Arc::clone(&self.material),
distance: tmin,
})
} else {
None // No intersection with the box
}
}
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>> {
self.bounding_box.intersect_bounding_box(ray)
}
fn get_material(&self) -> Arc<Material> {
Arc::clone(&self.material)
}
}
// TRIANGLE -----------------------------------------------------------------
struct Triangle {
u: Point3<f32>,
v: Point3<f32>,
w: Point3<f32>,
normal: Vector3<f32>,
material: Arc<Material>,
bounding_box: BoundingBox,
}
impl Triangle {
fn new(u: Point3<f32>, v: Point3<f32>, w: Point3<f32>, material: Arc<Material>) -> Self {
let uv = v - u;
let uw = w - u;
let normal = uv.cross(&uw).normalize();
let bln = u.inf(&v).inf(&w);
let trf = u.sup(&v).sup(&w);
let bounding_box = BoundingBox { bln, trf };
Triangle {
u,
v,
w,
normal,
material,
bounding_box,
}
}
pub fn unit(material: Arc<Material>) -> Self {
let u = Point3::new(-1.0, 0.0, -1.0);
let v = Point3::new(0.0, 0.0, 1.0);
let w = Point3::new(1.0, 0.0, -1.0);
let material = material;
Triangle::new(u, v, w, material)
}
}
impl Primitive for Triangle {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
let constant = self.u.coords.dot(&self.normal);
let denominator = ray.b.dot(&self.normal);
let t = (constant - ray.a.coords.dot(&self.normal)) / denominator;
if t > INFINITY {
return None;
}
let intersect = ray.at_t(t);
let uv = self.v - self.u;
let vw = self.w - self.v;
let wu = self.u - self.w;
let ui = intersect - self.u;
let vi = intersect - self.v;
let wi = intersect - self.w;
let u_cross = uv.cross(&ui);
let v_cross = vw.cross(&vi);
let w_cross = wu.cross(&wi);
let normal = self.normal;
if u_cross.dot(&normal) >= 0.0 && v_cross.dot(&normal) >= 0.0 && w_cross.dot(&normal) >= 0.0
{
Some(Intersection {
point: intersect,
normal,
incidence: ray.b,
material: Arc::clone(&self.material),
distance: t,
})
} else {
None
}
}
fn get_material(&self) -> Arc<Material> {
Arc::clone(&self.material)
}
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>> {
self.bounding_box.intersect_bounding_box(ray)
}
}
// MESH -----------------------------------------------------------------
struct Mesh {
triangles: Vec<Triangle>,
material: Arc<Material>,
bounding_box: BoundingBox,
}
impl Mesh {
fn new(triangles: Vec<Triangle>, material: Arc<Material>) -> Self {
// Calculate the bounding box for the entire mesh based on the bounding boxes of individual triangles
let bounding_box = Mesh::compute_bounding_box(&triangles);
Mesh {
triangles,
material,
bounding_box,
}
}
fn compute_bounding_box(triangles: &Vec<Triangle>) -> BoundingBox {
let mut bln = Point3::new(INFINITY, INFINITY, INFINITY);
let mut trf = -bln;
for triangle in triangles {
bln = bln.inf(&triangle.u);
bln = bln.inf(&triangle.v);
bln = bln.inf(&triangle.w);
trf = trf.sup(&triangle.u);
trf = trf.sup(&triangle.v);
trf = trf.sup(&triangle.w);
}
BoundingBox { bln, trf }
}
fn from_file(filename: &str, material: Arc<Material>) -> Self {
let mut triangles: Vec<Triangle> = Vec::new();
let mut vertices: Vec<Point3<f32>> = Vec::new();
let file = File::open(filename).expect("Failed to open file");
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(line) = line {
let mut parts = line.split_whitespace();
if let Some(keyword) = parts.next() {
match keyword {
"v" => {
// Parse vertex coordinates
if let (Some(x_str), Some(y_str), Some(z_str)) =
(parts.next(), parts.next(), parts.next())
{
let x: f32 = x_str.parse().expect("Failed to parse vertex X");
let y: f32 = y_str.parse().expect("Failed to parse vertex Y");
let z: f32 = z_str.parse().expect("Failed to parse vertex Z");
vertices.push(Point3::new(x, y, z));
}
}
"f" => {
// Parse face indices
if let (Some(v1_str), Some(v2_str), Some(v3_str)) =
(parts.next(), parts.next(), parts.next())
{
let v1: usize =
v1_str.parse().expect("Failed to parse vertex index");
let v2: usize =
v2_str.parse().expect("Failed to parse vertex index");
let v3: usize =
v3_str.parse().expect("Failed to parse vertex index");
// Indices in OBJ files are 1-based, so subtract 1 to convert to 0-based.
let a = vertices[v1 - 1];
let b = vertices[v2 - 1];
let c = vertices[v3 - 1];
triangles.push(Triangle::new(a, b, c, Arc::clone(&material)));
}
}
_ => {}
}
}
}
}
todo!();
}
}
impl Primitive for Mesh {
fn intersect_ray(&self, ray: &Ray) -> Option<Intersection> {
let mut closest_distance = INFINITY;
let mut closest_intersect: Option<Intersection> = None;
for triangle in &self.triangles {
match triangle.intersect_ray(ray) {
Some(intersect) => {
let distance = intersect.distance;
if distance < closest_distance {
closest_distance = distance;
closest_intersect = Some(intersect);
};
}
None => continue,
}
}
closest_intersect
}
fn get_material(&self) -> Arc<Material> {
Arc::clone(&self.material)
}
fn intersect_bounding_box(&self, ray: &Ray) -> Option<Point3<f32>> {
self.bounding_box.intersect_bounding_box(ray)
}
}