From 094f4947dcb640ae954ef6902a1b25833f209353 Mon Sep 17 00:00:00 2001 From: wires Date: Thu, 29 May 2025 16:30:25 -0400 Subject: [PATCH] reorganize core systems no change in functionality, but preparing for stuff in the near future like more accurately modeling film sensors, filtering, color science, etc. --- Cargo.lock | 7 ++++ core/Cargo.toml | 1 + core/src/camera.rs | 57 ++++++++++++++++++++++++++++--- core/src/color.rs | 58 +++++++++++++++++++++++++++---- core/src/geometry.rs | 2 +- core/src/geometry/_2d.rs | 68 ++++++++++++++++++++++++++++++++++--- core/src/geometry/shapes.rs | 4 +-- core/src/lib.rs | 64 +++++++++++++++------------------- py/src/lib.rs | 5 +-- 9 files changed, 210 insertions(+), 56 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d4675a..fc0899a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,7 @@ dependencies = [ "enum_dispatch", "fasthash", "glam", + "range2d", ] [[package]] @@ -257,6 +258,12 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" +[[package]] +name = "range2d" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7169c4e8549f72214a10fe6a64e24bf5e2d75ca9378d0ac7934afb9acbdfc313" + [[package]] name = "rdrand" version = "0.4.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index fd419bc..b7d2e00 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -11,6 +11,7 @@ bytemuck = { version = "1.22.0", optional = true } enum_dispatch = "0.3.13" fasthash = "0.4.0" glam = "0.30.2" +range2d = "0.2.0" [features] fast-math = ["glam/fast-math"] diff --git a/core/src/camera.rs b/core/src/camera.rs index b98d330..548e610 100644 --- a/core/src/camera.rs +++ b/core/src/camera.rs @@ -1,4 +1,8 @@ -use crate::geometry::{Point, Ray, Transform, Vector}; +use crate::{ + color::Rgba, + geometry::{Point, Point2, Ray, Transform, Transform2, Vector, point2, ray, vec3}, + sampling::PixelSample, +}; #[derive(Debug, Clone)] pub struct Camera { @@ -16,9 +20,52 @@ impl Camera { Self { transform } } - pub fn ray(&self, x: f32, y: f32) -> Ray { - let origin = self.transform * Point::ORIGIN; - let direction = self.transform * Vector::new(x, y, 1.0); - Ray::new(origin, direction) + pub fn ray(&self, p_film: Point2) -> Ray { + ray( + self.transform * Point::ORIGIN, + self.transform * vec3(p_film.x, p_film.y, 1.0), + ) + } +} + +pub struct Film { + width: u32, + height: u32, + transform: Transform2, + data: Box<[Rgba]>, +} + +impl Film { + pub fn new(width: u32, height: u32) -> Self { + let size = width as usize * height as usize; + + let data = vec![Rgba::BLACK; size].into_boxed_slice(); + + let w = width as f32; + let h = height as f32; + let ar = h / w; + + let transform = Transform2::translate(-1.0, ar) * Transform2::scale(2.0 / w, -2.0 / w); + + Self { + width, + height, + transform, + data, + } + } + + pub fn add_sample(&mut self, x: u32, y: u32, color: Rgba, weight: f32) { + let i = y as usize * self.width as usize + x as usize; + self.data[i] += color * weight; + } + + pub fn get_camera_sample(&self, x: u32, y: u32, sampler: &mut PixelSample) -> Point2 { + let p = point2(x as f32, y as f32) + sampler.get_2d().into(); + self.transform * p + } + + pub fn into_data(self) -> Box<[Rgba]> { + self.data } } diff --git a/core/src/color.rs b/core/src/color.rs index 9498ddc..44a62ef 100644 --- a/core/src/color.rs +++ b/core/src/color.rs @@ -1,4 +1,7 @@ -use std::fmt; +use std::{ + fmt, + ops::{Add, AddAssign, Mul}, +}; #[derive(Clone, Copy)] #[repr(C)] @@ -9,12 +12,12 @@ pub struct Rgba { a: f32, } -impl Rgba { - pub const BLACK: Self = Self::new(0.0, 0.0, 0.0, 1.0); +pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Rgba { + Rgba { r, g, b, a } +} - pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self { - Self { r, g, b, a } - } +impl Rgba { + pub const BLACK: Self = rgba(0.0, 0.0, 0.0, 1.0); } impl fmt::Debug for Rgba { @@ -27,3 +30,46 @@ impl fmt::Debug for Rgba { .finish() } } + +impl Add for Rgba { + type Output = Self; + + fn add(self, Self { r, g, b, a }: Self) -> Self::Output { + Self { + r: self.r + r, + g: self.g + g, + b: self.b + b, + a: self.a + a, + } + } +} + +impl AddAssign for Rgba { + fn add_assign(&mut self, Self { r, g, b, a }: Self) { + self.r += r; + self.g += g; + self.b += b; + self.a += a; + } +} + +impl Mul for Rgba { + type Output = Self; + + fn mul(self, rhs: f32) -> Self::Output { + Self { + r: self.r * rhs, + g: self.g * rhs, + b: self.b * rhs, + a: self.a * rhs, + } + } +} + +impl Mul for f32 { + type Output = Rgba; + + fn mul(self, rhs: Rgba) -> Self::Output { + rhs * self + } +} diff --git a/core/src/geometry.rs b/core/src/geometry.rs index 5946273..0c719fc 100644 --- a/core/src/geometry.rs +++ b/core/src/geometry.rs @@ -8,7 +8,7 @@ mod shapes; mod transform; pub use _2d::*; -pub use shapes::Shape; +pub use shapes::{Hit, Hittable, Shape}; pub use transform::Transform; #[repr(transparent)] diff --git a/core/src/geometry/_2d.rs b/core/src/geometry/_2d.rs index 492e844..bd37b70 100644 --- a/core/src/geometry/_2d.rs +++ b/core/src/geometry/_2d.rs @@ -1,6 +1,6 @@ use std::{ fmt, - ops::{Add, AddAssign, Div, DivAssign, Mul, Sub}, + ops::{Add, AddAssign, Deref, Div, DivAssign, Mul, Sub}, }; #[repr(transparent)] @@ -88,6 +88,12 @@ pub const fn point2(x: f32, y: f32) -> Point2 { Point2(glam::vec2(x, y)) } +impl From<(f32, f32)> for Point2 { + fn from((x, y): (f32, f32)) -> Self { + point2(x, y) + } +} + impl fmt::Debug for Point2 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple(stringify!(Point)) @@ -97,9 +103,12 @@ impl fmt::Debug for Point2 { } } -impl From<(f32, f32)> for Point2 { - fn from((x, y): (f32, f32)) -> Self { - point2(x, y) +impl Deref for Point2 { + type Target = glam::Vec2; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 } } @@ -126,3 +135,54 @@ impl Sub for Point2 { Vector2(self.0 - rhs.0) } } + +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct Transform2(glam::Affine2); + +impl fmt::Debug for Transform2 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct(stringify!(Transform)) + .field(stringify!(matrix2), &self.0.matrix2) + .field(stringify!(translation), &self.0.translation) + .finish() + } +} + +impl Transform2 { + pub fn scale(x: f32, y: f32) -> Self { + Self(glam::Affine2::from_scale(glam::vec2(x, y))) + } + + pub fn inverse(self) -> Self { + Self(self.0.inverse()) + } + + pub fn translate(x: f32, y: f32) -> Self { + Self(glam::Affine2::from_translation(glam::vec2(x, y))) + } +} + +impl Mul for Transform2 { + type Output = Transform2; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0 * rhs.0) + } +} + +impl Mul for Transform2 { + type Output = Vector2; + + fn mul(self, rhs: Vector2) -> Self::Output { + Vector2(self.0.transform_vector2(rhs.0)) + } +} + +impl Mul for Transform2 { + type Output = Point2; + + fn mul(self, rhs: Point2) -> Self::Output { + Point2(self.0.transform_point2(rhs.0)) + } +} diff --git a/core/src/geometry/shapes.rs b/core/src/geometry/shapes.rs index 68be55d..2af3b6e 100644 --- a/core/src/geometry/shapes.rs +++ b/core/src/geometry/shapes.rs @@ -5,14 +5,14 @@ use enum_dispatch::enum_dispatch; use super::{Point, Ray, Vector}; #[derive(Debug)] -pub(crate) struct Hit { +pub struct Hit { pub point: Point, pub normal: Vector, pub t: f32, } #[enum_dispatch] -pub(crate) trait Hittable { +pub trait Hittable { fn intersect(&self, ray: &Ray, t_max: f32) -> Option; } diff --git a/core/src/lib.rs b/core/src/lib.rs index bce4906..84c9bbe 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,3 +1,8 @@ +use core::f32; + +use geometry::{Hit, Hittable, Ray, point2, vec3}; +use range2d::Range2D; + #[cfg(feature = "bytemuck")] pub mod impl_bytemuck; @@ -7,70 +12,57 @@ mod camera; mod geometry; mod sampling; -use core::f32; - pub use camera::Camera; -pub use geometry::{Point, Transform, Vector, shapes::Shape}; +pub use geometry::{Point, Shape, Transform, Vector}; -use color::Rgba; -use geometry::{ - Ray, - shapes::{Hit, Hittable}, -}; +use camera::Film; +use color::{Rgba, rgba}; use sampling::Sampler; pub struct Scene { camera: Camera, - contents: Vec, + objects: Vec, } impl Scene { - pub fn new(camera: Camera, contents: Vec) -> Self { - Self { camera, contents } + pub fn new(camera: Camera, objects: Vec) -> Self { + Self { camera, objects } } - pub fn render(&self, width: u32, height: u32, samples: u32) -> Box<[Rgba]> { - let mut sampler = Sampler::new(width, height, samples, 0); - let mut data = Box::new_uninit_slice(width as usize * height as usize); - let w = width as f32; - let h = height as f32; - let aspect_ratio = h / w; - - for (i, pixel) in data.iter_mut().enumerate() { - let bx = i as u32 % width; - let by = i as u32 / width; + pub fn render(&self, width: u32, height: u32, samples: u32, seed: u32) -> Box<[Rgba]> { + let mut sampler = Sampler::new(width, height, samples, seed); + let mut film = Film::new(width, height); + for (y, x) in Range2D::new(0..height, 0..width) { let mut rgb = Vector::ZERO; - for mut pixel_samples in sampler.pixel(bx, by) { - let (x_offset, y_offset) = pixel_samples.get_2d(); - let x = bx as f32 + x_offset; - let y = by as f32 + y_offset; + for mut pixel_sample in sampler.pixel(x, y) { + let p_film = film.get_camera_sample(x, y, &mut pixel_sample); + let ray = self.camera.ray(p_film); - let cx = 2.0 * x / w - 1.0; - let cy = (1.0 - 2.0 * y / h) * aspect_ratio; - - if let Some(h) = self.intersect(self.camera.ray(cx, cy)) { - rgb += h.normal.normalize() * 0.5 + Vector::new(0.5, 0.5, 0.5); + if let Some(h) = self.intersect(ray) { + rgb += h.normal.normalize() * 0.5 + vec3(0.5, 0.5, 0.5); } } rgb /= samples as f32; - pixel.write(Rgba::new(rgb.x, rgb.y, rgb.z, 1.0)); + film.add_sample(x, y, rgba(rgb.x, rgb.y, rgb.z, 0.0), 1.0); } - unsafe { data.assume_init() } + film.into_data() } - fn intersect(&self, ray: Ray) -> Option { + fn intersect(&self, r: Ray) -> Option { let mut hit: Option = None; - for shape in &self.contents { + + for obj in &self.objects { let t_max = hit.as_ref().map(|h| h.t).unwrap_or(f32::INFINITY); - if let Some(h) = shape.intersect(&ray, t_max) { - hit = Some(h); + if let Some(h) = obj.intersect(&r, t_max) { + hit = Some(h) } } + hit } } diff --git a/py/src/lib.rs b/py/src/lib.rs index 222533a..aad00b3 100644 --- a/py/src/lib.rs +++ b/py/src/lib.rs @@ -55,8 +55,9 @@ impl PyScene { Self(Scene::new(camera.0.clone(), contents)) } - pub fn render(&self, width: u32, height: u32, samples: u32) -> Vec { - let result = self.0.render(width, height, samples); + #[pyo3(signature = (width, height, samples, seed = 0))] + pub fn render(&self, width: u32, height: u32, samples: u32, seed: u32) -> Vec { + let result = self.0.render(width, height, samples, seed); cast_slice_box::(result).into_vec() }