Compare commits

...

8 Commits

Author SHA1 Message Date
Savanni D'Gerinel 2569a48792 Finish PPM conversions and plot the projectile 2024-06-09 15:58:24 -04:00
Savanni D'Gerinel 15d87fbde6 Create the converter to PPM 2024-06-09 15:45:13 -04:00
Savanni D'Gerinel 3c8536deb6 Create the color and the canvas 2024-06-09 14:57:33 -04:00
Savanni D'Gerinel d0a8be63e9 Change the Tuple to a tuple without field names
This helps out when I go to use the same data structure as the backing
for colors.
2024-06-09 14:36:23 -04:00
Savanni D'Gerinel 2a38ca38e1 Extract the types into separate files
Not for any reason other than clarity. The number of operations that are
declared for each type is making it difficult to find operations and
difficult to keep the order consistent.
2024-06-09 14:18:46 -04:00
Savanni D'Gerinel 39c947b461 Implement the remaining operations and the projectile simulator 2024-06-09 14:08:01 -04:00
Savanni D'Gerinel e23a4aacab Add primitive math operations 2024-06-09 13:19:47 -04:00
Savanni D'Gerinel 01f3e05235 Create the basic Tuple, Point, and Vector types 2024-06-09 12:24:41 -04:00
12 changed files with 732 additions and 1 deletions

4
Cargo.lock generated
View File

@ -3331,6 +3331,10 @@ dependencies = [
"rand_core 0.6.4",
]
[[package]]
name = "ray-tracer"
version = "0.1.0"
[[package]]
name = "rayon"
version = "1.8.0"

View File

@ -27,5 +27,5 @@ members = [
"sgf",
"timezone-testing",
"tree",
"visions/server",
"visions/server", "ray-tracer",
]

11
ray-tracer/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "ray-tracer"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[[bin]]
name = "projectile"

View File

@ -0,0 +1,53 @@
use ray_tracer::{types::*, PPM};
use std::{fs::File, io::Write};
#[derive(Clone, Copy)]
struct Projectile {
position: Point,
velocity: Vector,
}
struct Environment {
gravity: Vector,
wind: Vector,
}
fn tick(env: &Environment, proj: &Projectile) -> Projectile {
let position = proj.position + proj.velocity;
let velocity = proj.velocity + env.gravity + env.wind;
Projectile { position, velocity }
}
fn main() {
let mut canvas = Canvas::new(900, 550);
let start = Projectile {
position: Point::new(0., 1., 0.),
velocity: Vector::new(1., 5., 0.).normalize() * 5.,
};
let e = Environment {
gravity: Vector::new(0., -0.1, 0.),
wind: Vector::new(-0.01, 0., 0.),
};
let mut p = start;
while p.position.y() > 0. {
p = tick(&e, &p);
let x = p.position.x().round() as usize;
let y = p.position.y().round() as usize;
if x > 1 && x < 900 && y > 1 && y < 550 {
*canvas.pixel_mut(x, 550 - y - 1) = Color::new(1., 1., 0.);
*canvas.pixel_mut(x+1, 550 - y - 1) = Color::new(1., 1., 0.);
*canvas.pixel_mut(x-1, 550 - y - 1) = Color::new(1., 1., 0.);
*canvas.pixel_mut(x, 550 - y) = Color::new(1., 1., 0.);
*canvas.pixel_mut(x, 550 - y - 2) = Color::new(1., 1., 0.);
}
}
let ppm = PPM::from(canvas);
let mut file = File::create("projectile.ppm").unwrap();
file.write(ppm.as_bytes()).unwrap();
}

5
ray-tracer/src/lib.rs Normal file
View File

@ -0,0 +1,5 @@
mod ppm;
pub mod types;
pub use ppm::PPM;

119
ray-tracer/src/ppm.rs Normal file
View File

@ -0,0 +1,119 @@
use crate::types::Canvas;
fn color_float_to_int(val: f64) -> u8 {
(val * 256.).clamp(0., 255.) as u8
}
fn join_to_line_limit(data: impl IntoIterator<Item = String>) -> Vec<String> {
let mut lines = vec![];
let mut line = String::new();
let mut iter = data.into_iter();
while let Some(element) = iter.next() {
if line.is_empty() {
line = line + &element;
} else if line.len() + 1 + element.len() < 70 {
line = line + " " + &element;
} else {
lines.push(line);
line = element;
}
}
if !line.is_empty() {
lines.push(line);
}
lines
}
#[derive(Debug)]
pub struct PPM(String);
impl From<Canvas> for PPM {
fn from(c: Canvas) -> Self {
// let v = vec![0.; c.width() * c.height() * 3];
let header = format!("P3\n{} {}\n255\n", c.width(), c.height());
let mut data = vec![];
for y in 0..c.height() {
let mut row = vec![];
for x in 0..c.width() {
let pixel = c.pixel(x, y);
row.push(color_float_to_int(pixel.red()).to_string());
row.push(color_float_to_int(pixel.green()).to_string());
row.push(color_float_to_int(pixel.blue()).to_string());
}
let mut lines = join_to_line_limit(row);
data.append(&mut lines);
}
let data = data.join("\n");
Self(format!("{header}{data}\n"))
}
}
impl std::ops::Deref for PPM {
type Target = String;
fn deref(&self) -> &String {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::Color;
#[test]
fn construct_ppm_header() {
let c = Canvas::new(5, 3);
let ppm = PPM::from(c);
assert!(ppm.starts_with(
"P3\n\
5 3\n\
255\n"
));
}
#[test]
fn construct_full_ppm_data() {
let mut c = Canvas::new(5, 3);
*c.pixel_mut(0, 0) = Color::new(1.5, 0., 0.);
*c.pixel_mut(2, 1) = Color::new(0., 0.5, 0.);
*c.pixel_mut(4, 2) = Color::new(-0.5, 0., 1.);
let expected = "P3\n\
5 3\n\
255\n\
255 0 0 0 0 0 0 0 0 0 0 0 0 0 0\n\
0 0 0 0 0 0 0 128 0 0 0 0 0 0 0\n\
0 0 0 0 0 0 0 0 0 0 0 0 0 0 255\n";
let ppm = PPM::from(c);
assert_eq!(*ppm, expected);
}
#[test]
fn ppm_line_length_limit() {
let mut c = Canvas::new(10, 2);
for y in 0..2 {
for x in 0..10 {
*c.pixel_mut(x, y) = Color::new(1., 0.8, 0.6);
}
}
let expected = "P3\n\
10 2\n\
255\n\
255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204\n\
153 255 204 153 255 204 153 255 204 153 255 204 153\n\
255 204 153 255 204 153 255 204 153 255 204 153 255 204 153 255 204\n\
153 255 204 153 255 204 153 255 204 153 255 204 153\n";
let ppm = PPM::from(c);
assert_eq!(*ppm, expected);
}
}

View File

@ -0,0 +1,59 @@
use crate::types::Color;
pub struct Canvas {
width: usize,
height: usize,
pixels: Vec<Color>,
}
impl Canvas {
pub fn new(width: usize, height: usize) -> Self {
Self {
width,
height,
pixels: vec![Color::default(); width * height],
}
}
#[inline]
pub fn width(&self) -> usize {
self.width
}
#[inline]
pub fn height(&self) -> usize {
self.height
}
pub fn pixel(&self, x: usize, y: usize) -> &Color {
&self.pixels[self.addr(y, x)]
}
pub fn pixel_mut<'a>(&'a mut self, x: usize, y: usize) -> &'a mut Color {
let addr = self.addr(y, x);
&mut self.pixels[addr]
}
#[inline]
fn addr(&self, y: usize, x: usize) -> usize {
let val = y * self.width() + x;
if val >= self.pixels.len() {
eprintln!("[{val}] out of range: [{x}, {y}]");
}
val
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn addresses() {
let c = Canvas::new(5, 3);
assert_eq!(c.addr(0, 0), 0);
assert_eq!(c.addr(1, 0), 5);
assert_eq!(c.addr(2, 0), 10);
}
}

View File

@ -0,0 +1,87 @@
use crate::types::Tuple;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Color(Tuple);
impl Color {
pub fn new(red: f64, green: f64, blue: f64) -> Self {
Self(Tuple(red, green, blue, 0.))
}
#[inline]
pub fn red(&self) -> f64 {
self.0.0
}
#[inline]
pub fn green(&self) -> f64 {
self.0.1
}
#[inline]
pub fn blue(&self) -> f64 {
self.0.2
}
}
impl Default for Color {
fn default() -> Self {
Self::new(0., 0., 0.)
}
}
impl From<Tuple> for Color {
fn from(tuple: Tuple) -> Self {
assert_eq!(tuple.3, 0.0);
Self(tuple)
}
}
impl std::ops::Deref for Color {
type Target = Tuple;
fn deref(&self) -> &Tuple {
&self.0
}
}
impl std::ops::Add for Color {
type Output = Color;
fn add(self, r: Self) -> Self {
Color::from(self.0 + r.0)
}
}
impl std::ops::Sub for Color {
type Output = Color;
fn sub(self, r: Self) -> Self {
Color::from(self.0 - r.0)
}
}
impl std::ops::Neg for Color {
type Output = Color;
fn neg(self) -> Self::Output {
let mut t = -self.0;
t.0 = 0.;
Color::from(t)
}
}
impl std::ops::Mul for Color {
type Output = Color;
fn mul(self, r: Color) -> Self {
let red = self.red() * r.red();
let green = self.green() * r.green();
let blue = self.blue() * r.blue();
Self::new(red, green, blue)
}
}
impl std::ops::Mul<f64> for Color {
type Output = Color;
fn mul(self, r: f64) -> Self {
Color::from(self.0 * r)
}
}

151
ray-tracer/src/types/mod.rs Normal file
View File

@ -0,0 +1,151 @@
mod canvas;
mod color;
mod point;
mod tuple;
mod vector;
pub use canvas::Canvas;
pub use color::Color;
pub use point::Point;
pub use tuple::Tuple;
pub use vector::Vector;
const EPSILON: f64 = 0.00001;
fn eq_f64(l: f64, r: f64) -> bool {
(l - r).abs() < EPSILON
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn eq_f64_compares_values() {
assert!(eq_f64(1.0, 1.0));
assert!(eq_f64(0.9994, 0.9994));
assert!(eq_f64(0.9999994, 0.9999995));
assert!(!eq_f64(0.9995, 0.9994));
}
#[test]
fn add_two_tuples() {
let a = Tuple(3., -2., 5., 1.);
let b = Tuple(-2., 3., 1., 0.);
assert_eq!(a + b, Tuple(1., 1., 6., 1.));
}
#[test]
fn subtracts_two_tuples() {
let a = Tuple(3., 2., 1., 1.);
let b = Tuple(5., 6., 7., 1.);
assert_eq!(a - b, Tuple(-2., -4., -6., 0.));
}
#[test]
fn adds_point_and_vector() {
let a = Point::new(3., -2., 5.);
let b = Vector::new(-2., 3., 1.);
assert_eq!(a + b, Point::new(1., 1., 6.,));
}
#[test]
fn subtracts_two_points() {
let a = Point::new(3., 2., 1.);
let b = Point::new(5., 6., 7.);
assert_eq!(a - b, Vector::new(-2., -4., -6.,));
}
#[test]
fn subtracts_vector_from_point() {
let a = Point::new(3., 2., 1.);
let b = Vector::new(5., 6., 7.);
assert_eq!(a - b, Point::new(-2., -4., -6.));
}
#[test]
fn subtracts_two_vectors() {
let a = Vector::new(3., 2., 1.);
let b = Vector::new(5., 6., 7.);
assert_eq!(a - b, Vector::new(-2., -4., -6.));
}
#[test]
fn it_negates_primitives() {
assert_eq!(-Tuple(1., 2., 3., 4.), Tuple(-1., -2., -3., -4.),);
}
#[test]
fn multiply_tuple_by_scalar() {
assert_eq!(Tuple(1., -2., 3., -4.) * 3.5, Tuple(3.5, -7., 10.5, -14.));
}
#[test]
fn divide_tuple_by_scalar() {
assert_eq!(Tuple(1., -2., 3., -4.) / 2., Tuple(0.5, -1., 1.5, -2.));
}
#[test]
fn magnitude_of_vector() {
assert_eq!(Vector::new(1., 0., 0.).magnitude(), 1.);
assert_eq!(Vector::new(0., 1., 0.).magnitude(), 1.);
assert_eq!(Vector::new(0., 0., 1.).magnitude(), 1.);
assert_eq!(Vector::new(1., 2., 3.).magnitude(), 14_f64.sqrt());
assert_eq!(Vector::new(-1., -2., -3.).magnitude(), 14_f64.sqrt());
}
#[test]
fn normalize_vector() {
assert_eq!(Vector::new(4., 0., 0.).normalize(), Vector::new(1., 0., 0.));
assert_eq!(
Vector::new(1., 2., 3.).normalize(),
Vector::new(0.26726, 0.53452, 0.80178)
);
assert_eq!(Vector::new(1., 2., 3.).normalize().magnitude(), 1.);
}
#[test]
fn dot_product() {
assert_eq!(Vector::new(1., 2., 3.).dot(&Vector::new(2., 3., 4.)), 20.);
}
#[test]
fn cross_product() {
assert_eq!(
Vector::new(1., 2., 3.).cross(&Vector::new(2., 3., 4.)),
Vector::new(-1., 2., -1.)
);
assert_eq!(
Vector::new(2., 3., 4.).cross(&Vector::new(1., 2., 3.)),
Vector::new(1., -2., 1.)
);
}
#[test]
fn multiply_colors() {
let c1 = Color::new(1., 0.2, 0.4);
let c2 = Color::new(0.9, 1., 0.1);
assert_eq!(c1 * c2, Color::new(0.9, 0.2, 0.04));
}
#[test]
fn it_creates_a_canvas() {
let c = Canvas::new(10, 20);
assert_eq!(c.width(), 10);
assert_eq!(c.height(), 20);
for row in 0..20 {
for col in 0..10 {
assert_eq!(*c.pixel(row, col), Color::default());
}
}
}
#[test]
fn it_can_write_pixel() {
let mut c = Canvas::new(10, 20);
let red = Color::new(1., 0., 0.);
*c.pixel_mut(2, 3) = red;
assert_eq!(*c.pixel(2, 3), red);
}
}

View File

@ -0,0 +1,75 @@
use crate::types::{Tuple, Vector};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Point(Tuple);
impl Point {
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self(Tuple(x, y, z, 1.0))
}
#[inline]
pub fn x(&self) -> f64 {
self.0.0
}
#[inline]
pub fn y(&self) -> f64 {
self.0.1
}
#[inline]
pub fn z(&self) -> f64 {
self.0.2
}
}
impl Default for Point {
fn default() -> Self {
Self::new(0., 0., 0.)
}
}
impl From<Tuple> for Point {
fn from(tuple: Tuple) -> Self {
assert_eq!(tuple.3, 1.0);
Self(tuple)
}
}
impl std::ops::Deref for Point {
type Target = Tuple;
fn deref(&self) -> &Tuple {
&self.0
}
}
impl std::ops::Add<Vector> for Point {
type Output = Point;
fn add(self, r: Vector) -> Self::Output {
Point::from(self.0 + *r)
}
}
impl std::ops::Sub for Point {
type Output = Vector;
fn sub(self, r: Point) -> Self::Output {
Vector::from(self.0 - r.0)
}
}
impl std::ops::Sub<Vector> for Point {
type Output = Point;
fn sub(self, r: Vector) -> Self::Output {
Point::from(self.0 - *r)
}
}
impl std::ops::Neg for Point {
type Output = Point;
fn neg(self) -> Self::Output {
let mut t = -self.0;
t.3 = 1.;
Point::from(t)
}
}

View File

@ -0,0 +1,72 @@
use crate::types::eq_f64;
#[derive(Debug, Clone, Copy)]
pub struct Tuple(
pub f64, // x or red
pub f64, // y or green
pub f64, // z or blue
pub f64, // w, the flag which
// indicates point vs vec, or alpha
);
impl Tuple {
pub fn dot(&self, r: &Tuple) -> f64 {
self.0 * r.0 + self.1 * r.1 + self.2 * r.2 + self.3 * r.3
}
}
impl PartialEq for Tuple {
fn eq(&self, r: &Tuple) -> bool {
eq_f64(self.0, r.0) && eq_f64(self.1, r.1) && eq_f64(self.2, r.2) && eq_f64(self.3, r.3)
}
}
impl std::ops::Add for Tuple {
type Output = Tuple;
fn add(self, r: Tuple) -> Self::Output {
Tuple(self.0 + r.0, self.1 + r.1, self.2 + r.2, self.3 + r.3)
}
}
impl std::ops::Sub for Tuple {
type Output = Tuple;
fn sub(self, r: Tuple) -> Self::Output {
Tuple(self.0 - r.0, self.1 - r.1, self.2 - r.2, self.3 - r.3)
}
}
impl std::ops::Neg for Tuple {
type Output = Tuple;
fn neg(self) -> Self::Output {
return Self::Output {
0: -self.0,
1: -self.1,
2: -self.2,
3: -self.3,
};
}
}
impl std::ops::Mul<f64> for Tuple {
type Output = Tuple;
fn mul(self, scalar: f64) -> Self::Output {
Tuple(
self.0 * scalar,
self.1 * scalar,
self.2 * scalar,
self.3 * scalar,
)
}
}
impl std::ops::Div<f64> for Tuple {
type Output = Tuple;
fn div(self, scalar: f64) -> Self::Output {
Tuple(
self.0 / scalar,
self.1 / scalar,
self.2 / scalar,
self.3 / scalar,
)
}
}

View File

@ -0,0 +1,95 @@
use crate::types::Tuple;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Vector(Tuple);
impl Vector {
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self(Tuple(x, y, z, 0.0))
}
#[inline]
pub fn x(&self) -> f64 {
self.0.0
}
#[inline]
pub fn y(&self) -> f64 {
self.0.1
}
#[inline]
pub fn z(&self) -> f64 {
self.0.2
}
pub fn magnitude(&self) -> f64 {
(self.x() * self.x() + self.y() * self.y() + self.z() * self.z()).sqrt()
}
pub fn normalize(&self) -> Self {
let mag = self.magnitude();
Self::new(self.x() / mag, self.y() / mag, self.z() / mag)
}
pub fn dot(&self, r: &Vector) -> f64 {
self.0.dot(r)
}
pub fn cross(&self, r: &Vector) -> Self {
let x = self.y() * r.z() - self.z() * r.y();
let y = self.z() * r.x() - self.x() * r.z();
let z = self.x() * r.y() - self.y() * r.x();
Self::new(x, y, z)
}
}
impl Default for Vector {
fn default() -> Self {
Self::new(0., 0., 0.)
}
}
impl From<Tuple> for Vector {
fn from(tuple: Tuple) -> Self {
assert_eq!(tuple.3, 0.0);
Self(tuple)
}
}
impl std::ops::Deref for Vector {
type Target = Tuple;
fn deref(&self) -> &Tuple {
&self.0
}
}
impl std::ops::Add for Vector {
type Output = Vector;
fn add(self, r: Self) -> Self {
Vector::from(self.0 + r.0)
}
}
impl std::ops::Sub for Vector {
type Output = Vector;
fn sub(self, r: Self) -> Self {
Vector::from(self.0 - r.0)
}
}
impl std::ops::Neg for Vector {
type Output = Vector;
fn neg(self) -> Self::Output {
let mut t = -self.0;
t.0 = 0.;
Vector::from(t)
}
}
impl std::ops::Mul<f64> for Vector {
type Output = Vector;
fn mul(self, r: f64) -> Self {
Vector::from(self.0 * r)
}
}