ft: moved random scene code to function; cleaned up tri/quad/cube hit logic; made camera attributes private

This commit is contained in:
2026-05-31 13:51:34 +02:00
parent 71985a6c34
commit 64cad7eda6
11 changed files with 259 additions and 221 deletions

View File

@@ -1,11 +1,11 @@
use std::{f32::consts::PI};
use std::f32::consts::PI;
use crate::{ray::Ray, vec3::Vec3};
#[derive(Debug)]
pub struct Camera {
// raytracing
pub anti_alias_rate: u32,
anti_alias_rate: u32,
pixel00_loc: Vec3,
pixel_delta_u: Vec3,
pixel_delta_v: Vec3,
@@ -83,7 +83,7 @@ impl Camera {
}
}
pub fn init(&mut self, w: f32, hi: f32) {
pub fn init(&mut self, width: u32, height: u32) {
if !self.dirty {
return;
}
@@ -97,13 +97,13 @@ impl Camera {
// viewport
let viewport_height = 2. * h * self.focus_dist;
let viewport_width = viewport_height * (w / hi);
let viewport_width = viewport_height * (width as f32 / height as f32);
let viewport_u = viewport_width * self.u;
let viewport_v = viewport_height * -self.v;
// variables
self.pixel_delta_u = viewport_u / w;
self.pixel_delta_v = viewport_v / hi;
self.pixel_delta_u = viewport_u / width;
self.pixel_delta_v = viewport_v / height;
self.pixel00_loc =
self.look_from - (self.focus_dist * self.w) - viewport_u / 2. - viewport_v / 2.;
self.dirty = false;

View File

@@ -8,26 +8,19 @@ mod scenes;
mod vec3;
use std::{env, fs};
use std::sync::Arc;
use crate::camera::Camera;
use crate::objects::materials::dielectric::Dielectric;
use crate::objects::materials::lambertian::{Lambertian, Metal};
use crate::objects::sphere::Sphere;
use crate::objects::traits::Hittable;
use crate::ray::Ray;
use crate::raytracer::render;
use crate::scenes::scene::Scene;
use crate::vec3::Vec3;
use dotenv::dotenv;
use rand::RngExt;
// TODO: implement scene serialization
fn main() {
dotenv().ok();
pretty_env_logger::init();
// TODO: better cli parsing
// TODO: better cli parsing || add random flag to generate random scene
let mut json_file = "./scenes/scene.json";
let args: Vec<String> = env::args().collect();
if args.len() > 1 {
@@ -36,93 +29,6 @@ fn main() {
let json_str = fs::read_to_string(json_file).expect("Reading specified scene file failed!");
let mut scene: Scene = serde_json::from_str(&json_str).unwrap();
let mut scene: Scene = Scene::random();
render(&mut scene);
return;
// random spheres code; thought: make this available as cli flag?
let ground = Lambertian::rgb(-2.5, 0.5, 0.5, 1.);
let mut world: Vec<Arc<dyn Hittable>> = vec![Arc::new(Sphere::xyz(
0.,
-1000.,
0.,
1000.,
Arc::new(ground),
))];
let mut rng = rand::rng();
let point = Vec3::new(4., 0.2, 0.);
for a in -11..11 {
for b in -11..11 {
let mat = rng.random_range((0.)..1.);
let center = Vec3::new(
a as f32 + 0.9 * rng.random_range((0.)..1.),
0.2,
b as f32 + 0.9 * rng.random_range((0.)..1.),
);
if (center - point).length() > 0.9 {
if mat < 0.8 {
// diffuse
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Lambertian::new(Vec3::random() * Vec3::random(), 1.)),
)));
} else if mat < 0.95 {
// metal
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Metal::rgb(
rng.random_range(0.5..1.),
rng.random_range(0.5..1.),
rng.random_range(0.5..1.),
1.,
rng.random_range((0.)..0.5),
)),
)));
} else {
// glass
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Dielectric::new(1.5)),
)));
}
}
}
}
world.push(Arc::new(Sphere::xyz(
0.,
1.,
0.,
1.,
Arc::new(Dielectric::new(1.5)),
)));
world.push(Arc::new(Sphere::xyz(
-4.,
1.,
0.,
1.,
Arc::new(Lambertian::rgb(0.4, 0.2, 0.1, 1.)),
)));
world.push(Arc::new(Sphere::xyz(
4.,
1.,
0.,
1.,
Arc::new(Metal::rgb(0.7, 0.6, 0.5, 1., 0.)),
)));
let mut c = Camera::new();
c.set_fov(20.);
c.set_anti_alias_rate(23);
c.set_vup(Vec3::new(0., 1., 0.));
c.set_look_from(Vec3::new(13., 2., 3.));
c.set_look_at(Vec3::new(0., 0., 0.));
c.add_defocus_blur(0.6, 10.);
let mut s = Scene::new(c, world, "output.png".to_string(), 1920, 1080, 50);
render(&mut s);
}

View File

@@ -17,7 +17,15 @@ pub struct Hit {
}
impl Hit {
pub fn new(t: f32, p: Vec3, n: Vec3, mat: Arc<dyn Material>, front_face: bool, u: f32, v: f32) -> Self {
pub fn new(
t: f32,
p: Vec3,
n: Vec3,
mat: Arc<dyn Material>,
front_face: bool,
u: f32,
v: f32,
) -> Self {
Self {
t,
p,
@@ -25,7 +33,26 @@ impl Hit {
mat,
front_face,
u,
v
v,
}
}
pub fn from_minimal(
mh: MinimalHit,
n: Vec3,
mat: Arc<dyn Material>,
front_face: bool,
u: f32,
v: f32,
) -> Self {
Self {
t: mh.t,
p: mh.p,
n,
mat,
front_face,
u,
v,
}
}
@@ -69,3 +96,22 @@ impl Hit {
closest
}
}
pub struct MinimalHit {
t: f32,
p: Vec3,
}
impl MinimalHit {
pub fn new(t: f32, p: Vec3) -> Self {
Self { t, p }
}
pub fn t(&self) -> f32 {
self.t
}
pub fn p(&self) -> &Vec3 {
&self.p
}
}

View File

@@ -7,6 +7,11 @@ use crate::{
vec3::Vec3,
};
// TODO: overhaul texture implementation so we can support textures for arbitrary materials.
// Basically remove this struct, and instead make lambertian/metal/dielectric/take either a texture
// or a flat colour
// I think we can use an enum ColourSource with a function to fetch a specific colour depending on
// if we have a texture/flat colour. and yeah.
#[derive(Debug)]
pub struct Texture {
source: DynamicImage,
@@ -48,7 +53,7 @@ impl Texture {
}
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 x: usize = (u * ((self.width - 1) as f32)) as usize;
let y: usize = (v * ((self.height - 1) as f32)) as usize;
y * self.width as usize + x

View File

@@ -13,6 +13,10 @@ pub struct Quad {
p4: Vec3,
material: Arc<dyn Material>,
normal: Vec3,
// helpers
t1: Triangle,
t2: Triangle,
}
impl Quad {
@@ -22,27 +26,12 @@ impl Quad {
p2,
p3,
p4,
material,
material: material.clone(),
normal: (p2 - p1).cross(&(p4 - p1)).get_unit(),
t1: Triangle::new(p1, p2, p4, material.clone()),
t2: Triangle::new(p2, p3, p4, material),
}
}
pub fn hit(
p1: Vec3,
p2: Vec3,
p3: Vec3,
p4: Vec3,
material: Arc<dyn Material>,
normal: Vec3,
r: &Ray,
) -> Option<Hit> {
let isct1 = Triangle::hit(p1, p2, p4, material.clone(), normal, r);
let isct2 = Triangle::hit(p2, p3, p4, material.clone(), normal, r);
if isct1.is_some() {
return isct1;
}
isct2
}
}
impl Debug for Quad {
@@ -57,20 +46,19 @@ impl Debug for Quad {
}
}
// FIXME: texture mapping does not work properly for quads
impl Hittable for Quad {
fn hit(&self, r: &Ray) -> Option<Hit> {
let hit = Quad::hit(
self.p1,
self.p2,
self.p3,
self.p4,
self.material.clone(),
self.normal,
r,
);
let mh = self.t1.calculate_hit(r).or(self.t2.calculate_hit(r))?;
let uvw = self.to_uv(mh.p());
return hit
Some(Hit::from_minimal(
mh,
self.normal,
self.material.clone(),
false,
*uvw.x(),
*uvw.y(),
))
}
fn to_uv(&self, point: &Vec3) -> Vec3 {

View File

@@ -5,7 +5,6 @@ use crate::objects::hit::Hit;
use crate::vec3::Vec3;
pub trait Hittable: Debug + Send + Sync {
fn to_uv(&self, point: &Vec3) -> Vec3;
fn hit(&self, r: &Ray) -> Option<Hit>;
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.
}

View File

@@ -1,4 +1,4 @@
use crate::objects::hit::Hit;
use crate::objects::hit::{Hit, MinimalHit};
use crate::objects::traits::Hittable;
use crate::ray::Ray;
use crate::{objects::materials::traits::Material, vec3::Vec3};
@@ -19,6 +19,7 @@ pub struct Triangle {
d01: f32,
d11: f32,
denom: f32,
a4: f32,
}
impl Triangle {
@@ -29,6 +30,7 @@ impl Triangle {
let d00 = v0.dot(&v0);
let d01 = v0.dot(&v1);
let d11 = v1.dot(&v1);
let a4 = (p3 - p1).cross(&(p2 - p1)).length();
Self {
p1,
@@ -42,69 +44,33 @@ impl Triangle {
d01,
d11,
denom: d00 * d11 - d01 * d01,
a4,
}
}
// 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,
p3: Vec3,
material: Arc<dyn Material>,
normal: Vec3,
r: &Ray,
) -> Option<Hit> {
pub fn calculate_hit(&self, r: &Ray) -> Option<MinimalHit> {
// check if ray parallel to plane
let dot = normal.dot(r.dir());
let dot = self.normal.dot(r.dir());
if dot == 0.0 {
return None;
}
let d = (-normal).dot(&p1);
let d = (-self.normal).dot(&self.p1);
// hitpoint on plane
let t = -(normal.dot(&(r.origin() + d))) / dot;
let t = -(self.normal.dot(&(r.origin() + d))) / dot;
// hits behind camera
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();
let a3 = (p3 - p).cross(&(p2 - p)).length();
let a2 = (p3 - p).cross(&(p1 - p)).length();
let a1 = (p2 - p).cross(&(p1 - p)).length();
let a3 = (self.p3 - p).cross(&(self.p2 - p)).length();
let a2 = (self.p3 - p).cross(&(self.p1 - p)).length();
let a1 = (self.p2 - p).cross(&(self.p1 - p)).length();
let diff = (a4 - a1 - a2 - a3).abs();
let diff = (self.a4 - a1 - a2 - a3).abs();
if diff < 0.001 {
Some(Hit::new(
t,
p,
normal,
material,
normal.dot(r.dir()) < 0.,
*uvw.x(),
*uvw.y(),
))
Some(MinimalHit::new(t, p))
} else {
None
}
@@ -124,14 +90,17 @@ impl Debug for Triangle {
impl Hittable for Triangle {
fn hit(&self, r: &Ray) -> Option<Hit> {
Triangle::hit(
self.p1,
self.p2,
self.p3,
self.material.clone(),
let mh = self.calculate_hit(r)?;
let uvw = self.to_uv(mh.p());
Some(Hit::from_minimal(
mh,
self.normal,
r,
)
self.material.clone(),
self.normal.dot(r.dir()) < 0.,
*uvw.x(),
*uvw.y(),
))
}
fn to_uv(&self, point: &Vec3) -> Vec3 {

View File

@@ -27,9 +27,9 @@ pub fn render(scene: &mut Scene) {
scene.init();
// TODO: currently splits per vertical line, but could be more granular (per chunk)
let mut pixels = vec![0_u8; (scene.image_width * scene.image_height * 3) as usize];
let mut pixels = vec![0_u8; (scene.get_image_width() * scene.get_image_height() * 3) as usize];
let scanlines: Vec<(usize, &mut [u8])> = pixels
.chunks_mut(scene.image_width as usize * 3)
.chunks_mut(scene.get_image_width() as usize * 3)
.enumerate()
.collect();
@@ -37,14 +37,18 @@ pub fn render(scene: &mut Scene) {
scanlines.into_par_iter().for_each(|(i, chunk)| {
render_chunk(chunk, scene, i as u32);
});
info!("rendering took {}.{} seconds.", now.elapsed().as_secs(), now.elapsed().subsec_nanos());
info!(
"rendering took {}.{} seconds.",
now.elapsed().as_secs(),
now.elapsed().subsec_nanos()
);
info!("Writing image file...");
write_image(
&scene.filename,
&scene.get_filename(),
&pixels,
scene.image_width,
scene.image_height,
scene.get_image_width() as u32,
scene.get_image_height() as u32,
);
}
@@ -52,19 +56,18 @@ pub fn render_chunk(chunk: &mut [u8], scene: &Scene, y: u32) {
let camera = &scene.get_camera();
let aa_rate = &scene.get_camera().get_anti_alias_rate();
for i in 0..scene.image_width {
for i in 0..scene.get_image_width() {
let pixel_tl = camera.get_pixel_tl(i, y);
let mut pixel_colour = Colour::default();
for y in 1..(aa_rate + 1) {
for x in 1..(aa_rate + 1) {
let r = camera.get_ray(pixel_tl, x, y);
pixel_colour += ray_colour(&scene.objects, &r, scene.max_depth);
pixel_colour += ray_colour(&scene.get_objects(), &r, scene.get_max_depth());
}
}
let (r, g, b) =
(pixel_colour / (aa_rate * aa_rate) as f32).output();
let (r, g, b) = (pixel_colour / (aa_rate * aa_rate) as f32).output();
chunk[(i * 3) as usize] = r;
chunk[(i * 3) as usize + 1] = g;
chunk[(i * 3) as usize + 2] = b;

View File

@@ -22,7 +22,7 @@ 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
MaterialDef::Texture(t) => Arc::new(Texture::new(&t.source)),
}
}
}

View File

@@ -1,23 +1,35 @@
use std::sync::Arc;
use rand::RngExt;
use serde::Deserialize;
use crate::{
camera::Camera,
objects::{materials::traits::Material, traits::Hittable},
objects::{
materials::{
dielectric::Dielectric,
lambertian::{Lambertian, Metal},
traits::Material,
},
sphere::Sphere,
traits::Hittable,
},
scenes::{hittable_def::HittableDef, material_def::MaterialDef, raw_camera::RawCamera},
vec3::Vec3,
};
#[derive(Debug)]
pub struct Scene {
pub camera: Camera, // FIXME: should not be public
pub objects: Vec<Arc<dyn Hittable>>,
camera: Camera,
objects: Vec<Arc<dyn Hittable>>,
// image
pub filename: String,
pub image_width: u32,
pub image_height: u32,
filename: String,
image_width: u32,
image_height: u32,
// raytracing // TODO: think about organisation of these vars, also in Camera
pub max_depth: u32,
max_depth: u32,
}
impl Scene {
@@ -39,20 +51,127 @@ impl Scene {
}
}
pub fn random() -> Self {
let ground = Lambertian::rgb(-2.5, 0.5, 0.5, 1.);
let mut world: Vec<Arc<dyn Hittable>> = vec![Arc::new(Sphere::xyz(
0.,
-1000.,
0.,
1000.,
Arc::new(ground),
))];
let mut rng = rand::rng();
let point = Vec3::new(4., 0.2, 0.);
for a in -11..11 {
for b in -11..11 {
let mat = rng.random_range((0.)..1.);
let center = Vec3::new(
a as f32 + 0.9 * rng.random_range((0.)..1.),
0.2,
b as f32 + 0.9 * rng.random_range((0.)..1.),
);
if (center - point).length() > 0.9 {
if mat < 0.8 {
// diffuse
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Lambertian::new(Vec3::random() * Vec3::random(), 1.)),
)));
} else if mat < 0.95 {
// metal
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Metal::rgb(
rng.random_range(0.5..1.),
rng.random_range(0.5..1.),
rng.random_range(0.5..1.),
1.,
rng.random_range((0.)..0.5),
)),
)));
} else {
// glass
world.push(Arc::new(Sphere::new(
center,
0.2,
Arc::new(Dielectric::new(1.5)),
)));
}
}
}
}
world.push(Arc::new(Sphere::xyz(
0.,
1.,
0.,
1.,
Arc::new(Dielectric::new(1.5)),
)));
world.push(Arc::new(Sphere::xyz(
-4.,
1.,
0.,
1.,
Arc::new(Lambertian::rgb(0.4, 0.2, 0.1, 1.)),
)));
world.push(Arc::new(Sphere::xyz(
4.,
1.,
0.,
1.,
Arc::new(Metal::rgb(0.7, 0.6, 0.5, 1., 0.)),
)));
let mut c = Camera::new();
c.set_fov(20.);
c.set_anti_alias_rate(2);
c.set_vup(Vec3::new(0., 1., 0.));
c.set_look_from(Vec3::new(13., 2., 3.));
c.set_look_at(Vec3::new(0., 0., 0.));
c.add_defocus_blur(0.6, 10.);
Self {
camera: c,
objects: world,
filename: "random.png".to_string(),
image_width: 1920,
image_height: 1080,
max_depth: 50,
}
}
pub fn get_camera(&self) -> &Camera {
&self.camera
}
pub fn get_image_width(&self) -> f32 {
self.image_width as f32
pub fn get_image_width(&self) -> u32 {
self.image_width
}
pub fn get_image_height(&self) -> f32 {
self.image_height as f32
pub fn get_image_height(&self) -> u32 {
self.image_height
}
pub fn get_objects(&self) -> &Vec<Arc<dyn Hittable>> {
&self.objects
}
pub fn get_max_depth(&self) -> u32 {
self.max_depth
}
pub fn get_filename(&self) -> &String {
&self.filename
}
pub fn init(&mut self) {
self.camera.init(self.get_image_width(), self.get_image_height());
self.camera
.init(self.get_image_width(), self.get_image_height());
}
}

View File

@@ -33,7 +33,7 @@ impl Vec3 {
Self {
x: r as f32 / 255.,
y: g as f32 / 255.,
z: b as f32/ 255.,
z: b as f32 / 255.,
}
}
@@ -444,16 +444,20 @@ impl Div<Vec3> for Vec3 {
}
}
}
impl Div<u32> for Vec3 {
type Output = Self;
fn div(self, u: u32) -> Self::Output {
self / u as f32
}
}
impl Div<i32> for Vec3 {
type Output = Self;
fn div(self, i: i32) -> Self::Output {
let f: f32 = i as f32;
Self {
x: self.x / f,
y: self.y / f,
z: self.z / f,
}
self / i as f32
}
}
@@ -500,4 +504,3 @@ impl Display for Vec3 {
write!(f, "({}, {}, {})", self.x, self.y, self.z)
}
}