csci5607/assignment-1b/src/scene/data.rs

203 lines
5.7 KiB
Rust
Raw Normal View History

2023-02-06 03:52:42 +00:00
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
}
}