203 lines
5.7 KiB
Rust
203 lines
5.7 KiB
Rust
|
use std::fmt::Debug;
|
||
|
|
||
|
use anyhow::Result;
|
||
|
use nalgebra::Vector3;
|
||
|
use ordered_float::NotNan;
|
||
|
|
||
|
use crate::image::Color;
|
||
|
use crate::ray::Ray;
|
||
|
|
||
|
pub trait ObjectKind: Debug + Send + Sync {
|
||
|
/// Determine where the ray intersects this object, returning the earliest
|
||
|
/// time this happens. Returns None if no intersection occurs.
|
||
|
///
|
||
|
/// Also known as Trace_Ray in the slides, except not the part where it calls
|
||
|
/// Shade_Ray.
|
||
|
fn intersects_ray_at(&self, ray: &Ray)
|
||
|
-> Result<Option<IntersectionContext>>;
|
||
|
}
|
||
|
|
||
|
/// An object in the scene
|
||
|
#[derive(Debug)]
|
||
|
pub struct Object {
|
||
|
pub kind: Box<dyn ObjectKind>,
|
||
|
|
||
|
/// Index into the scene's material color list
|
||
|
pub material: usize,
|
||
|
}
|
||
|
|
||
|
#[derive(Debug)]
|
||
|
pub struct Rect {
|
||
|
pub upper_left: Vector3<f64>,
|
||
|
pub upper_right: Vector3<f64>,
|
||
|
pub lower_left: Vector3<f64>,
|
||
|
pub lower_right: Vector3<f64>,
|
||
|
}
|
||
|
|
||
|
#[derive(Debug)]
|
||
|
pub struct Material {
|
||
|
pub diffuse_color: Vector3<f64>,
|
||
|
pub specular_color: Vector3<f64>,
|
||
|
|
||
|
pub k_a: f64,
|
||
|
pub k_d: f64,
|
||
|
pub k_s: f64,
|
||
|
pub exponent: f64,
|
||
|
}
|
||
|
|
||
|
#[derive(Debug)]
|
||
|
pub enum LightKind {
|
||
|
/// A point light source exists at a point and emits light in all directions
|
||
|
Point {
|
||
|
location: Vector3<f64>,
|
||
|
},
|
||
|
|
||
|
Directional {
|
||
|
direction: Vector3<f64>,
|
||
|
},
|
||
|
}
|
||
|
|
||
|
#[derive(Debug)]
|
||
|
pub struct Light {
|
||
|
pub kind: LightKind,
|
||
|
pub color: Vector3<f64>,
|
||
|
}
|
||
|
|
||
|
#[derive(Debug, Default)]
|
||
|
pub struct Scene {
|
||
|
pub eye_pos: Vector3<f64>,
|
||
|
pub view_dir: Vector3<f64>,
|
||
|
pub up_dir: Vector3<f64>,
|
||
|
|
||
|
/// Horizontal field of view (in degrees)
|
||
|
pub hfov: f64,
|
||
|
pub parallel_projection: bool,
|
||
|
|
||
|
pub image_width: usize,
|
||
|
pub image_height: usize,
|
||
|
|
||
|
/// Background color
|
||
|
pub bkg_color: Color,
|
||
|
|
||
|
pub materials: Vec<Material>,
|
||
|
pub lights: Vec<Light>,
|
||
|
pub objects: Vec<Object>,
|
||
|
}
|
||
|
|
||
|
#[derive(Derivative)]
|
||
|
#[derivative(Debug, PartialEq, PartialOrd, Ord)]
|
||
|
pub struct IntersectionContext {
|
||
|
/// The time of the intersection in the parametric ray
|
||
|
///
|
||
|
/// Unfortunately, IEEE floats in Rust don't have total ordering, because
|
||
|
/// NaNs violate ordering properties. The way to remedy this is to ensure we
|
||
|
/// don't have NaNs by wrapping it into this type, which then implements
|
||
|
/// total ordering.
|
||
|
pub time: NotNan<f64>,
|
||
|
|
||
|
/// The intersection point.
|
||
|
#[derivative(PartialEq = "ignore", Ord = "ignore")]
|
||
|
pub point: Vector3<f64>,
|
||
|
|
||
|
/// The normal vector protruding from the surface of the object at the
|
||
|
/// intersection point
|
||
|
#[derivative(PartialEq = "ignore", Ord = "ignore")]
|
||
|
pub normal: Vector3<f64>,
|
||
|
}
|
||
|
|
||
|
impl Eq for IntersectionContext {}
|
||
|
|
||
|
impl Scene {
|
||
|
/// Determine the color that should be used to fill this pixel.
|
||
|
///
|
||
|
/// - material_idx is the index into the materials list.
|
||
|
/// - intersection_context contains information on vectors where the
|
||
|
/// intersection occurred
|
||
|
///
|
||
|
/// Also known as Shade_Ray in the slides.
|
||
|
pub fn compute_pixel_color(
|
||
|
&self,
|
||
|
material_idx: usize,
|
||
|
intersection_context: IntersectionContext,
|
||
|
) -> Color {
|
||
|
// TODO: Does it make sense to make this function fallible from an API
|
||
|
// design standpoint?
|
||
|
let material = match self.materials.get(material_idx) {
|
||
|
Some(v) => v,
|
||
|
None => return self.bkg_color,
|
||
|
};
|
||
|
|
||
|
let ambient_component = material.k_a * material.diffuse_color;
|
||
|
|
||
|
let diffuse_and_specular: Vector3<f64> = self
|
||
|
.lights
|
||
|
.iter()
|
||
|
.map(|light| {
|
||
|
// The vector pointing in the direction of the light
|
||
|
let light_direction = match light.kind {
|
||
|
LightKind::Point { location } => {
|
||
|
location - intersection_context.point
|
||
|
}
|
||
|
LightKind::Directional { direction } => direction,
|
||
|
}
|
||
|
.normalize();
|
||
|
|
||
|
let normal = intersection_context.normal.normalize();
|
||
|
let viewer_direction = self.eye_pos - intersection_context.point;
|
||
|
let halfway_direction =
|
||
|
((light_direction + viewer_direction) / 2.0).normalize();
|
||
|
|
||
|
let diffuse_component = material.k_d
|
||
|
* material.diffuse_color
|
||
|
* normal.dot(&light_direction).max(0.0);
|
||
|
|
||
|
let specular_component = material.k_s
|
||
|
* material.specular_color
|
||
|
* normal
|
||
|
.dot(&halfway_direction)
|
||
|
.max(0.0)
|
||
|
.powf(material.exponent);
|
||
|
|
||
|
diffuse_component + specular_component
|
||
|
})
|
||
|
.sum();
|
||
|
|
||
|
ambient_component + diffuse_and_specular
|
||
|
}
|
||
|
|
||
|
/// Determine the boundaries of the viewing window in world coordinates
|
||
|
pub fn compute_viewing_window(&self, distance: f64) -> Rect {
|
||
|
// Compute viewing directions
|
||
|
let u = self.view_dir.cross(&self.up_dir).normalize();
|
||
|
let v = u.cross(&self.view_dir).normalize();
|
||
|
|
||
|
// Compute dimensions of viewing window based on field of view
|
||
|
let viewing_width = {
|
||
|
// Divide the angle in 2 since we are trying to use trig rules so we must
|
||
|
// get it from a right triangle
|
||
|
let half_hfov = self.hfov.to_radians() / 2.0;
|
||
|
|
||
|
// tan(hfov / 2) = w / 2d
|
||
|
let w_over_2d = half_hfov.tan();
|
||
|
|
||
|
// To find the viewing width we must multiply by 2d now
|
||
|
w_over_2d * 2.0 * distance
|
||
|
};
|
||
|
|
||
|
let aspect_ratio = self.image_width as f64 / self.image_height as f64;
|
||
|
let viewing_height = viewing_width / aspect_ratio;
|
||
|
|
||
|
// Compute viewing window corners
|
||
|
let n = self.view_dir.normalize();
|
||
|
#[rustfmt::skip] // Otherwise this line wraps over
|
||
|
let view_window = Rect {
|
||
|
upper_left: self.eye_pos + n * distance - u * (viewing_width / 2.0) + v * (viewing_height / 2.0),
|
||
|
upper_right: self.eye_pos + n * distance + u * (viewing_width / 2.0) + v * (viewing_height / 2.0),
|
||
|
lower_left: self.eye_pos + n * distance - u * (viewing_width / 2.0) - v * (viewing_height / 2.0),
|
||
|
lower_right: self.eye_pos + n * distance + u * (viewing_width / 2.0) - v * (viewing_height / 2.0),
|
||
|
};
|
||
|
|
||
|
view_window
|
||
|
}
|
||
|
}
|