commit a86dbb516c7eff7231a4ba321fedd59f02e0511b Author: max.nuding Date: Thu Jun 30 09:54:25 2022 +0200 Initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7594a5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +*.ppm +.idea +*.iml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..151d364 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,75 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rustracer" +version = "0.1.0" +dependencies = [ + "rand", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7f9d3d0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rustracer" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = "0.8.5" diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..07edf10 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,45 @@ +use crate::{Point3, Ray, Vec3}; + +pub struct Camera { + aspect_ratio: f64, + viewport_height: f64, + viewport_width: f64, + focal_length: f64, + origin: Point3, + horizontal: Vec3, + vertical: Vec3, + lower_left_corner: Vec3 +} + +impl Camera { + pub fn get_ray(&self, u: f64, v: f64) -> Ray { + Ray::new( + self.origin, + self.lower_left_corner + u*self.horizontal + v*self.vertical - self.origin) + } + pub fn new(aspect_ratio: f64) -> Self { + let viewport_height = 2.0; + let viewport_width = aspect_ratio * viewport_height; + let origin = Point3::default(); + let horizontal = Vec3::new(viewport_width, 0.0, 0.0); + let vertical = Vec3::new(0.0, viewport_height, 0.0); + let focal_length = 1.0; + Camera { + aspect_ratio, + viewport_height, + viewport_width, + focal_length, + origin, + horizontal, + vertical, + lower_left_corner: origin - horizontal/2.0 - vertical/2.0 - Vec3::new(0.0, 0.0, focal_length) + } + } +} + +impl Default for Camera { + fn default() -> Self { + let aspect_ratio = 16.0 / 9.0; + Camera::new(aspect_ratio) + } +} diff --git a/src/hittable.rs b/src/hittable.rs new file mode 100644 index 0000000..2c060bc --- /dev/null +++ b/src/hittable.rs @@ -0,0 +1,64 @@ +use crate::{Point3, Ray, Vec3}; +use crate::material::{Material}; + +#[derive(Debug, Clone, Copy)] +pub struct HitRecord<'material> { + pub point: Point3, + pub normal: Vec3, + pub t: f64, + pub front_face: bool, + pub material: &'material Material +} + +pub trait Hittable { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option; +} +pub struct Sphere { + pub center: Point3, + pub radius: f64, + pub material: Material +} +impl Sphere { + pub fn new(center: Point3, radius: f64, material: Material) -> Sphere { + Sphere { center, radius, material } + } +} +impl Default for Sphere { + fn default() -> Self { + Sphere { + center: Point3::default(), + radius: 0.0, + material: Material::default() + } + } +} + +impl Hittable for Sphere { + fn hit(&self, ray: &Ray, t_min: f64, t_max: f64) -> Option { + let oc = &ray.origin() - &self.center; + let a = ray.direction().length_squared(); + let half_b = oc.dot(&ray.direction()); + let c = oc.length_squared() - &self.radius * &self.radius; + let discriminant = half_b * half_b - a * c; + if discriminant < 0.0 { return None; } + + let sqrtd = f64::sqrt(discriminant); + let root = (-half_b - sqrtd) / a; + if root < t_min || t_max < root { + let root = (-half_b + sqrtd) / a; + if root < t_min || t_max < root { return None; } + } + + let point = ray.at(root); + let normal = (point - self.center) / self.radius; + let front_face = ray.direction().dot(&normal) < 0.0; + let normal = if front_face { normal } else { -normal }; + Some(HitRecord { + point, + normal, + t: root, + front_face, + material: &self.material + }) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..322f2a6 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,81 @@ +use crate::camera::Camera; +use crate::hittable::{Hittable, Sphere}; +use crate::ray::Ray; +use crate::vec3::{Color, Point3, Vec3}; +use rand::distributions::{Distribution, Uniform}; +use crate::material::{Dielectric, Lambertian, Material, Metal}; + +mod vec3; +mod ray; +mod hittable; +mod camera; +mod material; + +fn main() { + // Image + const ASPECT_RATIO: f64 = 16.0 / 9.0; + const IMAGE_WIDTH: i32 = 400; + const IMAGE_HEIGHT: i32 = (IMAGE_WIDTH as f64 / ASPECT_RATIO) as i32; + const SAMPLES_PER_PIXEL: i32 = 10; + const MAX_DEPTH: i32 = 50; + + // Camera + let cam = Camera::new(ASPECT_RATIO); + + // World + let mut world:Vec> = Vec::new(); + + let center = Box::new(Sphere { + center: Point3::new(0.0, 0.0, -1.0), + radius: 0.5, + material: Material::Lambertian(Lambertian::new(Color::new(0.7, 0.3, 0.3))) + }); + let center_glass = Box::new(Sphere { + center: Point3::new(0.0, 0.0, -1.0), + radius: 0.5, + material: Material::Dielectric(Dielectric::new(1.5)) + }); + world.push(center_glass); + let ground = Sphere { + center: Point3::new(0.0, -100.5, -1.0), + radius: 100.0, + material: Material::Lambertian(Lambertian::new(Color::new(0.8, 0.8, 0.0))) + }; + world.push(Box::new(ground)); + let left = Box::new(Sphere { + center: Point3::new(-1.0, 0.0, -1.0), + radius: 0.5, + material: Material::Metal(Metal::new(Color::new(0.8, 0.8, 0.8), 0.3)) + }); + let left_glass = Box::new(Sphere { + center: Point3::new(-1.0, 0.0, -1.0), + radius: 0.5, + material: Material::Dielectric(Dielectric::new(1.5)) + }); + world.push(left_glass); + let right = Box::new(Sphere { + center: Point3::new(1.0, 0.0, -1.0), + radius: 0.5, + material: Material::Metal(Metal::new(Color::new(0.8, 0.6, 0.2), 1.0)) + }); + world.push(right); + + println!("P3\n{} {}\n255", IMAGE_WIDTH, IMAGE_HEIGHT); + let between = Uniform::from(0.0..1.0); + let mut rng = rand::thread_rng(); + for j in (0..IMAGE_HEIGHT).rev() { + eprint!("\rScanlines remaining: {} ", j); + for i in 0..IMAGE_WIDTH { + let mut color = Color::default(); + for s in 0..SAMPLES_PER_PIXEL { + let random_number = between.sample(&mut rng); + let u = (i as f64 + random_number) / (IMAGE_WIDTH - 1) as f64; + let v = (j as f64 + random_number) / (IMAGE_HEIGHT - 1) as f64; + let ray = cam.get_ray(u, v); + color += ray.pixel_color(&world, MAX_DEPTH); + } + color.write_color(SAMPLES_PER_PIXEL); + } + } + eprintln!("\nDone"); +} diff --git a/src/material.rs b/src/material.rs new file mode 100644 index 0000000..2d6ccaa --- /dev/null +++ b/src/material.rs @@ -0,0 +1,124 @@ +use rand::Rng; +use crate::hittable::HitRecord; +use crate::{Color, Ray, Vec3}; + +pub trait Scatterable { + fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Color)>; +} + +#[derive(Debug, Clone)] +pub enum Material { + Lambertian(Lambertian), + Metal(Metal), + Dielectric(Dielectric) +} + +impl Default for Material { + fn default() -> Self { + Material::Lambertian(Lambertian::new(Color::default())) + } +} + +impl Scatterable for Material { + fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Color)> { + match self { + Material::Lambertian(l) => l.scatter(ray, hit_record), + Material::Metal(m) => m.scatter(ray, hit_record), + Material::Dielectric(d) => d.scatter(ray, hit_record) + } + } +} + +#[derive(Debug, Clone)] +pub struct Lambertian { + pub albedo: Color +} +impl Lambertian { + pub fn new(albedo: Color) -> Self { + Lambertian { albedo } + } +} + +impl Scatterable for Lambertian { + fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Color)> { + let mut direction = hit_record.normal + Vec3::random_unit_vector(); + if direction.near_zero() { + direction = hit_record.normal; + } + let scattered = Ray::new(hit_record.point, direction); + Some((Some(scattered), self.albedo)) + } +} + +#[derive(Debug, Clone)] +pub struct Metal { + pub albedo: Color, + pub fuzz: f64 +} + +impl Metal { + pub fn new(albedo: Color, fuzz: f64) -> Self { + Metal { + albedo, + fuzz: if fuzz < 1.0 { fuzz} else { 1.0 } + } + } +} + +impl Scatterable for Metal { + fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Color)> { + let reflected = ray.direction().unit_vector().reflected(&hit_record.normal); + let scattered = Ray::new( + hit_record.point, + reflected + self.fuzz * Vec3::random_in_unit_sphere()); + if scattered.direction().dot(&hit_record.normal) > 0.0 { + Some((Some(scattered), self.albedo)) + } else { + None + } + } +} + +#[derive(Debug, Clone)] +pub struct Dielectric { // Glass + pub index_of_refraction: f64 +} + +impl Dielectric { + pub fn new(index_of_refraction: f64) -> Self { + Dielectric { index_of_refraction } + } + fn reflectance(cosine: f64, ref_idx: f64) -> f64 { + let r0 = (1.0-ref_idx) / (1.0+ref_idx); + let r0 = r0*r0; + r0 + (1.0-r0)*((1.0-cosine).powi(5)) + } +} + +impl Scatterable for Dielectric { + fn scatter(&self, ray: &Ray, hit_record: &HitRecord) -> Option<(Option, Color)> { + let color = Color::new(1.0, 1.0, 1.0); + let refraction_ratio = if hit_record.front_face { + 1.0/self.index_of_refraction + } else { + self.index_of_refraction + }; + let unit_direction = ray.direction().unit_vector(); + let cos_theta = ((-unit_direction).dot(&hit_record.normal)).min(1.0); + let sin_theta = (1.0 - cos_theta*cos_theta).sqrt(); + let cannot_refract = refraction_ratio * sin_theta > 1.0; + let reflectance = Dielectric::reflectance(cos_theta, refraction_ratio); + let mut rng = rand::thread_rng(); + + if cannot_refract || reflectance > rng.gen::() { + let reflected = unit_direction.reflected(&hit_record.normal); + let reflected = Vec3::new(-reflected.x(), reflected.y(), -reflected.z()); + let scattered = Ray::new(hit_record.point, reflected); + Some((Some(scattered), color)) + } else { + let direction = unit_direction.refract_orig(&hit_record.normal, refraction_ratio); + let scattered = Ray::new(hit_record.point, direction); + Some((Some(scattered), color)) + } + } +} diff --git a/src/ray.rs b/src/ray.rs new file mode 100644 index 0000000..7746b6c --- /dev/null +++ b/src/ray.rs @@ -0,0 +1,66 @@ +use crate::{Color, Hittable, Vec3}; +use crate::hittable::HitRecord; +use crate::material::Scatterable; +use crate::vec3::Point3; + +pub struct Ray { + origin: Point3, + direction: Vec3 +} +impl Default for Ray { + fn default() -> Self { + Ray::new(Vec3::default(), Vec3::default()) + } +} + +impl Ray { + pub fn new(origin: Point3, direction: Point3) -> Ray { + Ray { origin, direction } + } + pub fn at(&self, t: f64) -> Point3 { + self.origin + self.direction * t + } + pub fn direction(&self) -> Vec3 { self.direction } + pub fn origin(&self) -> Point3 { self.origin } + pub fn pixel_color(&self, world: &Vec>, depth: i32) -> Color { + if depth <= 0 { + return Color::default(); + } + if let Some(rect) = self.hit_world(world, 0.001, f64::INFINITY) { + let scattered = rect.material.scatter(self, &rect); + return match scattered { + Some((scattered_ray, albedo)) => { + match scattered_ray { + Some(sr) => { + albedo * sr.pixel_color(world, depth-1) + }, + None => albedo + } + }, + None => { return Color::default() } + }; + + } + + //Hot nothing, display sky color + let unit_direction = self.direction().unit_vector(); + let t = 0.5 * (unit_direction.y() + 1.0); + (1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0) + } + fn hit_world<'material>( + &self, + world: &'material Vec>, + t_min: f64, + t_max: f64, + ) -> Option> { + let mut closest_so_far = t_max; + let mut hit_record = None; + for sphere in world { + if let Some(hit) = sphere.hit(self, t_min, closest_so_far) { + closest_so_far = hit.t; + hit_record = Some(hit); + } + } + hit_record + } +} diff --git a/src/vec3.rs b/src/vec3.rs new file mode 100644 index 0000000..e62f8d2 --- /dev/null +++ b/src/vec3.rs @@ -0,0 +1,253 @@ +use std::fmt::{Display, Formatter}; +use std::ops::{Add, AddAssign, Div, Mul, Neg, Sub}; +use rand::distributions::{Distribution, Uniform}; + +#[derive(Debug, Clone, Copy)] +pub struct Vec3 { x: f64, y: f64, z: f64 } + +pub type Point3 = Vec3; +pub type Color = Vec3; + +impl Vec3 { + pub fn new(x: f64, y: f64, z: f64) -> Vec3 { Vec3 {x, y, z} } + pub fn x(&self) -> f64 { self.x } + pub fn y(&self) -> f64 { self.y } + pub fn z(&self) -> f64 { self.z } + pub fn length_squared(&self) -> f64 { + self.x * self.x + self.y * self.y + self.z * self.z + } + pub fn length(&self) -> f64 { self.distance(&Vec3::default()) } + pub fn unit_vector(&self) -> Vec3 { + let length = self.length(); + Vec3::new(self.x / length, self.y / length, self.z / length) + } + pub fn dot(&self, v:&Vec3) -> f64 { + self.x * v.x + self.y * v.y + self.z * v.z + } + pub fn cross(&self, v:&Vec3) -> Vec3 { + Vec3::new( + self.y * v.z - self.z * v.y, + self.z * v.x - self.x * v.z, + self.x * v.y - self.y * v.x + ) + } + pub fn distance(&self, other: &Vec3) -> f64 { + let dx = self.x - other.x(); + let dy = self.y - other.y(); + let dz = self.z - other.z(); + (dx * dx + dy * dy + dz * dz).sqrt() + } + + pub fn random(min: f64, max:f64) -> Self { + let between = Uniform::from(min..max); + let mut rng = rand::thread_rng(); + Vec3::new( + between.sample(&mut rng), + between.sample(&mut rng), + between.sample(&mut rng)) + } + pub fn random_in_unit_sphere() -> Self { + loop { + let v = Vec3::random(-1.0, 1.0); + if v.length_squared() < 1.0 { + return v; + } + } + } + pub fn random_in_hemisphere(normal: &Vec3) -> Self { + let vec = Vec3::random_in_unit_sphere(); + if vec.dot(normal) > 0.0 { + vec + } else { + -vec + } + } + pub fn random_unit_vector() -> Self { Vec3::random_in_unit_sphere().unit_vector() } + pub fn near_zero(&self) -> bool { + const MAXIMUM_DISTANCE_FROM_ZERO:f64 = 1e-8; + self.x.abs() < MAXIMUM_DISTANCE_FROM_ZERO && + self.y.abs() < MAXIMUM_DISTANCE_FROM_ZERO && + self.z.abs() < MAXIMUM_DISTANCE_FROM_ZERO + } + pub fn reflected(&self, normal: &Vec3) -> Vec3 { + let dp = self.dot(normal); + let dp = dp * 2.0 * (*normal); + *self - dp + } + pub fn refract(&self, normal: &Vec3, etai_over_etat: f64) -> Vec3 { + let dot = (-(*self)).dot(normal); + let cos_theta = dot.min(1.0); + let out_perp = etai_over_etat * ((*self) + cos_theta * (*normal)); + let inner = 1.0 - out_perp.length_squared(); + let abs = inner.abs(); + let r = -(abs.sqrt()); + out_parallel = r * (*normal); + out_perp + out_parallel + } + pub fn refract_sort_of_works(&self, normal: &Vec3, etai_over_etat: f64) -> Vec3 { + let dot = (-(*self)).dot(normal); + let cos_theta = dot.min(1.0); + let out_perp = etai_over_etat * ((*self) + cos_theta * (*normal)); + let inner = 1.0 - out_perp.length_squared(); + let abs = inner.abs(); + let r = -(abs.sqrt()); + let out_parallel = r * (*normal); + // Why? + let mut res = out_perp + out_parallel; + res.x = -res.x; + //res.y = -res.y; + res.z = -res.z; + res + } +} + +#[test] +fn test_refract() { + let uv = Point3::new(1.0, 1.0, 0.0); + let n = Point3::new(-1.0, 0.0, 0.0); + let etai_over_etat = 1.0; + let expected = Point3::new(0.0, 1.0, 0.0); + let actual = uv.refract_orig( &n, etai_over_etat); + assert_eq!(actual, expected); +} + +impl Color { + pub fn write_color(self: Color, samples_per_pixel: i32) { + let scale = 1.0 / samples_per_pixel as f64; + let r = f64::sqrt(scale * self.x); + let g = f64::sqrt(scale * self.y); + let b = f64::sqrt(scale * self.z); + let r = 256.0 * f64::clamp(r, 0.0, 0.999); + let g = 256.0 * f64::clamp(g, 0.0, 0.999); + let b = 256.0 * f64::clamp(b, 0.0, 0.999); + println!("{} {} {}", r as i32, g as i32, b as i32); + } +} + +impl Default for Vec3 { + fn default() -> Self { Vec3 {x: 0.0, y: 0.0, z: 0.0} } +} + +impl Display for Vec3 { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{} {} {}", self.x, self.y, self.z) + } +} + +impl Add for Vec3 { + type Output = Vec3; + + fn add(self, other: Vec3) -> Vec3 { + Vec3 { + x: self.x + other.x(), + y: self.y + other.y(), + z: self.z + other.z(), + } + } +} + +impl AddAssign for Vec3 { + fn add_assign(&mut self, rhs: Self) { + self.x += rhs.x; + self.y += rhs.y; + self.z += rhs.z; + } +} + +impl Sub for Vec3 { + type Output = Vec3; + + fn sub(self, other: Vec3) -> Vec3 { + Vec3 { + x: self.x - other.x(), + y: self.y - other.y(), + z: self.z - other.z(), + } + } +} + +impl Sub for &Vec3 { + type Output = Vec3; + + fn sub(self, other: &Vec3) -> Vec3 { + Vec3 { + x: self.x - other.x(), + y: self.y - other.y(), + z: self.z - other.z(), + } + } +} + +impl Neg for Vec3 { + type Output = Vec3; + + fn neg(self) -> Vec3 { + Vec3 { + x: -self.x, + y: -self.y, + z: -self.z, + } + } +} + +impl Mul for Vec3 { + type Output = Vec3; + + fn mul(self, other: Vec3) -> Vec3 { + Vec3 { + x: self.x * other.x(), + y: self.y * other.y(), + z: self.z * other.z(), + } + } +} + +impl Mul for Vec3 { + type Output = Vec3; + + fn mul(self, other: f64) -> Vec3 { + Vec3 { + x: self.x * other, + y: self.y * other, + z: self.z * other, + } + } +} + +impl Mul for f64 { + type Output = Vec3; + + fn mul(self, other: Vec3) -> Vec3 { + other * self + } +} + +impl Div for Vec3 { + type Output = Vec3; + + fn div(self, other: Vec3) -> Vec3 { + Vec3 { + x: self.x / other.x(), + y: self.y / other.y(), + z: self.z / other.z(), + } + } +} + +impl Div for Vec3 { + type Output = Vec3; + + fn div(self, other: f64) -> Vec3 { + Vec3 { + x: self.x / other, + y: self.y / other, + z: self.z / other, + } + } +} + +impl PartialEq for Vec3 { + fn eq(&self, other: &Vec3) -> bool { + self.x == other.x() && self.y == other.y() && self.z == other.z() + } +}