diff --git a/scenes/texture.json b/scenes/texture.json new file mode 100644 index 0000000..751cb12 --- /dev/null +++ b/scenes/texture.json @@ -0,0 +1,34 @@ +{ + "filename": "textured.png", + "image_width": 1920, + "image_height": 1080, + "max_depth": 50, + "camera": { + "anti_alias_rate": 2, + "fov": 50.0, + "look_from": [-2, 4, 5], + "look_at": [-2, 0.0, 0.0], + "vup": [0.0, 1.0, 0.0], "defocus_angle": 0, "focus_dist": 15.68 + }, + "materials": [ + { "type": "lambertian", "albedo": [0.2, 0.2, 0.2], "prob": 0.8 }, + { "type": "lambertian", "albedo": [0.9, 0.9, 0.0], "prob": 1.0, "fuzz": 0.1 }, + { "type": "dielectric", "refraction_index": 1.5}, + { "type": "normal"}, + {"type": "texture", "source": "./textures/earthmap1k.png"}, + {"type": "texture", "source": "./textures/yellow.png"} + ], + "objects": [ + { "type": "sphere", "center": [0.0, 0.0, -1.2], "radius": 0.9, "material": 5}, + { "type": "sphere", "center": [-2, 0, -1], "radius": 0.8, "material": 4}, + { "type": "triangle", "p1": [0, 0, -4], "p2": [4, 0, -4], "p3": [2, 2, -4], "material": 5}, + { "type": "triangle", "p1": [-2, 2, -4], "p2": [2, 2, -4], "p3": [0, 4, -4], "material": 4}, + { "type": "quad", "p1": [-20, -1, -20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [-20, 20, -20], "material": 0}, + { "type": "quad", "p1": [-20, -1, 20], "p2": [-20, -1, -20], "p3": [-20, 20, -20], "p4": [-20, 20, 20], "material": 0}, + { "type": "quad", "p1": [-20, -1, 20], "p2": [20, -1, 20], "p3": [20, -1, -20], "p4": [-20, -1, -20], "material": 0}, + { "type": "quad", "p1": [20, -1, 20], "p2": [20, -1, -20], "p3": [20, 20, -20], "p4": [20, 20, 20], "material": 0} + ] +} + + + diff --git a/src/objects/circle.rs b/src/objects/circle.rs index ee545fc..1f5cead 100644 --- a/src/objects/circle.rs +++ b/src/objects/circle.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{f32::consts::PI, sync::Arc}; use crate::{ objects::{materials::traits::Material, traits::Hittable}, @@ -45,8 +45,27 @@ impl Hittable for Circle { let p = r.at(t); if (p - self.center).length() < self.radius { - return Some(Hit::new(t, p, self.normal, self.material.clone(), self.normal.dot(&r.dir()) < 0.)); + let uv = self.to_uv(&p); + return Some(Hit::new( + t, + p, + self.normal, + self.material.clone(), + self.normal.dot(&r.dir()) < 0., + *uv.x(), + *uv.y(), + )); } None } + + fn to_uv(&self, point: &Vec3) -> Vec3 { + let p = *point - self.center; + // TODO: add rotated texture support + return Vec3::new( + 0.5 + p.y().atan2(*p.x()) / (2. * PI), + 1. - (p.z() / self.radius).acos() / PI, + 0.0, + ); + } } diff --git a/src/objects/cube.rs b/src/objects/cube.rs index 04e3e5b..3bd351d 100644 --- a/src/objects/cube.rs +++ b/src/objects/cube.rs @@ -41,4 +41,9 @@ impl Hittable for Cube { fn hit(&self, r: &Ray) -> Option { Hit::hit_list(&self.faces, r) } + + fn to_uv(&self, point: &Vec3) -> Vec3 { + // TODO: map to [0.1] relative to specific face + todo!() + } } diff --git a/src/objects/cylinder.rs b/src/objects/cylinder.rs index 78d052d..88759e7 100644 --- a/src/objects/cylinder.rs +++ b/src/objects/cylinder.rs @@ -23,4 +23,8 @@ impl Hittable for Cylinder { fn hit(&self, r: &crate::ray::Ray) -> Option { todo!() } + + fn to_uv(&self, point: &Vec3) -> Vec3 { + todo!() + } } diff --git a/src/objects/hit.rs b/src/objects/hit.rs index 71043fe..9284603 100644 --- a/src/objects/hit.rs +++ b/src/objects/hit.rs @@ -12,16 +12,20 @@ pub struct Hit { n: Vec3, mat: Arc, front_face: bool, + u: f32, + v: f32, } impl Hit { - pub fn new(t: f32, p: Vec3, n: Vec3, mat: Arc, front_face: bool) -> Self { + pub fn new(t: f32, p: Vec3, n: Vec3, mat: Arc, front_face: bool, u: f32, v: f32) -> Self { Self { t, p, n: if front_face { n } else { -n }, mat, front_face, + u, + v } } @@ -45,6 +49,10 @@ impl Hit { self.front_face } + pub fn uv(&self) -> (f32, f32) { + (self.u, self.v) + } + // TODO: use front_face to discard back-hits for culling pub fn hit_list(hittables: &Vec>, r: &Ray) -> Option { let mut closest: Option = None; diff --git a/src/objects/materials.rs b/src/objects/materials.rs index 60d9347..b4ae60d 100644 --- a/src/objects/materials.rs +++ b/src/objects/materials.rs @@ -2,3 +2,4 @@ pub mod dielectric; pub mod lambertian; pub mod normal; pub mod traits; +pub mod texture; diff --git a/src/objects/materials/texture.rs b/src/objects/materials/texture.rs new file mode 100644 index 0000000..f610d4b --- /dev/null +++ b/src/objects/materials/texture.rs @@ -0,0 +1,47 @@ +use image::{DynamicImage, ImageReader}; + +use crate::{ + objects::{hit::Hit, materials::traits::Material}, + ray::Ray, + vec3::Vec3, +}; + +#[derive(Debug)] +pub struct Texture { + source: DynamicImage, + width: u32, + height: u32, +} + +impl Texture { + pub fn new(texture: &str) -> Self { + let img = ImageReader::open(texture).unwrap().decode().unwrap(); // FIXME: unwraps + Self { + width: img.width(), + height: img.height(), + source: img, + } + } + + fn _idx(&self, u: f32, v: f32) -> usize { + let x: usize = (u * ((self.width - 1) as f32)) as usize; // TODO: check these calcs work + let y: usize = (v * ((self.height - 1) as f32)) as usize; + + y * self.width as usize + x + } + + fn _at(&self, u: f32, v: f32) -> Vec3 { + let b = self.source.as_bytes(); // FIXME: i think im indexing colours incorrectly + let idx = self._idx(u, v); + + Vec3::from_u8(b[3 * idx], b[3 * idx + 1], b[3 * idx + 2]) + } +} + +impl Material for Texture { + fn scatter(&self, hit: &Hit, _ray: &Ray) -> Option<(Option, Vec3)> { + let (u, v) = hit.uv(); + let col = self._at(u, 1. - v); + return Some((None, col)); + } +} diff --git a/src/objects/quad.rs b/src/objects/quad.rs index 5d3a3ea..88f6ab9 100644 --- a/src/objects/quad.rs +++ b/src/objects/quad.rs @@ -69,4 +69,10 @@ impl Hittable for Quad { r, ) } + + fn to_uv(&self, point: &Vec3) -> Vec3 { + let u = (*point - self.p1).dot(&(self.p2 - self.p1)) / (self.p2 - self.p1).length_squared(); + let v = (*point - self.p1).dot(&(self.p4 - self.p1)) / (self.p4 - self.p1).length_squared(); + Vec3::new(u, v, 0.) + } } diff --git a/src/objects/sphere.rs b/src/objects/sphere.rs index 943e54e..106c5df 100644 --- a/src/objects/sphere.rs +++ b/src/objects/sphere.rs @@ -1,4 +1,5 @@ use core::f32::math::sqrt; +use std::f32::consts::PI; use std::fmt::{self, Debug}; use std::sync::Arc; @@ -14,7 +15,6 @@ pub struct Sphere { material: Arc, } - impl Debug for Sphere { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Sphere") @@ -59,13 +59,26 @@ impl Hittable for Sphere { let t = if tl > 0.001 { tl } else { tr }; let p = r.at(t); let out_n = (p - self.center) / self.radius; + let uv = self.to_uv(&p); Some(Hit::new( t, p, (p - self.center).get_unit(), self.material.clone(), out_n.dot(r.dir()) < 0., + *uv.x(), + *uv.y(), )) } } + + fn to_uv(&self, point: &Vec3) -> Vec3 { + let p = *point - self.center; + // TODO: add rotated texture support + return Vec3::new( + 0.5 + p.y().atan2(*p.x()) / (2. * PI), + 1. - (p.z() / self.radius).acos() / PI, + 0.0, + ); + } } diff --git a/src/objects/traits.rs b/src/objects/traits.rs index 2a33df5..b65803b 100644 --- a/src/objects/traits.rs +++ b/src/objects/traits.rs @@ -2,7 +2,10 @@ use std::fmt::Debug; use crate::Ray; use crate::objects::hit::Hit; +use crate::vec3::Vec3; pub trait Hittable: Debug + Send + Sync { fn hit(&self, r: &Ray) -> Option; + fn to_uv(&self, point: &Vec3) -> Vec3; // TODO: overhaul; remove u,v in Hit and change Mat ref to + // Hittable ref. Then call this function to calculate u,v coords as necessary. } diff --git a/src/objects/triangle.rs b/src/objects/triangle.rs index c0b1f64..b743881 100644 --- a/src/objects/triangle.rs +++ b/src/objects/triangle.rs @@ -11,19 +11,60 @@ pub struct Triangle { p3: Vec3, material: Arc, normal: Vec3, + + // barycentric helpers + v0: Vec3, + v1: Vec3, + d00: f32, + d01: f32, + d11: f32, + denom: f32, } impl Triangle { pub fn new(p1: Vec3, p2: Vec3, p3: Vec3, material: Arc) -> Self { + // barycentric helpers cached to save computations + let v0 = p2 - p1; + let v1 = p3 - p1; + let d00 = v0.dot(&v0); + let d01 = v0.dot(&v1); + let d11 = v1.dot(&v1); + Self { p1, p2, p3, material, normal: (p2 - p1).cross(&(p3 - p1)).get_unit(), + v0, + v1, + d00, + d01, + d11, + denom: d00 * d11 - d01 * d01, } } + // TODO: make the global Triangle::hit function not take precomped u,v somehow + // FIXME/DEBT: this function recalculates values for each pass that can be cached on the + // triangle struct itself. REFACTOR to not use this function + pub fn to_uv(p1: &Vec3, p2: &Vec3, p3: &Vec3, point: &Vec3) -> Vec3 { + let v0 = *p2 - p1; + let v1 = *p3 - p1; + let v2 = *point - p1; + let d00 = v0.dot(&v0); + let d01 = v0.dot(&v1); + let d11 = v1.dot(&v1); + let denom = d00 * d11 - d01 * d01; + + let d20 = v2.dot(&v0); + let d21 = v2.dot(&v1); + + let v = (d11 * d20 - d01 * d21) / denom; + let w = (d00 * d21 - d01 * d20) / denom; + Vec3::new(1. - v - w, v, w) + } + pub fn hit( p1: Vec3, p2: Vec3, @@ -45,6 +86,7 @@ impl Triangle { if t < 0. { return None; }; + let uvw = Triangle::to_uv(&p1, &p2, &p3, &r.at(t)); let p = r.at(t); let a4 = (p3 - p1).cross(&(p2 - p1)).length(); @@ -54,7 +96,15 @@ impl Triangle { let diff = (a4 - a1 - a2 - a3).abs(); if diff < 0.001 { - Some(Hit::new(t, p, normal, material, normal.dot(&r.dir()) < 0.)) + Some(Hit::new( + t, + p, + normal, + material, + normal.dot(&r.dir()) < 0., + *uvw.x(), + *uvw.y(), + )) } else { None } @@ -83,4 +133,14 @@ impl Hittable for Triangle { r, ) } + + fn to_uv(&self, point: &Vec3) -> Vec3 { + let v2 = *point - self.p1; + let d20 = v2.dot(&self.v0); + let d21 = v2.dot(&self.v1); + + let v = (self.d11 * d20 - self.d01 * d21) / self.denom; + let w = (self.d00 * d21 - self.d01 * d20) / self.denom; + Vec3::new(1. - v - w, v, w) + } } diff --git a/src/scenes/material_def.rs b/src/scenes/material_def.rs index fd8ae64..2618a7b 100644 --- a/src/scenes/material_def.rs +++ b/src/scenes/material_def.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use serde::Deserialize; -use crate::objects::materials::{dielectric::Dielectric, lambertian::{Lambertian, Metal}, normal::Normal, traits::Material}; +use crate::objects::materials::{dielectric::Dielectric, lambertian::{Lambertian, Metal}, normal::Normal, texture::Texture, traits::Material}; #[derive(Deserialize)] @@ -12,6 +12,7 @@ pub(crate) enum MaterialDef { Metal(Metal), Dielectric(Dielectric), Normal(Normal), + Texture(RawTexture), } impl MaterialDef { @@ -21,7 +22,13 @@ impl MaterialDef { MaterialDef::Metal(m) => Arc::new(m), MaterialDef::Dielectric(d) => Arc::new(d), MaterialDef::Normal(n) => Arc::new(n), + MaterialDef::Texture(t) => Arc::new(Texture::new(&t.source)), // FIXME: error handling } } } +#[derive(Deserialize)] +pub(crate) struct RawTexture { + pub source: String, +} + diff --git a/src/vec3.rs b/src/vec3.rs index 8dcba26..a2466cb 100644 --- a/src/vec3.rs +++ b/src/vec3.rs @@ -29,6 +29,14 @@ impl Vec3 { } } + pub fn from_u8(r: u8, b: u8, g: u8) -> Self { + Self { + x: r as f32 / 255., + y: g as f32 / 255., + z: b as f32/ 255., + } + } + pub fn random() -> Self { let mut rng = rand::rng(); Self { @@ -492,3 +500,4 @@ impl Display for Vec3 { write!(f, "({}, {}, {})", self.x, self.y, self.z) } } +