From bfaf3c2cb6695e75dfc619e9101c2d9f64c02d22 Mon Sep 17 00:00:00 2001 From: wires Date: Thu, 24 Apr 2025 09:26:08 -0400 Subject: [PATCH] render normals --- core/src/camera.rs | 24 +++++++++++++++++ core/src/geometry.rs | 15 ++++++++++- core/src/geometry/shapes.rs | 19 +++++++------- core/src/lib.rs | 52 ++++++++++++++++++++++++++----------- examples/basic.py | 20 ++++++++++---- py/src/lib.rs | 30 ++++++++++++++++++--- 6 files changed, 127 insertions(+), 33 deletions(-) create mode 100644 core/src/camera.rs diff --git a/core/src/camera.rs b/core/src/camera.rs new file mode 100644 index 0000000..b98d330 --- /dev/null +++ b/core/src/camera.rs @@ -0,0 +1,24 @@ +use crate::geometry::{Point, Ray, Transform, Vector}; + +#[derive(Debug, Clone)] +pub struct Camera { + /// camera to world transform + transform: Transform, +} + +impl Camera { + pub fn new(eye: Point, center: Point, up: Vector, fov: f32) -> Self { + let f = (center - eye).length(); + let tan = (fov / 2.0).to_radians().tan(); + let transform = + Transform::look_at(eye, center, up).inverse() * Transform::scale(tan * f, tan * f, f); + + 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) + } +} diff --git a/core/src/geometry.rs b/core/src/geometry.rs index b319ac9..599474b 100644 --- a/core/src/geometry.rs +++ b/core/src/geometry.rs @@ -1,6 +1,6 @@ use std::{ fmt, - ops::{Add, Div, Mul, Sub}, + ops::{Add, Deref, Div, Mul, Sub}, }; pub mod shapes; @@ -31,6 +31,14 @@ impl From<(f32, f32, f32)> for Vector { } } +impl Deref for Vector { + type Target = glam::Vec3A; + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl Vector { pub const ZERO: Self = Self(glam::Vec3A::ZERO); pub const I: Self = Self(glam::Vec3A::X); @@ -49,6 +57,10 @@ impl Vector { self.0.length_squared() } + pub fn normalize(self) -> Vector { + Self(self.0.normalize()) + } + pub fn dot(self, other: Vector) -> f32 { self.0.dot(other.0) } @@ -199,6 +211,7 @@ impl Mul for Transform { } } +#[derive(Debug)] pub struct Ray { origin: Point, direction: Vector, diff --git a/core/src/geometry/shapes.rs b/core/src/geometry/shapes.rs index 0a237b2..03258f7 100644 --- a/core/src/geometry/shapes.rs +++ b/core/src/geometry/shapes.rs @@ -6,24 +6,24 @@ use super::{Point, Ray, Vector}; #[derive(Debug)] pub(crate) struct Hit { - point: Point, - normal: Vector, - t: f32, + pub point: Point, + pub normal: Vector, + pub t: f32, } #[enum_dispatch] pub(crate) trait Hittable { - fn intersect(&self, ray: Ray, t_max: f32) -> Option; + fn intersect(&self, ray: &Ray, t_max: f32) -> Option; } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Sphere { center: Point, radius: f32, } impl Hittable for Sphere { - fn intersect(&self, ray: Ray, t_max: f32) -> Option { + fn intersect(&self, ray: &Ray, t_max: f32) -> Option { let oc = self.center - ray.origin; let a = ray.direction.length_squared(); let h = ray.direction.dot(oc); @@ -41,19 +41,19 @@ impl Hittable for Sphere { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Plane { normal: Vector, d: f32, } impl Hittable for Plane { - fn intersect(&self, ray: Ray, t_max: f32) -> Option { + fn intersect(&self, ray: &Ray, t_max: f32) -> Option { let n_dot_r = self.normal.dot(ray.direction); (n_dot_r.abs() > f32::EPSILON) .then(|| { let n_dot_o = self.normal.dot(ray.origin.to_vector()); - -(n_dot_o + self.d) / n_dot_r + -(n_dot_o - self.d) / n_dot_r }) .filter(|t| *t < t_max && *t > 0.0) .map(|t| Hit { @@ -65,6 +65,7 @@ impl Hittable for Plane { } #[enum_dispatch(Hittable)] +#[derive(Clone)] pub enum Shape { Sphere, Plane, diff --git a/core/src/lib.rs b/core/src/lib.rs index 9c189dd..d4bec95 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -3,40 +3,62 @@ pub mod impl_bytemuck; pub mod color; +mod camera; mod geometry; +use core::f32; + +pub use camera::Camera; pub use geometry::{Point, Transform, Vector, shapes::Shape}; use color::Rgba; +use geometry::{ + Ray, + shapes::{Hit, Hittable}, +}; -pub struct Scene; +pub struct Scene { + camera: Camera, + contents: Vec, +} impl Scene { - pub fn new() -> Self { - Self + pub fn new(camera: Camera, contents: Vec) -> Self { + Self { camera, contents } } pub fn render(&self, width: usize, height: usize) -> Box<[Rgba]> { + let ray = self.camera.ray(0.0, 0.0); + dbg!(&ray); + dbg!(self.intersect(ray)); let mut data = Box::new_uninit_slice(width * height); + let w = width as f32; + let h = height as f32; + let aspect_ratio = h / w; for (i, pixel) in data.iter_mut().enumerate() { - let x = (i % width) as f32; - let y = (i / width) as f32; + let x = ((i % width) as f32 * 2.0 / w) - 1.0; + let y = (1.0 - (i / width) as f32 * 2.0 / h) * aspect_ratio; - pixel.write(Rgba::new( - x / (width - 1) as f32, - y / (height - 1) as f32, - 0.5, - 1.0, - )); + let rgb = self + .intersect(self.camera.ray(x, y)) + .map(|h| h.normal.normalize() * 0.5 + Vector::new(0.5, 0.5, 0.5)) + .unwrap_or(Vector::ZERO); + + pixel.write(Rgba::new(rgb.x, rgb.y, rgb.z, 1.0)); } unsafe { data.assume_init() } } -} -impl Default for Scene { - fn default() -> Self { - Self::new() + fn intersect(&self, ray: Ray) -> Option { + let mut hit: Option = None; + for shape in &self.contents { + 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); + } + } + hit } } diff --git a/examples/basic.py b/examples/basic.py index 46dab78..b7444ee 100755 --- a/examples/basic.py +++ b/examples/basic.py @@ -1,10 +1,20 @@ #!python +from PIL import Image +from mechthild import Scene, Shape, Camera -from mechthild import Shape - -s1 = Shape.sphere((0.0, 0.5, 0.0), 0.5) -s2 = Shape.sphere((1.5, 2.0, -1.0), 2.0) +WIDTH = 800 +HEIGHT = 600 +s1 = Shape.sphere((0.75, 1.0, 1.0), 1.0) +s2 = Shape.sphere((-1.5, 2.0, -2.0), 2.0) ground = Shape.plane((0.0, 1.0, 0.0), 0.0) -print(f"{s1}\n{s2}\n{ground}") +camera = Camera((0.0, 1.5, 5.0), (0.0, 1.0, 0.0), (0.0, 1.0, 0.0), 80.0) + +scene = Scene(camera, [s1, s2, ground]) + +render = scene.render(WIDTH, HEIGHT) +rgba = bytes([int(v * 255) for v in render]) + +image = Image.frombuffer('RGBA', (WIDTH, HEIGHT), rgba) +image.save("test.png") diff --git a/py/src/lib.rs b/py/src/lib.rs index a428282..1b084e4 100644 --- a/py/src/lib.rs +++ b/py/src/lib.rs @@ -1,8 +1,30 @@ use bytemuck::allocation::cast_slice_box; -use mechthild_core::{Scene, Shape, color::Rgba}; +use mechthild_core::{Camera, Scene, Shape, color::Rgba}; use pyo3::prelude::*; +#[pyclass(name = "Camera")] +#[derive(Clone)] +struct PyCamera(Camera); + +#[pymethods] +impl PyCamera { + #[new] + pub fn new( + eye: (f32, f32, f32), + center: (f32, f32, f32), + up: (f32, f32, f32), + fov: f32, + ) -> Self { + Self(Camera::new(eye.into(), center.into(), up.into(), fov)) + } + + pub fn __str__(&self) -> PyResult { + Ok(format!("{:#?}", self.0)) + } +} + #[pyclass(name = "Shape")] +#[derive(Clone)] struct PyShape(Shape); #[pymethods] @@ -28,8 +50,9 @@ struct PyScene(Scene); #[pymethods] impl PyScene { #[new] - pub fn new() -> Self { - Self(Scene::new()) + pub fn new(camera: &PyCamera, contents: Vec) -> Self { + let contents = contents.into_iter().map(|s| s.0).collect(); + Self(Scene::new(camera.0.clone(), contents)) } pub fn render(&self, width: usize, height: usize) -> Vec { @@ -42,6 +65,7 @@ impl PyScene { /// A Python module implemented in Rust. #[pymodule] fn mechthild(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; m.add_class::()?; m.add_class::()?; Ok(())