From b58dc7cc6b5bdeb38aafe06b57ad6d69d01390a8 Mon Sep 17 00:00:00 2001 From: "Sean P. Kelly" Date: Tue, 26 Nov 2024 00:52:35 -0800 Subject: [PATCH] feat: implement camera-space hatching based on lighting Geometry can be associated with arbitrary metadata; however, if associated with a Material, the scene can be rendered usng raydeon's lighting model. The lighting model implements diffuse and specular lighting, with hatch line subsegments removed stochastically based on the brightess at the associated point in world space. --- Cargo.toml | 1 + README.md | 18 +- pyraydeon/examples/py_sphere.py | 80 ++ pyraydeon/examples/py_sphere_expected.svg | 1 + pyraydeon/src/lib.rs | 7 +- pyraydeon/src/light.rs | 82 ++ pyraydeon/src/material.rs | 48 + pyraydeon/src/ray.rs | 9 +- pyraydeon/src/scene.rs | 88 +- pyraydeon/src/shapes/mod.rs | 29 +- pyraydeon/src/shapes/primitive.rs | 86 +- raydeon/Cargo.toml | 1 + raydeon/examples/cube.rs | 6 +- raydeon/examples/cube_expected.svg | 20 +- raydeon/examples/geom_perf.rs | 6 +- raydeon/examples/lit_cubes.rs | 95 ++ raydeon/examples/lit_cubes_expected.svg | 1503 +++++++++++++++++++++ raydeon/examples/triangles.rs | 6 +- raydeon/src/bvh.rs | 157 ++- raydeon/src/camera.rs | 46 +- raydeon/src/lib.rs | 12 +- raydeon/src/lights.rs | 148 ++ raydeon/src/material.rs | 18 + raydeon/src/path.rs | 34 +- raydeon/src/ray.rs | 15 +- raydeon/src/scene.rs | 428 +++++- raydeon/src/shapes/aacuboid.rs | 77 +- raydeon/src/shapes/plane.rs | 15 +- raydeon/src/shapes/quad.rs | 4 + raydeon/src/shapes/sphere.rs | 27 +- raydeon/src/shapes/triangle.rs | 16 +- 31 files changed, 2858 insertions(+), 225 deletions(-) create mode 100644 pyraydeon/examples/py_sphere.py create mode 100644 pyraydeon/examples/py_sphere_expected.svg create mode 100644 pyraydeon/src/light.rs create mode 100644 pyraydeon/src/material.rs create mode 100644 raydeon/examples/lit_cubes.rs create mode 100644 raydeon/examples/lit_cubes_expected.svg create mode 100644 raydeon/src/lights.rs create mode 100644 raydeon/src/material.rs diff --git a/Cargo.toml b/Cargo.toml index 7a493a3..0eb7026 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ euclid = "0.22" float-cmp = "0.5" log = "0.4" pyo3 = "0.22" +rand = "0.8" rayon = "1.2" numpy = "0.22" svg = "0.18" diff --git a/README.md b/README.md index fcdc4c4..8d28d9e 100644 --- a/README.md +++ b/README.md @@ -25,12 +25,12 @@ use raydeon::shapes::AxisAlignedCuboid; use raydeon::{Camera, Scene, WPoint3, WVec3}; use std::sync::Arc; -env_logger::Builder::from_default_env() - .format_timestamp_nanos() - .init(); - fn main() { - let scene = Scene::new(vec![Arc::new(AxisAlignedCuboid::new( + env_logger::Builder::from_default_env() + .format_timestamp_nanos() + .init(); + + let scene = Scene::new().with_geometry(vec![Arc::new(AxisAlignedCuboid::new( WVec3::new(-1.0, -1.0, -1.0), WVec3::new(1.0, 1.0, 1.0), ))]); @@ -40,12 +40,14 @@ fn main() { let up = WVec3::new(0.0, 0.0, 1.0); let fovy = 50.0; - let width = 1024.0; - let height = 1024.0; + let width = 1024; + let height = 1024; let znear = 0.1; let zfar = 10.0; - let camera = Camera::new().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar); + let camera = Camera::new() + .look_at(eye, focus, up) + .perspective(fovy, width, height, znear, zfar); let paths = scene.attach_camera(camera).render(); diff --git a/pyraydeon/examples/py_sphere.py b/pyraydeon/examples/py_sphere.py new file mode 100644 index 0000000..7f8e5a5 --- /dev/null +++ b/pyraydeon/examples/py_sphere.py @@ -0,0 +1,80 @@ +import svg + +from pyraydeon import ( + Camera, + Point3, + Scene, + Sphere, + Vec3, + Geometry, + Material, + PointLight, +) + + +class PySphere(Geometry): + def __init__(self, point, radius, material=None): + if material is not None: + self._material = material + + self.sphere = Sphere(point, radius) + + @property + def material(self): + return self._material + + def collision_geometry(self): + return [self.sphere] + + def paths(self, cam): + return [] + + +scene = Scene( + geometry=[PySphere(Point3(0, 0, 0), 1.0, Material(3.0, 3.0, 3))], + lights=[PointLight((2, 0, 5), 1.0, 2.0, 0.05, 0.2, 0.09)], +) + + +eye = Point3(0, 0, 5) +focus = Vec3(0, 0, 0) +up = Vec3(0, 1, 0) + +fovy = 50.0 +width = 1024 +height = 1024 +znear = 0.1 +zfar = 10.0 + +cam = Camera().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar) + +paths = scene.render_with_lighting(cam, seed=5) + +canvas = svg.SVG( + width="8in", + height="8in", + viewBox="0 0 1024 1024", +) +backing_rect = svg.Rect( + x=0, + y=0, + width="100%", + height="100%", + fill="white", +) +svg_lines = [ + svg.Line( + x1=f"{path.p1[0]}", + y1=f"{path.p1[1]}", + x2=f"{path.p2[0]}", + y2=f"{path.p2[1]}", + stroke_width="0.7mm", + stroke="black", + ) + for path in paths +] +line_group = svg.G(transform=f"translate(0, {height}) scale(1, -1)", elements=svg_lines) +canvas.elements = [backing_rect, line_group] + + +print(canvas) diff --git a/pyraydeon/examples/py_sphere_expected.svg b/pyraydeon/examples/py_sphere_expected.svg new file mode 100644 index 0000000..0598e58 --- /dev/null +++ b/pyraydeon/examples/py_sphere_expected.svg @@ -0,0 +1 @@ + diff --git a/pyraydeon/src/lib.rs b/pyraydeon/src/lib.rs index 7b39792..991bb6c 100644 --- a/pyraydeon/src/lib.rs +++ b/pyraydeon/src/lib.rs @@ -1,8 +1,5 @@ use pyo3::prelude::*; -#[derive(Copy, Clone, Debug, Default)] -struct Material; - macro_rules! pywrap { ($name:ident, $wraps:ty) => { #[derive(Debug, Clone, Copy)] @@ -25,7 +22,9 @@ macro_rules! pywrap { }; } +mod light; mod linear; +mod material; mod ray; mod scene; mod shapes; @@ -37,5 +36,7 @@ fn pyraydeon(m: &Bound<'_, PyModule>) -> PyResult<()> { crate::shapes::register(m)?; crate::scene::register(m)?; crate::ray::register(m)?; + crate::material::register(m)?; + crate::light::register(m)?; Ok(()) } diff --git a/pyraydeon/src/light.rs b/pyraydeon/src/light.rs new file mode 100644 index 0000000..305dcf0 --- /dev/null +++ b/pyraydeon/src/light.rs @@ -0,0 +1,82 @@ +use crate::linear::Point3; +use pyo3::prelude::*; + +pywrap!(PointLight, raydeon::lights::PointLight); + +#[pymethods] +impl PointLight { + #[new] + #[pyo3(signature = ( + position, + intensity=0.0, + specular_intensity=0.0, + constant_attenuation=0.0, + linear_attenuation=0.0, + quadratic_attenuation=0.0 + ))] + fn new( + position: &Bound<'_, PyAny>, + intensity: f64, + specular_intensity: f64, + constant_attenuation: f64, + linear_attenuation: f64, + quadratic_attenuation: f64, + ) -> PyResult { + let position = Point3::try_from(position)?; + Ok(raydeon::lights::PointLight::new( + intensity, + specular_intensity, + position.0.cast_unit(), + constant_attenuation, + linear_attenuation, + quadratic_attenuation, + ) + .into()) + } + + #[getter] + fn intensity(&self) -> f64 { + self.0.intensity() + } + + #[getter] + fn specular(&self) -> f64 { + self.0.specular() + } + + #[getter] + fn position(&self) -> Point3 { + self.0.position().cast_unit().into() + } + + #[getter] + fn constant_attenuation(&self) -> f64 { + self.0.constant_attenuation() + } + + #[getter] + fn linear_attenuation(&self) -> f64 { + self.0.linear_attenuation() + } + + #[getter] + fn quadratic_attenuation(&self) -> f64 { + self.0.quadratic_attenuation() + } + + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { + let class_name = slf.get_type().qualname()?; + Ok(format!("{}<{:#?}>", class_name, slf.borrow().0)) + } +} + +impl Default for PointLight { + fn default() -> Self { + raydeon::lights::PointLight::default().into() + } +} + +pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/pyraydeon/src/material.rs b/pyraydeon/src/material.rs new file mode 100644 index 0000000..e647e1b --- /dev/null +++ b/pyraydeon/src/material.rs @@ -0,0 +1,48 @@ +use pyo3::prelude::*; + +pywrap!(Material, raydeon::material::Material); + +#[pymethods] +impl Material { + #[new] + #[pyo3(signature = (diffuse=0.0, specular=0.0, shininess=0.0, tag=0))] + fn new(diffuse: f64, specular: f64, shininess: f64, tag: usize) -> PyResult { + Ok(raydeon::material::Material::new(diffuse, specular, shininess, tag).into()) + } + + #[getter] + fn diffuse(&self) -> f64 { + self.diffuse + } + + #[getter] + fn specular(&self) -> f64 { + self.specular + } + + #[getter] + fn shininess(&self) -> f64 { + self.shininess + } + + #[getter] + fn tag(&self) -> usize { + self.tag + } + + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { + let class_name = slf.get_type().qualname()?; + Ok(format!("{}<{:#?}>", class_name, slf.borrow().0)) + } +} + +impl Default for Material { + fn default() -> Self { + raydeon::material::Material::default().into() + } +} + +pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + Ok(()) +} diff --git a/pyraydeon/src/ray.rs b/pyraydeon/src/ray.rs index 2a0a1b9..297c141 100644 --- a/pyraydeon/src/ray.rs +++ b/pyraydeon/src/ray.rs @@ -36,9 +36,14 @@ pywrap!(HitData, raydeon::HitData); #[pymethods] impl HitData { #[new] - fn new(hit_point: PyReadonlyArray1, dist_to: f64) -> PyResult { + fn new( + hit_point: PyReadonlyArray1, + dist_to: f64, + normal: PyReadonlyArray1, + ) -> PyResult { let hit_point = Point3::try_from(hit_point)?; - Ok(raydeon::HitData::new(hit_point.0.cast_unit(), dist_to).into()) + let normal = Vec3::try_from(normal)?; + Ok(raydeon::HitData::new(hit_point.0.cast_unit(), dist_to, normal.0.cast_unit()).into()) } #[getter] diff --git a/pyraydeon/src/scene.rs b/pyraydeon/src/scene.rs index 4298947..6f73d0c 100644 --- a/pyraydeon/src/scene.rs +++ b/pyraydeon/src/scene.rs @@ -4,9 +4,10 @@ use numpy::{Ix1, PyArray, PyReadonlyArray1}; use pyo3::prelude::*; use raydeon::WorldSpace; +use crate::light::PointLight; use crate::linear::{ArbitrarySpace, Point2, Point3, Vec3}; +use crate::material::Material; use crate::shapes::Geometry; -use crate::Material; pywrap!(Camera, raydeon::Camera); @@ -32,7 +33,7 @@ impl Camera { .into()) } - fn perspective(&self, fovy: f64, width: f64, height: f64, znear: f64, zfar: f64) -> Camera { + fn perspective(&self, fovy: f64, width: usize, height: usize, znear: f64, zfar: f64) -> Camera { self.0.perspective(fovy, width, height, znear, zfar).into() } @@ -44,23 +45,44 @@ impl Camera { #[pyclass(frozen)] pub(crate) struct Scene { - scene: Arc>, + scene: Arc< + raydeon::Scene< + raydeon::scene::SceneGeometry, + raydeon::scene::SceneLighting, + >, + >, } #[pymethods] impl Scene { #[new] - fn new(py: Python, geometry: Vec) -> PyResult { - let geometry: Vec>> = geometry + #[pyo3(signature = (geometry=None, lights=None))] + fn new( + py: Python, + geometry: Option>, + lights: Option>, + ) -> PyResult { + let geometry = geometry.unwrap_or_default(); + let lights = lights.unwrap_or_default(); + let geometry: Vec>> = + geometry + .into_iter() + .map(|g| { + let geom: Py = g.extract(py)?; + let raydeon_shape = geom.borrow(py); + let raydeon_shape = raydeon_shape.geometry(g); + Ok(raydeon_shape) + }) + .collect::>()?; + let lights: Vec> = lights .into_iter() - .map(|g| { - let geom: Py = g.extract(py)?; - let raydeon_shape = geom.borrow(py); - let raydeon_shape = raydeon_shape.geometry(g); - Ok(raydeon_shape) - }) - .collect::>()?; - let scene = Arc::new(raydeon::Scene::new(geometry)); + .map(|l| Arc::new(l.0) as Arc) + .collect(); + let scene = Arc::new( + raydeon::Scene::new() + .with_geometry(geometry) + .with_lighting(lights), + ); Ok(Self { scene }) } @@ -74,6 +96,30 @@ impl Scene { }) } + #[pyo3(signature = (camera, seed=None))] + fn render_with_lighting( + &self, + py: Python, + camera: &Camera, + seed: Option, + ) -> Vec { + py.allow_threads(|| { + let cam = self.scene.attach_camera(camera.0); + let cam = if let Some(seed) = seed { + cam.with_seed(seed) + } else { + cam + }; + let render_result = cam.render_with_lighting(); + render_result + .geometry_paths + .into_iter() + .chain(render_result.hatch_paths) + .map(|ls| ls.cast_unit().into()) + .collect() + }) + } + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { let class_name = slf.get_type().qualname()?; Ok(format!("{}<{:?}>", class_name, slf.borrow().scene)) @@ -107,15 +153,25 @@ impl LineSegment2D { } } -pywrap!(LineSegment3D, raydeon::path::LineSegment3D); +pywrap!(LineSegment3D, raydeon::path::LineSegment3D); #[pymethods] impl LineSegment3D { #[new] - fn new(p1: &Bound<'_, PyAny>, p2: &Bound<'_, PyAny>) -> PyResult { + #[pyo3(signature = (p1, p2, material=None))] + fn new( + p1: &Bound<'_, PyAny>, + p2: &Bound<'_, PyAny>, + material: Option, + ) -> PyResult { let p1 = Point3::try_from(p1)?; let p2 = Point3::try_from(p2)?; - Ok(raydeon::path::LineSegment3D::tagged(p1.cast_unit(), p2.cast_unit(), Material).into()) + Ok(raydeon::path::LineSegment3D::tagged( + p1.cast_unit(), + p2.cast_unit(), + material.unwrap_or_default().0, + ) + .into()) } #[getter] diff --git a/pyraydeon/src/shapes/mod.rs b/pyraydeon/src/shapes/mod.rs index 1c030e7..8d18d08 100644 --- a/pyraydeon/src/shapes/mod.rs +++ b/pyraydeon/src/shapes/mod.rs @@ -1,4 +1,4 @@ -use primitive::{Plane, Quad}; +use primitive::{Plane, Quad, Sphere}; use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; use raydeon::WorldSpace; @@ -8,13 +8,15 @@ mod primitive; pub(crate) use primitive::{AxisAlignedCuboid, Tri}; +use crate::material::Material; use crate::ray::{HitData, Ray, AABB3}; use crate::scene::{Camera, LineSegment3D}; -use crate::Material; + +type RMaterial = raydeon::material::Material; #[derive(Debug)] enum InnerGeometry { - Native(Arc>), + Native(Arc>), Py, } @@ -25,7 +27,7 @@ pub(crate) struct Geometry { } impl Geometry { - pub(crate) fn native(geom: Arc>) -> Self { + pub(crate) fn native(geom: Arc>) -> Self { let geom = InnerGeometry::Native(geom); Self { geom } } @@ -35,7 +37,7 @@ impl Geometry { Self { geom } } - pub(crate) fn geometry(&self, obj: PyObject) -> Arc> { + pub(crate) fn geometry(&self, obj: PyObject) -> Arc> { match &self.geom { InnerGeometry::Native(ref geom) => Arc::clone(geom), InnerGeometry::Py => Arc::new(PythonGeometry::new(obj, PythonGeometryKind::Draw)), @@ -174,7 +176,7 @@ impl PythonGeometry { } } -impl raydeon::Shape for PythonGeometry { +impl raydeon::Shape for PythonGeometry { fn collision_geometry(&self) -> Option>>> { let collision_geometry: Option<_> = Python::with_gil(|py| { let inner = self.slf.bind(py); @@ -201,7 +203,7 @@ impl raydeon::Shape for PythonGeometry { fn paths( &self, cam: &raydeon::Camera, - ) -> Vec> { + ) -> Vec> { let segments: Option<_> = Python::with_gil(|py| { let inner = self.slf.bind(py); let cam = Camera::from(*cam); @@ -220,6 +222,18 @@ impl raydeon::Shape for PythonGeometry { }); segments.unwrap_or_default() } + + fn metadata(&self) -> raydeon::material::Material { + let material: Option = Python::with_gil(|py| { + let inner = self.slf.bind(py); + let attr_value = inner.getattr("material").ok()?; + + let material: Material = attr_value.extract().ok()?; + + Some(material) + }); + material.unwrap_or_default().0 + } } impl raydeon::CollisionGeometry for PythonGeometry { @@ -257,6 +271,7 @@ pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_class::()?; m.add_class::()?; Ok(()) diff --git a/pyraydeon/src/shapes/primitive.rs b/pyraydeon/src/shapes/primitive.rs index 4a74f3a..f2d1108 100644 --- a/pyraydeon/src/shapes/primitive.rs +++ b/pyraydeon/src/shapes/primitive.rs @@ -1,25 +1,27 @@ use super::{CollisionGeometry, Geometry}; use crate::linear::{Point3, Vec3}; -use crate::Material; +use crate::material::Material; use numpy::{PyArrayLike1, PyArrayLike2}; use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; use raydeon::WorldSpace; use std::sync::Arc; +type RMaterial = raydeon::material::Material; + #[pyclass(frozen, extends=Geometry, subclass)] -pub(crate) struct AxisAlignedCuboid(pub(crate) Arc>); +pub(crate) struct AxisAlignedCuboid(pub(crate) Arc>); impl ::std::ops::Deref for AxisAlignedCuboid { - type Target = Arc>; + type Target = Arc>; fn deref(&self) -> &Self::Target { &self.0 } } -impl From>> for AxisAlignedCuboid { - fn from(value: Arc>) -> Self { +impl From>> for AxisAlignedCuboid { + fn from(value: Arc>) -> Self { Self(value) } } @@ -27,36 +29,41 @@ impl From>> for AxisAlignedCubo #[pymethods] impl AxisAlignedCuboid { #[new] - #[pyo3(signature = (min, max))] - fn new(min: &Bound<'_, PyAny>, max: &Bound<'_, PyAny>) -> PyResult<(Self, Geometry)> { + #[pyo3(signature = (min, max, material=None))] + fn new( + min: &Bound<'_, PyAny>, + max: &Bound<'_, PyAny>, + material: Option, + ) -> PyResult<(Self, Geometry)> { let min: Vec3 = min.try_into()?; let max: Vec3 = max.try_into()?; let shape = Arc::new(raydeon::shapes::AxisAlignedCuboid::tagged( min.cast_unit(), max.cast_unit(), - Material, + material.unwrap_or_default().0, )); let geom = - Geometry::native(Arc::clone(&shape) as Arc>); + Geometry::native(Arc::clone(&shape) + as Arc>); Ok((Self(shape), geom)) } } #[pyclass(frozen, extends=Geometry, subclass)] -pub(crate) struct Tri(pub(crate) Arc>); +pub(crate) struct Tri(pub(crate) Arc>); impl ::std::ops::Deref for Tri { - type Target = Arc>; + type Target = Arc>; fn deref(&self) -> &Self::Target { &self.0 } } -impl From>> for Tri { - fn from(value: Arc>) -> Self { +impl From>> for Tri { + fn from(value: Arc>) -> Self { Self(value) } } @@ -64,11 +71,12 @@ impl From>> for Tri { #[pymethods] impl Tri { #[new] - #[pyo3(signature = (p1, p2, p3))] + #[pyo3(signature = (p1, p2, p3, material=None))] fn new( p1: &Bound<'_, PyAny>, p2: &Bound<'_, PyAny>, p3: &Bound<'_, PyAny>, + material: Option, ) -> PyResult<(Self, Geometry)> { let p1: Point3 = p1.try_into()?; let p2: Point3 = p2.try_into()?; @@ -78,10 +86,10 @@ impl Tri { p1.cast_unit(), p2.cast_unit(), p3.cast_unit(), - Material, + material.unwrap_or_default().0, )); let geom = - Geometry::native(Arc::clone(&shape) as Arc>); + Geometry::native(Arc::clone(&shape) as Arc>); Ok((Self(shape), geom)) } } @@ -124,19 +132,50 @@ impl Plane { } } +#[pyclass(frozen, extends=CollisionGeometry, subclass)] +pub(crate) struct Sphere(pub(crate) Arc); + +impl ::std::ops::Deref for Sphere { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for Sphere { + fn from(value: Arc) -> Self { + Self(value) + } +} + +#[pymethods] +impl Sphere { + #[new] + fn new(point: &Bound<'_, PyAny>, radius: f64) -> PyResult<(Self, CollisionGeometry)> { + let point: Point3 = point.try_into()?; + + let shape = Arc::new(raydeon::shapes::Sphere::new(point.0.cast_unit(), radius)); + let geom = CollisionGeometry::native( + Arc::clone(&shape) as Arc> + ); + Ok((Self(shape), geom)) + } +} + #[pyclass(frozen, extends=Geometry, subclass)] -pub(crate) struct Quad(pub(crate) Arc>); +pub(crate) struct Quad(pub(crate) Arc>); impl ::std::ops::Deref for Quad { - type Target = Arc>; + type Target = Arc>; fn deref(&self) -> &Self::Target { &self.0 } } -impl From>> for Quad { - fn from(value: Arc>) -> Self { +impl From>> for Quad { + fn from(value: Arc>) -> Self { Self(value) } } @@ -144,11 +183,12 @@ impl From>> for Quad { #[pymethods] impl Quad { #[new] - #[pyo3(signature = (origin, basis, dims))] + #[pyo3(signature = (origin, basis, dims, material=None))] fn new( origin: &Bound<'_, PyAny>, basis: PyArrayLike2<'_, f64>, dims: PyArrayLike1<'_, f64>, + material: Option, ) -> PyResult<(Self, Geometry)> { let origin: Point3 = origin.try_into()?; let basis = basis @@ -185,10 +225,10 @@ impl Quad { origin.0.cast_unit(), basis, dims, - Material, + material.unwrap_or_default().0, )); let geom = - Geometry::native(Arc::clone(&shape) as Arc>); + Geometry::native(Arc::clone(&shape) as Arc>); Ok((Self(shape), geom)) } } diff --git a/raydeon/Cargo.toml b/raydeon/Cargo.toml index 6a51e49..ada7f5a 100644 --- a/raydeon/Cargo.toml +++ b/raydeon/Cargo.toml @@ -9,6 +9,7 @@ cgmath.workspace = true collision.workspace = true euclid.workspace = true log.workspace = true +rand = { workspace = true, features = ["std_rng"] } rayon.workspace = true tracing = { workspace = true, features = ["log"] } diff --git a/raydeon/examples/cube.rs b/raydeon/examples/cube.rs index 274639f..8c90f1e 100644 --- a/raydeon/examples/cube.rs +++ b/raydeon/examples/cube.rs @@ -7,7 +7,7 @@ fn main() { .format_timestamp_nanos() .init(); - let scene = Scene::new(vec![Arc::new(AxisAlignedCuboid::new( + let scene = Scene::new().with_geometry(vec![Arc::new(AxisAlignedCuboid::new( WVec3::new(-1.0, -1.0, -1.0), WVec3::new(1.0, 1.0, 1.0), ))]); @@ -17,8 +17,8 @@ fn main() { let up = WVec3::new(0.0, 0.0, 1.0); let fovy = 50.0; - let width = 1024.0; - let height = 1024.0; + let width = 1024; + let height = 1024; let znear = 0.1; let zfar = 10.0; diff --git a/raydeon/examples/cube_expected.svg b/raydeon/examples/cube_expected.svg index a3f822b..5605610 100644 --- a/raydeon/examples/cube_expected.svg +++ b/raydeon/examples/cube_expected.svg @@ -1,14 +1,14 @@ - - - - - - - - - + + + + + + + + + - \ No newline at end of file + diff --git a/raydeon/examples/geom_perf.rs b/raydeon/examples/geom_perf.rs index 92c7c9a..c3ef601 100644 --- a/raydeon/examples/geom_perf.rs +++ b/raydeon/examples/geom_perf.rs @@ -15,12 +15,12 @@ fn main() { let up = look.cross(WVec3::new(0.0, 1.0, 0.0)).cross(look); let fovy = 50.0; - let width = 1024.0; - let height = 1024.0; + let width = 1024; + let height = 1024; let znear = 0.1; let zfar = 100.0; - let scene = Scene::new(generate_scene()); + let scene = Scene::new().with_geometry(generate_scene()); let camera = Camera::new() .look_at(eye, focus, up) diff --git a/raydeon/examples/lit_cubes.rs b/raydeon/examples/lit_cubes.rs new file mode 100644 index 0000000..a33c42a --- /dev/null +++ b/raydeon/examples/lit_cubes.rs @@ -0,0 +1,95 @@ +use raydeon::lights::PointLight; +use raydeon::material::Material; +use raydeon::shapes::AxisAlignedCuboid; +use raydeon::{Camera, Scene, WPoint3, WVec3}; +use std::sync::Arc; + +fn main() { + env_logger::Builder::from_default_env() + .format_timestamp_nanos() + .init(); + + let scene = Scene::new() + .with_geometry(vec![ + Arc::new(AxisAlignedCuboid::tagged( + (-1.0, -1.0, -1.0), + (1.0, 1.0, 1.0), + Material::new(3.0, 2.0, 2.0, 0), + )), + Arc::new(AxisAlignedCuboid::tagged( + (1.8, -1.0, -1.0), + (3.8, 1.0, 1.0), + Material::new(2.0, 2.0, 2.0, 0), + )), + Arc::new(AxisAlignedCuboid::tagged( + (-1.4, 1.8, -1.0), + (0.6, 3.8, 1.0), + Material::new(3.0, 2.0, 2.0, 0), + )), + ]) + .with_lighting(vec![Arc::new(PointLight::new( + 20.0, + 100.0, + (5.5, 12.0, 7.3), + 0.0, + 0.09, + 0.23, + ))]); + + let eye = WPoint3::new(8.0, 6.0, 4.0); + let focus = WVec3::new(0.0, 0.0, 0.0); + let up = WVec3::new(0.0, 0.0, 1.0); + + let fovy = 50.0; + let width = 1024; + let height = 1024; + let znear = 0.1; + let zfar = 20.0; + + let camera = Camera::new() + .look_at(eye, focus, up) + .perspective(fovy, width, height, znear, zfar); + + let render_result = scene + .attach_camera(camera) + .with_seed(0) + .render_with_lighting(); + + let mut svg_doc = svg::Document::new() + .set("width", "8in") + .set("height", "8in") + .set("viewBox", (0, 0, width, height)) + .set("stroke-width", "0.7mm") + .set("stroke", "black") + .set("fill", "none") + .add( + svg::node::element::Rectangle::new() + .set("x", 0) + .set("y", 0) + .set("width", "100%") + .set("height", "100%") + .set("fill", "white"), + ); + + // We have to flip the y-axis in our svg... + let mut item_group = svg::node::element::Group::new() + .set("transform", format!("translate(0, {}) scale(1,-1)", height)); + + for path in render_result + .geometry_paths + .iter() + .chain(render_result.hatch_paths.iter()) + { + let (p1, p2) = (path.p1, path.p2); + item_group = item_group.add( + svg::node::element::Line::new() + .set("x1", p1.x) + .set("y1", p1.y) + .set("x2", p2.x) + .set("y2", p2.y), + ); + } + + svg_doc = svg_doc.add(item_group); + println!("{}", svg_doc); +} diff --git a/raydeon/examples/lit_cubes_expected.svg b/raydeon/examples/lit_cubes_expected.svg new file mode 100644 index 0000000..fdf4d07 --- /dev/null +++ b/raydeon/examples/lit_cubes_expected.svg @@ -0,0 +1,1503 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/raydeon/examples/triangles.rs b/raydeon/examples/triangles.rs index eaf0518..7b7e4f7 100644 --- a/raydeon/examples/triangles.rs +++ b/raydeon/examples/triangles.rs @@ -8,7 +8,7 @@ fn main() { .format_timestamp_nanos() .init(); - let scene = Scene::new(vec![ + let scene = Scene::new().with_geometry(vec![ Arc::new(Triangle::new( WPoint3::new(0.0, 0.0, 0.0), WPoint3::new(0.0, 0.0, 1.0), @@ -28,8 +28,8 @@ fn main() { let up = look.cross(WVec3::new(0.0, 0.0, 1.0)).cross(look); let fovy = 50.0; - let width = 1024.0; - let height = 1024.0; + let width = 1024; + let height = 1024; let znear = 0.1; let zfar = 10.0; diff --git a/raydeon/src/bvh.rs b/raydeon/src/bvh.rs index b8e1611..92b0b1f 100644 --- a/raydeon/src/bvh.rs +++ b/raydeon/src/bvh.rs @@ -1,9 +1,24 @@ -use crate::{CollisionGeometry, HitData, Ray, WorldSpace, AABB3}; +use crate::{CollisionGeometry, HitData, PathMeta, Ray, Shape, WorldSpace, AABB3}; use euclid::Point3D; use rayon::prelude::*; use std::sync::Arc; use tracing::info; +#[derive(Debug, Clone)] +pub(crate) struct Collidable { + shape: Arc>, + collision: Arc>, +} + +impl Collidable { + pub(crate) fn new( + shape: Arc>, + collision: Arc>, + ) -> Self { + Self { shape, collision } + } +} + #[derive(Debug, Clone, Copy)] pub(crate) enum Axis { X, @@ -12,39 +27,41 @@ pub(crate) enum Axis { } #[derive(Debug)] -pub(crate) struct BVHTree +pub(crate) struct BVHTree where Space: Copy + Send + Sync + Sized + std::fmt::Debug + 'static, + P: PathMeta, { aabb: AABB3, - root: Option>, - unbounded: Vec>>, + root: Option>, + unbounded: Vec>, } -impl BVHTree +impl BVHTree where Space: Copy + Send + Sync + Sized + std::fmt::Debug + 'static, + P: PathMeta, { - pub(crate) fn new(shapes: &[Arc>]) -> Self { + pub(crate) fn new(collidables: &[Collidable]) -> Self { info!( "Creating Bounded Volume Hierarchy for {} shapes", - shapes.len() + collidables.len() ); - let mut bounded = Vec::with_capacity(shapes.len()); - let mut unbounded = Vec::with_capacity(shapes.len()); + let mut bounded = Vec::with_capacity(collidables.len()); + let mut unbounded = Vec::with_capacity(collidables.len()); - for shape in shapes.iter() { - let aabb = shape.bounding_box(); + for collidable in collidables.iter() { + let aabb = collidable.collision.bounding_box(); - let shape = Arc::clone(shape); + let collidable = collidable.clone(); match aabb { - Some(aabb) => bounded.push(Arc::new(BoundedShape { aabb, shape })), - None => unbounded.push(shape), + Some(aabb) => bounded.push(Arc::new(BoundedShape { aabb, collidable })), + None => unbounded.push(collidable), } } let aabb = bounding_box_for_shapes(&bounded); - let root = (!shapes.is_empty()).then(|| { + let root = (!collidables.is_empty()).then(|| { let (root, depth) = Node::new(bounded); info!("Created Bounded Volume Hierarchy with depth {}", depth); root @@ -57,18 +74,21 @@ where } } -impl BVHTree { - pub(crate) fn intersects(&self, ray: Ray) -> Option { +impl BVHTree { + pub(crate) fn intersects(&self, ray: Ray) -> Option<(HitData, Arc>)> { vec![ self.intersects_bounded_volume(ray), self.intersects_unbounded_volume(ray), ] .into_iter() .flatten() - .min_by(|hit1, hit2| hit1.dist_to.partial_cmp(&hit2.dist_to).unwrap()) + .min_by(|(hit1, _), (hit2, _)| hit1.dist_to.partial_cmp(&hit2.dist_to).unwrap()) } - fn intersects_bounded_volume(&self, ray: Ray) -> Option { + fn intersects_bounded_volume( + &self, + ray: Ray, + ) -> Option<(HitData, Arc>)> { let (tmin, tmax) = bounding_box_intersects(self.aabb, ray); if tmax < tmin || tmax <= 0.0 { None @@ -79,36 +99,51 @@ impl BVHTree { } } - fn intersects_unbounded_volume(&self, ray: Ray) -> Option { + fn intersects_unbounded_volume( + &self, + ray: Ray, + ) -> Option<(HitData, Arc>)> { self.unbounded .iter() - .filter_map(|geom| geom.hit_by(&ray)) - .min_by(|hit1, hit2| hit1.dist_to.partial_cmp(&hit2.dist_to).unwrap()) + .filter_map(|collidable| { + collidable + .collision + .hit_by(&ray) + .map(|hit_point| (hit_point, collidable.shape.clone())) + }) + .min_by(|(hit1, _), (hit2, _)| hit1.dist_to.partial_cmp(&hit2.dist_to).unwrap()) } } #[derive(Debug)] -enum Node +enum Node where Space: Copy + Send + Sync + Sized + std::fmt::Debug + 'static, + P: PathMeta, { - Parent(ParentNode), - Leaf(LeafNode), + Parent(ParentNode), + Leaf(LeafNode), } #[derive(Debug)] -struct ParentNode +struct ParentNode where Space: Copy + Send + Sync + Sized + std::fmt::Debug + 'static, + P: PathMeta, { axis: Axis, point: f64, - left: Box>, - right: Box>, + left: Box>, + right: Box>, } -impl ParentNode { - fn intersects(&self, ray: Ray, tmin: f64, tmax: f64) -> Option { +impl ParentNode { + fn intersects( + &self, + ray: Ray, + tmin: f64, + tmax: f64, + ) -> Option<(HitData, Arc>)> { let rp: f64; let rd: f64; match self.axis { @@ -140,14 +175,14 @@ impl ParentNode { } else { let h1 = first.intersects(ray, tmin, tsplit); - if h1.is_some_and(|hit| hit.dist_to <= tsplit) { + if h1.as_ref().is_some_and(|(hit, _)| hit.dist_to <= tsplit) { return h1; } - let h1t = h1.map(|hit| hit.dist_to).unwrap_or(f64::MAX); + let h1t = h1.as_ref().map(|(hit, _)| hit.dist_to).unwrap_or(f64::MAX); let h2 = second.intersects(ray, tsplit, f64::min(tmax, h1t)); - let h2t = h2.map(|hit| hit.dist_to).unwrap_or(f64::MAX); + let h2t = h2.as_ref().map(|(hit, _)| hit.dist_to).unwrap_or(f64::MAX); if h1t < h2t { h1 @@ -159,20 +194,30 @@ impl ParentNode { } #[derive(Debug)] -struct LeafNode +struct LeafNode where Space: Copy + Send + Sync + Sized + std::fmt::Debug + 'static, + P: PathMeta, { - shapes: Vec>>, + shapes: Vec>>, } -type PartitionedSegments = (Vec>>, Vec>>); +type PartitionedSegments = ( + Vec>>, + Vec>>, +); -impl LeafNode +impl LeafNode where Space: Copy + Send + Sync + Sized + std::fmt::Debug + 'static, + P: PathMeta, { - fn partition(&self, best: u64, best_axis: Axis, best_point: f64) -> PartitionedSegments { + fn partition( + &self, + best: u64, + best_axis: Axis, + best_point: f64, + ) -> PartitionedSegments { let mut left = Vec::with_capacity(best as usize); let mut right = Vec::with_capacity(best as usize); for shape in &self.shapes { @@ -207,20 +252,27 @@ where } } -impl LeafNode { - fn intersects(&self, ray: Ray) -> Option { +impl LeafNode { + fn intersects(&self, ray: Ray) -> Option<(HitData, Arc>)> { self.shapes .iter() - .filter_map(|geom| geom.shape.hit_by(&ray)) - .min_by(|hit1, hit2| hit1.dist_to.partial_cmp(&hit2.dist_to).unwrap()) + .filter_map(|shape| { + shape + .collidable + .collision + .hit_by(&ray) + .map(|hitpoint| (hitpoint, shape.collidable.shape.clone())) + }) + .min_by(|(hit1, _), (hit2, _)| hit1.dist_to.partial_cmp(&hit2.dist_to).unwrap()) } } -impl Node +impl Node where Space: Copy + Send + Sync + Sized + std::fmt::Debug + 'static, + P: PathMeta, { - fn new(shapes: Vec>>) -> (Self, usize) { + fn new(shapes: Vec>>) -> (Self, usize) { let mut node = Self::Leaf(LeafNode { shapes }); let depth = node.split(); (node, depth + 1) @@ -293,8 +345,13 @@ where } } -impl Node { - fn intersects(&self, ray: Ray, tmin: f64, tmax: f64) -> Option { +impl Node { + fn intersects( + &self, + ray: Ray, + tmin: f64, + tmax: f64, + ) -> Option<(HitData, Arc>)> { match self { Self::Parent(parent_node) => parent_node.intersects(ray, tmin, tmax), Self::Leaf(leaf_node) => leaf_node.intersects(ray), @@ -303,17 +360,19 @@ impl Node { } #[derive(Debug)] -struct BoundedShape +struct BoundedShape where Space: Copy + Send + Sync + Sized + std::fmt::Debug + 'static, + P: PathMeta, { - shape: Arc>, + collidable: Collidable, aabb: AABB3, } -fn bounding_box_for_shapes(shapes: &[Arc>]) -> AABB3 +fn bounding_box_for_shapes(shapes: &[Arc>]) -> AABB3 where Space: Copy + Send + Sync + Sized + std::fmt::Debug + 'static, + P: PathMeta, { let aabb = AABB3::new(Point3D::splat(f64::MAX), Point3D::splat(f64::MIN)); let bounding_boxes = shapes.iter().map(|shape| shape.aabb).collect::>(); diff --git a/raydeon/src/camera.rs b/raydeon/src/camera.rs index 45d9ab3..9b793f8 100644 --- a/raydeon/src/camera.rs +++ b/raydeon/src/camera.rs @@ -1,4 +1,4 @@ -use euclid::Transform3D; +use euclid::{Point3D, Transform3D}; use path::SlicedSegment3D; use crate::*; @@ -55,8 +55,8 @@ impl Observation { #[derive(Debug, Copy, Clone)] pub struct Perspective { pub fovy: f64, - pub width: f64, - pub height: f64, + pub width: usize, + pub height: usize, pub aspect: f64, pub znear: f64, pub zfar: f64, @@ -64,7 +64,7 @@ pub struct Perspective { impl Default for Perspective { fn default() -> Self { - Self::new(45.0, 1920.0, 1080.0, 0.1, 100.0) + Self::new(45.0, 1920, 1080, 0.1, 100.0) } } @@ -73,8 +73,8 @@ impl Default for Perspective { pub struct NoPerspective; impl Perspective { - pub fn new(fovy: f64, width: f64, height: f64, znear: f64, zfar: f64) -> Self { - let aspect = width / height; + pub fn new(fovy: f64, width: usize, height: usize, znear: f64, zfar: f64) -> Self { + let aspect = width as f64 / height as f64; Self { fovy, width, @@ -128,8 +128,8 @@ impl Camera { pub fn perspective( self, fovy: f64, - width: f64, - height: f64, + width: usize, + height: usize, znear: f64, zfar: f64, ) -> Camera { @@ -158,9 +158,9 @@ impl Camera { self.canvas_transformation() .then_translate(Vec3::new(1.0, 1.0, 0.0)) .then_scale( - self.perspective.width / 2.0, - self.perspective.height / 2.0, - 0.0, + self.perspective.width as f64 / 2.0, + self.perspective.height as f64 / 2.0, + 1.0, ) .with_destination() } @@ -183,13 +183,9 @@ impl Camera { .map(|p2t| (p1t.xy(), p2t.xy())) }); - // The pixel fidelity of the drawing instrument. - // TODO: Make this configurable - let pen_px_size = 4; - let chunk_count = canvas_points .map(|(p1t, p2t)| { - let rough_chop_size = (p2t - p1t).length() / (pen_px_size as f64 / 2.0); + let rough_chop_size = (p2t - p1t).length() / (PEN_PX_SIZE / 2.0); rough_chop_size.round_ties_even() as usize }) .unwrap_or_else(|| { @@ -203,6 +199,22 @@ impl Camera { Some(SlicedSegment3D::new(chunk_count, segment)) } } + + pub fn ray_for_px_coords(&self, x: f64, y: f64) -> Ray { + let pix_ndc = Point3D::new(x, y, 0.0); + + let world_coord = self + .camera_transformation() + .inverse() + .unwrap() + .transform_point3d(pix_ndc) + .unwrap(); + + Ray { + point: self.observation.eye, + dir: (world_coord.to_vector() - self.observation.eye.to_vector()).normalize(), + } + } } impl Camera { @@ -213,7 +225,7 @@ impl Camera { let xmax = ymax * p.aspect; // TODO: We can apply scaling here based on pen size - let effective_dims: Vec2<()> = Vec2::new(p.width, p.height); + let effective_dims: Vec2<()> = Vec2::new(p.width as f64, p.height as f64); let znear_dims = Vec2::new(xmax, ymax) * 2.0; let est_min_pix = znear_dims.component_div(effective_dims); diff --git a/raydeon/src/lib.rs b/raydeon/src/lib.rs index 9227a9d..c14076d 100644 --- a/raydeon/src/lib.rs +++ b/raydeon/src/lib.rs @@ -1,6 +1,9 @@ +#[allow(clippy::needless_doctest_main)] #[doc = include_str!("../../README.md")] pub(crate) mod bvh; pub mod camera; +pub mod lights; +pub mod material; pub mod path; pub mod ray; pub mod scene; @@ -8,11 +11,15 @@ pub mod shapes; use std::sync::Arc; +pub use camera::{Camera, NoObservation, NoPerspective, Observation, Perspective}; +pub use lights::Light; pub use path::{LineSegment3D, PathMeta}; pub use ray::{HitData, Ray}; +pub use scene::{Scene, SceneGeometry, SceneLighting}; -pub use camera::{Camera, NoObservation, NoPerspective, Observation, Perspective}; -pub use scene::Scene; +// The pixel fidelity of the drawing instrument. +// TODO: Make this configurable +pub const PEN_PX_SIZE: f64 = 4.0; #[cfg(test)] pub(crate) static EPSILON: f64 = 0.004; @@ -50,6 +57,7 @@ where Meta: PathMeta, { fn collision_geometry(&self) -> Option>>>; + fn metadata(&self) -> Meta; fn paths(&self, cam: &Camera) -> Vec>; } diff --git a/raydeon/src/lights.rs b/raydeon/src/lights.rs new file mode 100644 index 0000000..47e2a53 --- /dev/null +++ b/raydeon/src/lights.rs @@ -0,0 +1,148 @@ +use material::Material; +use scene::{SceneGeometry, SceneLighting}; + +use crate::*; + +type LitScene = Scene, SceneLighting>; + +pub trait Light: std::fmt::Debug + Send + Sync + 'static { + fn compute_illumination( + &self, + scene: &Scene, SceneLighting>, + hitpoint: HitData, + shape: &Arc>, + ) -> f64; +} + +#[derive(Debug, Copy, Clone, Default)] +pub struct PointLight { + intensity: f64, + specular_intensity: f64, + position: WPoint3, + + constant_attenuation: f64, + linear_attenuation: f64, + quadratic_attenuation: f64, +} + +impl Light for PointLight { + fn compute_illumination( + &self, + scene: &LitScene, + hitpoint: HitData, + shape: &Arc>, + ) -> f64 { + let _light_hitpoint = match self.light_hitpoint_for_hit(scene, hitpoint, shape) { + Some(hit) => hit, + None => return 0.0, + }; + + let mut illum = 0.0; + let material = shape.metadata(); + + illum += self.diffuse_illumination(hitpoint, &material); + let specular = self.specular_illumination(hitpoint, &material); + tracing::debug!("specular: {}", specular); + illum += specular; + + let atten = self.attenuation(hitpoint); + tracing::debug!("pre-attenuated illum: {}", illum); + tracing::debug!("atten: {}", atten); + let illum = illum * atten; + + tracing::debug!("illum: {}", illum); + illum + } +} + +impl PointLight { + pub fn new( + intensity: f64, + specular_intensity: f64, + position: impl Into, + constant_attenuation: f64, + linear_attenuation: f64, + quadratic_attenuation: f64, + ) -> Self { + let position = position.into(); + Self { + intensity, + specular_intensity, + position, + constant_attenuation, + linear_attenuation, + quadratic_attenuation, + } + } + + pub fn intensity(&self) -> f64 { + self.intensity + } + + pub fn specular(&self) -> f64 { + self.specular_intensity + } + + pub fn position(&self) -> WPoint3 { + self.position + } + + pub fn constant_attenuation(&self) -> f64 { + self.constant_attenuation + } + + pub fn linear_attenuation(&self) -> f64 { + self.linear_attenuation + } + + pub fn quadratic_attenuation(&self) -> f64 { + self.quadratic_attenuation + } + + fn diffuse_illumination(&self, hitpoint: HitData, material: &Material) -> f64 { + let to_light = (self.position - hitpoint.hit_point).normalize(); + let diffuse_scale = to_light.dot(hitpoint.normal).max(0.0); + material.diffuse * self.intensity * diffuse_scale + } + + fn specular_illumination(&self, hitpoint: HitData, material: &Material) -> f64 { + let to_light = (self.position - hitpoint.hit_point).normalize(); + + let v = hitpoint.hit_point.to_vector() * -1.0; + let h = (to_light + v).normalize(); + + let ps = material.specular * self.specular_intensity; + let blinn_phong = h.dot(hitpoint.normal).max(0.0).powf(material.shininess); + let blinn_phong = blinn_phong / (8.0 * std::f64::consts::PI / (material.shininess + 2.0)); + + ps * blinn_phong + } + + fn attenuation(&self, hitpoint: HitData) -> f64 { + let distance = (self.position - hitpoint.hit_point).length(); + let attenuation = self.constant_attenuation + + self.linear_attenuation * distance + + self.quadratic_attenuation * distance * distance; + 1.0 / attenuation + } + + fn light_hitpoint_for_hit( + &self, + scene: &LitScene, + hitpoint: HitData, + shape: &Arc>, + ) -> Option { + let to_light = (self.position - hitpoint.hit_point).normalize(); + if to_light.dot(hitpoint.normal) < 0.0 { + return None; + } + + let to_hitpoint = (hitpoint.hit_point - self.position).normalize(); + let light_ray = Ray::new(self.position, to_hitpoint); + scene + .intersects(light_ray) + .and_then(|(light_hitpoint, light_shape)| { + Arc::ptr_eq(&light_shape, shape).then_some(light_hitpoint) + }) + } +} diff --git a/raydeon/src/material.rs b/raydeon/src/material.rs new file mode 100644 index 0000000..6a0eafd --- /dev/null +++ b/raydeon/src/material.rs @@ -0,0 +1,18 @@ +#[derive(Debug, Clone, Copy, Default)] +pub struct Material { + pub diffuse: f64, + pub specular: f64, + pub shininess: f64, + pub tag: usize, +} + +impl Material { + pub fn new(diffuse: f64, specular: f64, shininess: f64, tag: usize) -> Self { + Self { + diffuse, + specular, + shininess, + tag, + } + } +} diff --git a/raydeon/src/path.rs b/raydeon/src/path.rs index a4f9122..2a9fd2c 100644 --- a/raydeon/src/path.rs +++ b/raydeon/src/path.rs @@ -175,15 +175,45 @@ where } pub fn join_slices(&self) -> Vec> { + self.join_slices_with_forgiveness(0) + } + + /// Joins slices, ignoring gaps of size `forgiveness` or smaller + pub fn join_slices_with_forgiveness( + &self, + forgiveness: usize, + ) -> Vec> { if self.included.is_empty() { return Vec::new(); } + let mut included = self.included.clone(); + + let mut gap_start = None; + let mut last_filled = None; + for curr in 0..self.num_chops { + let empty = !included.contains(&curr); + match gap_start { + Some(start) if empty && curr - start > forgiveness => gap_start = None, + Some(start) if !empty => (start..curr).for_each(|ndx| { + included.insert(ndx); + gap_start = None + }), + None if empty && curr > 0 && last_filled == Some(curr - 1) => { + gap_start = Some(curr); + } + _ => (), + } + if !empty { + last_filled = Some(curr); + } + } + let mut ndx_groups = HashSet::new(); - let mut first = *self.included.first().unwrap(); + let mut first = *included.first().unwrap(); let mut last = first; - self.included.iter().for_each(|ndx| { + included.iter().for_each(|ndx| { if *ndx == first { return; } diff --git a/raydeon/src/ray.rs b/raydeon/src/ray.rs index e6c58c9..41e9f68 100644 --- a/raydeon/src/ray.rs +++ b/raydeon/src/ray.rs @@ -34,11 +34,18 @@ pub struct HitData { pub hit_point: WPoint3, /// The distance that a ray travelled to hit this shape. pub dist_to: f64, + pub normal: WVec3, } impl HitData { - pub fn new(hit_point: WPoint3, dist_to: f64) -> HitData { - HitData { hit_point, dist_to } + pub fn new(hit_point: impl Into, dist_to: f64, normal: impl Into) -> HitData { + let hit_point = hit_point.into(); + let normal = normal.into(); + HitData { + hit_point, + dist_to, + normal, + } } } @@ -58,6 +65,8 @@ impl ApproxEq for &HitData { fn approx_eq>(self, other: Self, margin: M) -> bool { let margin = margin.into(); - self.hit_point.approx_eq(&other.hit_point) && self.dist_to.approx_eq(other.dist_to, margin) + self.hit_point.approx_eq(&other.hit_point) + && self.dist_to.approx_eq(other.dist_to, margin) + && self.normal.approx_eq(&other.normal) } } diff --git a/raydeon/src/scene.rs b/raydeon/src/scene.rs index 5fc217a..ca927ce 100644 --- a/raydeon/src/scene.rs +++ b/raydeon/src/scene.rs @@ -1,34 +1,257 @@ -use bvh::BVHTree; +use bvh::{BVHTree, Collidable}; use camera::{Observation, Perspective}; +use collision::Continuous; +use euclid::{Point2D, Vector2D}; +use material::Material; use path::{LineSegment2D, SlicedSegment3D}; +use rand::distributions::Distribution; +use rand::SeedableRng; use rayon::prelude::*; use std::sync::Arc; use tracing::info; use crate::*; -pub struct SceneCamera<'s, P> +const VERTICAL_HATCH_SCALING: f64 = 0.8; +const DIAGONAL_HATCH_SCALING: f64 = 0.46; +const HATCH_SPACING: f64 = PEN_PX_SIZE * 2.0; +const HATCH_PIXEL_CHOP_SIZE: f64 = PEN_PX_SIZE / 3.0; +const HATCHING_SLICE_FORGIVENESS: usize = 1; + +#[derive(Debug)] +pub struct Scene { + geometry: G, + lighting: L, +} + +#[derive(Debug)] +pub struct SceneGeometry { + geometry: Vec>>, + bvh: BVHTree, +} + +impl SceneGeometry

{ + pub fn new() -> SceneGeometry

{ + Default::default() + } + + pub fn with_geometry(mut self, geometry: Vec>>) -> Self { + let bvh = Self::create_bvh(&geometry); + self.geometry = geometry; + self.bvh = bvh; + self + } + + pub fn push_geometry(mut self, geometry: Arc>) -> Self { + self.geometry.push(geometry); + self.bvh = Self::create_bvh(&self.geometry); + self + } + + pub fn concat_geometry(mut self, geometry: &[Arc>]) -> Self { + self.geometry.extend_from_slice(geometry); + self.bvh = Self::create_bvh(&self.geometry); + self + } + + fn create_bvh(geometry: &[Arc>]) -> BVHTree { + let collision_geometry: Vec<_> = geometry + .iter() + .filter_map(|s| { + s.collision_geometry().map(|collision_geom| { + collision_geom + .into_iter() + .map(|geom| (s.clone(), geom)) + .collect::>() + }) + }) + .flatten() + .map(|(s, collision_geom)| Collidable::new(s, collision_geom)) + .collect(); + BVHTree::new(&collision_geometry) + } +} + +impl Default for SceneGeometry

{ + fn default() -> Self { + Self { + geometry: Vec::new(), + bvh: BVHTree::new(&[]), + } + } +} + +impl + 'static> From>> for SceneGeometry

{ + fn from(geometry: Vec>) -> Self { + SceneGeometry::new().with_geometry( + geometry + .into_iter() + .map(|s| s as Arc>) + .collect::>(), + ) + } +} + +impl From>>> for SceneGeometry

{ + fn from(geometry: Vec>>) -> Self { + SceneGeometry::new().with_geometry(geometry) + } +} + +#[derive(Debug, Default)] +pub struct SceneLighting { + lights: Vec>, + ambient: f64, +} + +impl SceneLighting { + pub fn new() -> Self { + Default::default() + } + + pub fn with_lights(mut self, lights: Vec>) -> Self { + self.lights = lights; + self + } + + pub fn push_light(mut self, light: Arc) -> Self { + self.lights.push(light); + self + } + + pub fn concat_lights(mut self, lights: &[Arc]) -> Self { + self.lights.extend_from_slice(lights); + self + } + + pub fn with_ambient_lighting(mut self, ambient: f64) -> Self { + self.ambient = ambient; + self + } +} + +impl From>> for SceneLighting { + fn from(lights: Vec>) -> Self { + let lights = lights + .into_iter() + .map(|l| l as Arc) + .collect::>(); + SceneLighting::default().with_lights(lights) + } +} + +impl From>> for SceneLighting { + fn from(lights: Vec>) -> Self { + SceneLighting::default().with_lights(lights) + } +} + +impl Default for Scene<(), ()> { + fn default() -> Self { + Self::new() + } +} + +impl Scene<(), ()> { + pub fn new() -> Scene<(), ()> { + Scene { + geometry: (), + lighting: (), + } + } +} + +impl Scene { + pub fn with_geometry

( + self, + geometry: impl Into>, + ) -> Scene, L> + where + P: PathMeta, + { + let Self { lighting, .. } = self; + let geometry = geometry.into(); + Scene { geometry, lighting } + } + + pub fn with_lighting(self, lighting: impl Into) -> Scene { + let Self { geometry, .. } = self; + let lighting = lighting.into(); + Scene { geometry, lighting } + } +} + +impl Scene, L> +where + P: PathMeta, + L: Send + Sync + 'static, +{ + pub fn attach_camera(&self, camera: Camera) -> SceneCamera { + SceneCamera::new(camera, self) + } + + /// Find's the closest intersection point to geometry in the scene, if any + pub(crate) fn intersects(&self, ray: Ray) -> Option<(HitData, Arc>)> { + self.geometry.bvh.intersects(ray) + } + + /// Returns whether or not the given camera has a clear line of sight to a given point. + fn visible(&self, from: WPoint3, point: WPoint3) -> bool { + let v = from - point; + let r = Ray::new(point, v.normalize()); + + match self.intersects(r) { + Some((hitdata, _)) => { + let diff = (hitdata.dist_to - v.length()).abs(); + diff < 1.0e-1 + } + None => true, + } + } +} + +pub struct SceneCamera<'s, P, L> where P: PathMeta, { camera: Camera, - scene: &'s Scene

, + scene: &'s Scene, L>, + seed: Option, } -impl<'a, P> SceneCamera<'a, P> +impl<'a, P, L> SceneCamera<'a, P, L> where P: PathMeta, + L: Send + Sync + 'static, { + pub fn new( + camera: Camera, + scene: &'a Scene, L>, + ) -> Self { + SceneCamera { + camera, + scene, + seed: None, + } + } + + pub fn with_seed(mut self, seed: u64) -> Self { + self.seed = Some(seed); + self + } + fn clip_filter(&self, path: &LineSegment3D) -> bool { self.scene .visible(self.camera.observation.eye, path.midpoint()) } pub fn render(&self) -> Vec> { + info!("Querying geometry for subpaths"); let parent_paths: Vec> = self .scene .geometry - .par_iter() + .geometry + .iter() .flat_map(|s| s.paths(&self.camera)) .collect(); @@ -63,7 +286,6 @@ where let to_remove: Vec = path_group .subsegments() .enumerate() - .par_bridge() .filter_map(|(ndx, path)| { let from_cam = path.midpoint() - self.camera.observation.eye; let close_enough = from_cam.length() < self.camera.perspective.zfar; @@ -71,7 +293,6 @@ where (!visible).then_some(ndx) }) .collect(); - tracing::debug!("Removed {:?} subsegments", to_remove); to_remove .into_iter() .for_each(|ndx| path_group.remove_subsegment(ndx)); @@ -87,52 +308,173 @@ where } } -#[derive(Debug)] -pub struct Scene

-where - P: PathMeta, -{ - geometry: Vec>>, - bvh: BVHTree, +pub struct LitScene { + pub geometry_paths: Vec>, + pub hatch_paths: Vec>, } -impl

Scene

-where - P: PathMeta, -{ - pub fn new(geometry: Vec>>) -> Scene

{ - let collision_geometry: Vec<_> = geometry +impl<'a> SceneCamera<'a, Material, SceneLighting> { + pub fn render_with_lighting(&self) -> LitScene { + let geometry_paths = self.render(); + let mut rng = match self.seed { + Some(seed) => rand::rngs::StdRng::seed_from_u64(seed), + None => rand::rngs::StdRng::from_entropy(), + }; + + info!("Generating vertical hatch lines from lighting."); + let vert_lines = self.filter_hatch_lines_by(&self.vertical_hatch_lines(), |brightness| { + let threshold: f64 = rand::distributions::Standard.sample(&mut rng); + brightness > (threshold * VERTICAL_HATCH_SCALING) + }); + info!("Generated {} vertical hatch lines.", vert_lines.len()); + + info!("Generating diagonal hatch lines from lighting."); + let diag_lines = self.filter_hatch_lines_by(&self.diagonal_hatch_lines(), |brightness| { + let threshold: f64 = rand::distributions::Standard.sample(&mut rng); + brightness > (threshold * DIAGONAL_HATCH_SCALING) + }); + info!("Generated {} diagonal hatch lines.", diag_lines.len()); + + let hatch_paths = [vert_lines, diag_lines].concat(); + + // Compute visible lighting and do screen-space hatching + + LitScene { + geometry_paths, + hatch_paths, + } + } + + fn filter_hatch_lines_by( + &self, + segments: &[LineSegment2D], + mut filter: impl FnMut(f64) -> bool, + ) -> Vec> { + let segments = segments .iter() - .filter_map(|s| s.collision_geometry()) - .flatten() - .collect(); - let bvh = BVHTree::new(&collision_geometry); - Scene { geometry, bvh } + .map(|segment| LineSegment3D::new(segment.p1.to_3d(), segment.p2.to_3d())) + .collect::>(); + let mut split_segments = segments + .iter() + .map(|segment| { + let num_chops = (segment.length().ceil() / HATCH_PIXEL_CHOP_SIZE) as usize; + SlicedSegment3D::new(num_chops, segment) + }) + .collect::>(); + + let paths = split_segments + .iter_mut() + .flat_map(|path_group| { + let to_remove = path_group + .subsegments() + .enumerate() + .filter_map(|(ndx, path)| { + let midpoint = path.midpoint(); + let ray = self.camera.ray_for_px_coords(midpoint.x, midpoint.y); + let lighting = self.lighting_for_ray(ray); + if lighting.is_some() { + tracing::debug!("Lighting: {}", lighting.unwrap()); + } + (lighting.is_none() || filter(lighting.unwrap())).then_some(ndx) + }) + .collect::>(); + + to_remove + .into_iter() + .for_each(|ndx| path_group.remove_subsegment(ndx)); + path_group.join_slices_with_forgiveness(HATCHING_SLICE_FORGIVENESS) + }) + .map(|path| LineSegment2D::new(path.p1().to_2d(), path.p2().to_2d())) + .collect::>(); + + paths } - pub fn attach_camera(&self, camera: Camera) -> SceneCamera

{ - SceneCamera { - camera, - scene: self, - } + fn lighting_for_ray(&self, ray: Ray) -> Option { + let lighting = self.scene.intersects(ray).map(|(hitpoint, shape)| { + self.scene + .lighting + .lights + .iter() + .map(|light| light.compute_illumination(self.scene, hitpoint, &shape)) + .sum() + }); + + lighting } - /// Find's the closest intersection point to geometry in the scene, if any - fn intersects(&self, ray: Ray) -> Option { - self.bvh.intersects(ray) + // https://smashingpencilsart.com/how-do-you-hatch-with-a-pen/ + fn vertical_hatch_lines(&self) -> Vec> { + let initial_offset = HATCH_SPACING / 2.0; + + let mut segments = Vec::new(); + + // vertical lines + let mut x = initial_offset; + while x < self.camera.perspective.width as f64 { + let start = Point2::new(x, 0.0); + let end = Point2::new(x, self.camera.perspective.height as f64); + segments.push(LineSegment2D::new(start, end)); + x += HATCH_SPACING; + } + segments } - /// Returns whether or not the given camera has a clear line of sight to a given point. - fn visible(&self, from: WPoint3, point: WPoint3) -> bool { - let v = from - point; - let r = Ray::new(point, v.normalize()); + fn diagonal_hatch_lines(&self) -> Vec> { + let initial_offset = HATCH_SPACING / 2.0; - match self.intersects(r) { - Some(hitdata) => { - let diff = (hitdata.dist_to - v.length()).abs(); - diff < 1.0e-1 - } - None => true, + let mut segments = Vec::new(); + + // 60degree lines + let hatch_dir = 120.0 * std::f64::consts::PI / 180.0; + let hatch_dir: Vector2D = + Vec2::new(hatch_dir.cos(), hatch_dir.sin()).normalize(); + + let coll_aabb = collision::Aabb2::new( + (0.0, 0.0).into(), + ( + self.camera.perspective.width as f64, + self.camera.perspective.height as f64, + ) + .into(), + ); + let euclid_aabb = euclid::Box2D::new( + (0.0, 0.0).into(), + ( + self.camera.perspective.width as f64, + self.camera.perspective.height as f64, + ) + .into(), + ); + + let diagonal: Vector2D = Vec2::new( + self.camera.perspective.width as f64, + self.camera.perspective.height as f64, + ) + .normalize(); + let mut dist = initial_offset; + let mut curr_point = diagonal * dist; + while euclid_aabb.contains(curr_point.to_point()) { + let start = curr_point; + + let cgstart = cgmath::Point2::from(start.to_array()); + let cgd1 = cgmath::Vector2::from(hatch_dir.to_array()); + let cgd2 = cgd1 * -1.0; + let r1 = collision::Ray::new(cgstart, cgd1); + let r2 = collision::Ray::new(cgstart, cgd2); + + let p1 = coll_aabb.intersection(&r1).unwrap(); + let p2 = coll_aabb.intersection(&r2).unwrap(); + + segments.push(LineSegment2D::new( + Point2D::new(p1.x, p1.y), + Point2D::new(p2.x, p2.y), + )); + + dist += HATCH_SPACING; + curr_point = diagonal * dist; } + + segments } } diff --git a/raydeon/src/shapes/aacuboid.rs b/raydeon/src/shapes/aacuboid.rs index 3ad4455..fe3e8d9 100644 --- a/raydeon/src/shapes/aacuboid.rs +++ b/raydeon/src/shapes/aacuboid.rs @@ -1,8 +1,8 @@ //! Provides basic drawing and collision for axis-aligned cuboids. +use core::f64; +use euclid::Vector3D; use std::sync::Arc; -use collision::Continuous; - use crate::path::LineSegment3D; use crate::{ Camera, CollisionGeometry, HitData, Observation, PathMeta, Perspective, Ray, Shape, WPoint3, @@ -21,13 +21,15 @@ where } impl AxisAlignedCuboid { - pub fn new(min: WVec3, max: WVec3) -> AxisAlignedCuboid { + pub fn new(min: impl Into, max: impl Into) -> AxisAlignedCuboid { Self::tagged(min, max, 0) } } impl AxisAlignedCuboid

{ - pub fn tagged(min: WVec3, max: WVec3, meta: P) -> AxisAlignedCuboid

{ + pub fn tagged(min: impl Into, max: impl Into, meta: P) -> AxisAlignedCuboid

{ + let min = min.into(); + let max = max.into(); AxisAlignedCuboid { min, max, meta } } } @@ -39,12 +41,16 @@ impl From> for AxisAlignedCuboid { } impl Shape for AxisAlignedCuboid

{ + fn metadata(&self) -> P { + self.meta.clone() + } + fn collision_geometry(&self) -> Option>>> { Some(vec![Arc::new(self.clone())]) } fn paths(&self, _cam: &Camera) -> Vec> { - let expand = (self.max - self.min).normalize() * 0.0015; + let expand = (self.max - self.min).normalize() * 0.003; let pathmin = self.min - expand; let pathmax = self.max + expand; @@ -80,23 +86,47 @@ impl Shape for AxisAlignedCuboid

{ impl CollisionGeometry for AxisAlignedCuboid

{ fn hit_by(&self, ray: &Ray) -> Option { - let aabb = collision::Aabb3::new( - cgmath::Point3::new(self.min.x, self.min.y, self.min.z), - cgmath::Point3::new(self.max.x, self.max.y, self.max.z), - ); - let r = collision::Ray3::new( - cgmath::Point3::new(ray.point.x, ray.point.y, ray.point.z), - cgmath::Vector3::new(ray.dir.x, ray.dir.y, ray.dir.z), - ); + let dir_inv = Vector3D::new(1.0, 1.0, 1.0).component_div(ray.dir); + let t1: Vector3D = + (self.min - ray.point.to_vector()).component_mul(dir_inv); + let t2: Vector3D = + (self.max - ray.point.to_vector()).component_mul(dir_inv); + + let dir_inv = dir_inv.to_array(); + let t1 = t1.to_array(); + let t2 = t2.to_array(); + let mut hit_normal = [0.0; 3]; + + let mut tmin = f64::NEG_INFINITY; + let mut tmax = f64::INFINITY; + + for i in 0..3 { + let t1i = t1[i]; + let t2i = t2[i]; + + // Ensure t1 is the near plane and t2 is the far plane. + let (t1i, t2i) = if t1i > t2i { (t2i, t1i) } else { (t1i, t2i) }; + + // Update tmin and tmax to track the intersection range. + if t1i > tmin { + tmin = t1i; + // Determine the normal direction. + hit_normal = [0.0; 3]; + hit_normal[i] = if dir_inv[i] < 0.0 { 1.0 } else { -1.0 }; + } + tmax = f64::min(tmax, t2i); - match r.intersection(&aabb) { - Some(p) => { - let wp = WPoint3::new(p.x, p.y, p.z); - let dist = (wp - ray.point).length(); - Some(HitData::new(wp, dist)) + if tmin > tmax { + return None; } - None => None, } + + if tmin < 0.0 { + return None; + } + + let hit_point = ray.point + ray.dir * tmin; + Some(HitData::new(hit_point, tmin, hit_normal)) } fn bounding_box(&self) -> Option> { @@ -121,14 +151,19 @@ mod test { assert_eq!( prism1.hit_by(&ray1), - Some(HitData::new(WPoint3::new(0.0, 0.5, 0.5), 1.0)) + Some(HitData::new( + WPoint3::new(0.0, 0.5, 0.5), + 1.0, + (-1.0, 0.0, 0.0) + )) ); assert_eq!( prism1.hit_by(&ray2), Some(HitData::new( WPoint3::new(0.39999999999999947, 1.0, 0.29999999999999893), - 12.241323457861899 + 12.241323457861899, + (0.0, 1.0, 0.0) )) ); } diff --git a/raydeon/src/shapes/plane.rs b/raydeon/src/shapes/plane.rs index 1f65b46..170e27a 100644 --- a/raydeon/src/shapes/plane.rs +++ b/raydeon/src/shapes/plane.rs @@ -30,8 +30,9 @@ impl CollisionGeometry for Plane { return None; } + let hit_norm = if rdn > 0.0 { -self.normal } else { self.normal }; let hit_point = ray.point + (ray.dir.normalize() * t); - Some(HitData::new(hit_point, t)) + Some(HitData::new(hit_point, t, hit_norm)) } fn bounding_box(&self) -> Option> { @@ -52,7 +53,11 @@ mod test { WPoint3::new(0.0, 0.0, 0.0), WVec3::new(1.0, 0.0, 0.0) )), - Some(HitData::new(WPoint3::new(1.0, 0.0, 0.0), 1.0,)) + Some(HitData::new( + WPoint3::new(1.0, 0.0, 0.0), + 1.0, + plane1.normal + )) ); assert_eq!( @@ -60,7 +65,11 @@ mod test { WPoint3::new(0.0, 1.0, 0.0), WVec3::new(1.0, -1.0, 0.0) )), - Some(HitData::new(WPoint3::new(1.0, 0.0, 0.0), f64::sqrt(2.0),)) + Some(HitData::new( + WPoint3::new(1.0, 0.0, 0.0), + f64::sqrt(2.0), + plane1.normal + )) ); assert_eq!( diff --git a/raydeon/src/shapes/quad.rs b/raydeon/src/shapes/quad.rs index dbc66f4..1e7ba3a 100644 --- a/raydeon/src/shapes/quad.rs +++ b/raydeon/src/shapes/quad.rs @@ -44,6 +44,10 @@ impl Quad

{ } impl Shape for Quad

{ + fn metadata(&self) -> P { + self.meta.clone() + } + fn collision_geometry(&self) -> Option>>> { Some(vec![ Arc::new(Triangle::new(self.verts[0], self.verts[1], self.verts[3])), diff --git a/raydeon/src/shapes/sphere.rs b/raydeon/src/shapes/sphere.rs index 9ac1434..2094c09 100644 --- a/raydeon/src/shapes/sphere.rs +++ b/raydeon/src/shapes/sphere.rs @@ -46,7 +46,8 @@ impl CollisionGeometry for Sphere { let t = if t_0 < 0.0 { t_1 } else { t_0 }; let hit_point = ray.point + (ray.dir.normalize() * t); - Some(HitData::new(hit_point, t)) + let hit_normal = (hit_point - self.center).normalize(); + Some(HitData::new(hit_point, t, hit_normal)) } fn bounding_box(&self) -> Option> { @@ -70,7 +71,11 @@ mod test { WPoint3::new(0.0, 0.0, 0.0), WVec3::new(1.0, 0.0, 0.0) )), - Some(HitData::new(WPoint3::new(0.5, 0.0, 0.0), 0.5,)) + Some(HitData::new( + WPoint3::new(0.5, 0.0, 0.0), + 0.5, + (-1.0, 0.0, 0.0) + )) ); assert_eq!( @@ -96,7 +101,11 @@ mod test { WPoint3::new(0.0, 1.0, 0.0), WVec3::new(1.0, 0.0, 0.0) )), - Some(HitData::new(WPoint3::new(0.5, 1.0, 0.0), 0.5,)) + Some(HitData::new( + WPoint3::new(0.5, 1.0, 0.0), + 0.5, + (-1.0, 0.0, 0.0) + )) ); let sphere3 = Sphere::new(WPoint3::new(0.0, 0.0, 0.0), 1.0); @@ -106,7 +115,11 @@ mod test { WPoint3::new(0.0, 0.0, 0.0), WVec3::new(1.0, 0.0, 0.0) )), - Some(HitData::new(WPoint3::new(1.0, 0.0, 0.0), 1.0,)) + Some(HitData::new( + WPoint3::new(1.0, 0.0, 0.0), + 1.0, + (1.0, 0.0, 0.0) + )) ); assert_eq!( @@ -114,7 +127,11 @@ mod test { WPoint3::new(0.0, 0.0, 0.0), WVec3::new(-1.0, 0.0, 0.0) )), - Some(HitData::new(WPoint3::new(-1.0, 0.0, 0.0), 1.0,)) + Some(HitData::new( + WPoint3::new(-1.0, 0.0, 0.0), + 1.0, + (-1.0, 0.0, 0.0) + )) ); } } diff --git a/raydeon/src/shapes/triangle.rs b/raydeon/src/shapes/triangle.rs index 67309b2..be366b5 100644 --- a/raydeon/src/shapes/triangle.rs +++ b/raydeon/src/shapes/triangle.rs @@ -39,6 +39,10 @@ impl Triangle

{ } impl Shape for Triangle

{ + fn metadata(&self) -> P { + self.meta.clone() + } + fn collision_geometry(&self) -> Option>>> { Some(vec![Arc::new(self.clone())]) } @@ -146,7 +150,11 @@ mod test { assert_eq!( tri1.hit_by(&ray1), - Some(HitData::new(WPoint3::new(0.25, 0.25, 0.0), 2.0)) + Some(HitData::new( + WPoint3::new(0.25, 0.25, 0.0), + 2.0, + (0.0, 0.0, -1.0) + )) ); assert_eq!(tri1.hit_by(&ray2), None); @@ -154,7 +162,11 @@ mod test { assert_eq!( tri1.hit_by(&ray4), - Some(HitData::new(WPoint3::new(0.1, 0.01, 0.0), 2.0)) + Some(HitData::new( + WPoint3::new(0.1, 0.01, 0.0), + 2.0, + (0.0, 0.0, -1.0) + )) ); } }