From bb6402c6c4eba6d5acdc71bb670aca8cb2f30ae2 Mon Sep 17 00:00:00 2001 From: "Sean P. Kelly" Date: Thu, 28 Nov 2024 22:01:22 -0800 Subject: [PATCH] feat: allow specifying materials per-drawable --- Cargo.toml | 1 + README.md | 62 +- pyraydeon/examples/py_sphere.py | 28 +- pyraydeon/src/drawables.rs | 192 +++ pyraydeon/src/lib.rs | 2 + pyraydeon/src/material.rs | 8 +- pyraydeon/src/scene.rs | 118 +- pyraydeon/src/shapes/mod.rs | 53 +- pyraydeon/src/shapes/primitive.rs | 18 +- raydeon/Cargo.toml | 3 +- raydeon/examples/lit_cubes.rs | 57 +- raydeon/examples/lit_cubes_expected.svg | 1643 ++++++++++++----------- raydeon/src/bvh.rs | 59 +- raydeon/src/camera.rs | 6 +- raydeon/src/lib.rs | 53 +- raydeon/src/lights.rs | 55 +- raydeon/src/material.rs | 10 +- raydeon/src/path.rs | 156 ++- raydeon/src/ray.rs | 17 +- raydeon/src/scene.rs | 160 ++- raydeon/src/shapes/aacuboid.rs | 40 +- raydeon/src/shapes/quad.rs | 12 +- raydeon/src/shapes/triangle.rs | 10 +- 23 files changed, 1562 insertions(+), 1201 deletions(-) create mode 100644 pyraydeon/src/drawables.rs diff --git a/Cargo.toml b/Cargo.toml index 7cb1c17..ac57fa7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ anyhow = "1" bon = "3" cgmath = "0.17" collision = "0.20" +dot_vox = "5" env_logger = "0.11" euclid = "0.22" float-cmp = "0.5" diff --git a/README.md b/README.md index d91c219..9ed0260 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ lighting. ```rust use raydeon::lights::PointLight; use raydeon::shapes::AxisAlignedCuboid; -use raydeon::Material; use raydeon::{Camera, Scene, SceneLighting, WPoint3, WVec3}; +use raydeon::{DrawableShape, Material}; use std::sync::Arc; fn main() { @@ -33,29 +33,36 @@ fn main() { .format_timestamp_nanos() .init(); + let cube_material = Material::new_mat(3.0, 2.0, 2.0, 0); let scene = Scene::new() .geometry(vec![ - Arc::new( - AxisAlignedCuboid::new() - .min((-1.0, -1.0, -1.0)) - .max((1.0, 1.0, 1.0)) - .material(Material::new(3.0, 2.0, 2.0, 0)) - .build(), - ), - Arc::new( - AxisAlignedCuboid::new() - .min((1.8, -1.0, -1.0)) - .max((3.8, 1.0, 1.0)) - .material(Material::new(2.0, 2.0, 2.0, 0)) - .build(), - ), - Arc::new( - AxisAlignedCuboid::new() - .min((-1.4, 1.8, -1.0)) - .max((0.6, 3.8, 1.0)) - .material(Material::new(3.0, 2.0, 2.0, 0)) - .build(), - ), + DrawableShape::new() + .geometry(Arc::new( + AxisAlignedCuboid::new() + .min((-1.0, -1.0, -1.0)) + .max((1.0, 1.0, 1.0)) + .build(), + )) + .material(cube_material) + .build(), + DrawableShape::new() + .geometry(Arc::new( + AxisAlignedCuboid::new() + .min((1.8, -1.0, -1.0)) + .max((3.8, 1.0, 1.0)) + .build(), + )) + .material(cube_material) + .build(), + DrawableShape::new() + .geometry(Arc::new( + AxisAlignedCuboid::new() + .min((-1.4, 1.8, -1.0)) + .max((0.6, 3.8, 1.0)) + .build(), + )) + .material(cube_material) + .build(), ]) .lighting( SceneLighting::new() @@ -86,10 +93,7 @@ fn main() { .perspective(Camera::perspective(fovy, width, height, znear, zfar)) .build(); - let render_result = scene - .attach_camera(camera) - .with_seed(0) - .render_with_lighting(); + let render_result = scene.attach_camera(camera).render_with_lighting(); let mut svg_doc = svg::Document::new() .set("width", "8in") @@ -111,11 +115,7 @@ fn main() { 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()) - { + for path in render_result { let (p1, p2) = (path.p1, path.p2); item_group = item_group.add( svg::node::element::Line::new() diff --git a/pyraydeon/examples/py_sphere.py b/pyraydeon/examples/py_sphere.py index e700abc..d4f5b8a 100644 --- a/pyraydeon/examples/py_sphere.py +++ b/pyraydeon/examples/py_sphere.py @@ -18,16 +18,9 @@ class PySphere(Geometry): - def __init__(self, point, radius, material=None): - if material is not None: - self._material = material - + def __init__(self, point, radius): self.sphere = Sphere(point, radius) - @property - def material(self): - return self._material - def collision_geometry(self): return [self.sphere] @@ -71,16 +64,9 @@ def paths(self, cam): class PyPlane(Geometry): - def __init__(self, point, normal, material=None): - if material is not None: - self._material = material - + def __init__(self, point, normal): self.plane = Plane(point, normal) - @property - def material(self): - return self._material - def collision_geometry(self): return [self.plane] @@ -90,8 +76,14 @@ def paths(self, cam): scene = Scene( geometry=[ - PySphere(Point3(0, 0, 0), 1.0, Material(3.0, 3.0, 3)), - PyPlane(Point3(0, -2, 0), Vec3(0, 1, 0), Material(9000.0, 3.0, 3)), + PySphere( + Point3(0, 0, 0), + 1.0, + ).with_material(Material(3.0, 3.0, 3)), + PyPlane( + Point3(0, -2, 0), + Vec3(0, 1, 0), + ).with_material(Material(9000.0, 3.0, 3)), ], lights=[PointLight((4, 3, 10), 3.6, 2.0, 0.15, 0.4, 0.11)], ambient_light=0.13, diff --git a/pyraydeon/src/drawables.rs b/pyraydeon/src/drawables.rs new file mode 100644 index 0000000..16374d0 --- /dev/null +++ b/pyraydeon/src/drawables.rs @@ -0,0 +1,192 @@ +use std::sync::Arc; + +use numpy::{Ix1, PyArray}; +use pyo3::prelude::*; + +use crate::material::Material; +use crate::shapes::Geometry; + +#[derive(Debug)] +#[pyclass(frozen)] +/// A `DrawableShape` is the input geometry for a pyraydeon scene. +/// +/// It is essentially some drawable geometry joined with a given material. +pub(crate) struct DrawableShape { + pub raydeon_drawable: raydeon::DrawableShape, + pub pyobj: PyObject, +} + +impl ::std::ops::Deref for DrawableShape { + type Target = raydeon::DrawableShape; + + fn deref(&self) -> &Self::Target { + &self.raydeon_drawable + } +} + +impl DrawableShape { + pub(crate) fn raydeon_drawable(&self) -> raydeon::DrawableShape { + self.raydeon_drawable.clone() + } +} + +impl<'py> FromPyObject<'py> for DrawableShape { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + let py = obj.py(); + let shape: PyObject = obj.getattr("shape")?.extract()?; + let material: Option = obj.getattr("material")?.extract()?; + + let raydeon_geometry = raydeon_geometry_from_py_object(py, &shape)?; + let raydeon_drawable = raydeon::DrawableShape::new() + .geometry(raydeon_geometry) + .maybe_material(material.map(|m| m.0)) + .build(); + + Ok(DrawableShape { + raydeon_drawable, + pyobj: shape, + }) + } +} + +#[pymethods] +impl DrawableShape { + #[new] + #[pyo3(signature = (geometry, material=None))] + fn new(py: Python, geometry: PyObject, material: Option) -> PyResult { + let raydeon_geometry = raydeon_geometry_from_py_object(py, &geometry)?; + let material = material.map(|m| m.0); + let raydeon_drawable = raydeon::DrawableShape::new() + .geometry(raydeon_geometry) + .maybe_material(material) + .build(); + + Ok(DrawableShape { + raydeon_drawable, + pyobj: geometry, + }) + } + + #[getter] + fn material(&self) -> Option { + self.raydeon_drawable.material().map(Into::into) + } + + #[getter] + fn shape(&self, py: Python) -> PyObject { + self.pyobj.clone_ref(py) + } + + fn collision_geometry(&self, py: Python) -> PyResult { + self.pyobj.call_method0(py, "collision_geometry") + } + + fn paths(&self, py: Python) -> PyResult { + self.pyobj.call_method0(py, "paths") + } + + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { + let class_name = slf.get_type().qualname()?; + Ok(format!("{}<{:#?}>", class_name, slf.borrow())) + } +} + +pub(crate) fn raydeon_geometry_from_py_object( + py: Python, + g: &PyObject, +) -> PyResult> { + let geom: Py = g.extract(py)?; + let raydeon_shape = geom.borrow(py); + let raydeon_shape = raydeon_shape.geometry(g.clone_ref(py)); + Ok(raydeon_shape) +} + +#[derive(Debug)] +#[pyclass(frozen)] +/// The output of a raydeon render. +/// +/// A line segment, associated with a shape. +pub(crate) struct DrawableSegment { + p1: [f64; 2], + p2: [f64; 2], + kind: SegmentKind, + + raydeon_drawable: Option, +} + +impl From> for DrawableSegment { + fn from(value: raydeon::DrawableSegment<'_>) -> Self { + let p1 = value.p1.to_array(); + let p2 = value.p2.to_array(); + + let raydeon_drawable = value.segment.get_shape().cloned(); + + let kind = value.kind.into(); + + Self { + p1, + p2, + raydeon_drawable, + kind, + } + } +} + +#[pymethods] +impl DrawableSegment { + #[getter] + fn p1<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.p1) + } + + #[getter] + fn p2<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.p2) + } + + #[getter] + fn material(&self) -> Option { + self.raydeon_drawable + .as_ref() + .and_then(|d| d.material()) + .map(|m| m.into()) + } + + #[getter] + fn kind(&self) -> SegmentKind { + self.kind + } + + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { + let class_name = slf.get_type().qualname()?; + Ok(format!("{}<{:#?}>", class_name, slf.borrow())) + } +} + +#[pyclass(eq)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub(crate) enum SegmentKind { + VerticalHatch, + DiagonalHatch, + Path, +} + +impl From for SegmentKind { + fn from(value: raydeon::SegmentKind) -> Self { + match value { + raydeon::SegmentKind::ScreenSpaceHatch(raydeon::ScreenSpaceHatchKind::Vertical) => { + SegmentKind::VerticalHatch + } + raydeon::SegmentKind::ScreenSpaceHatch(raydeon::ScreenSpaceHatchKind::Diagonal60) => { + SegmentKind::VerticalHatch + } + raydeon::SegmentKind::Path => SegmentKind::Path, + } + } +} + +pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + Ok(()) +} diff --git a/pyraydeon/src/lib.rs b/pyraydeon/src/lib.rs index 3692d1d..784b815 100644 --- a/pyraydeon/src/lib.rs +++ b/pyraydeon/src/lib.rs @@ -44,5 +44,7 @@ fn pyraydeon(m: &Bound<'_, PyModule>) -> PyResult<()> { crate::material::register(m)?; crate::light::register(m)?; + crate::drawables::register(m)?; + Ok(()) } diff --git a/pyraydeon/src/material.rs b/pyraydeon/src/material.rs index e647e1b..142358e 100644 --- a/pyraydeon/src/material.rs +++ b/pyraydeon/src/material.rs @@ -7,7 +7,13 @@ 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()) + Ok(raydeon::material::Material::new() + .diffuse(diffuse) + .specular(specular) + .shininess(shininess) + .tag(tag) + .build() + .into()) } #[getter] diff --git a/pyraydeon/src/scene.rs b/pyraydeon/src/scene.rs index f07932b..12db79f 100644 --- a/pyraydeon/src/scene.rs +++ b/pyraydeon/src/scene.rs @@ -1,14 +1,13 @@ use std::sync::Arc; -use numpy::{Ix1, PyArray, PyReadonlyArray1}; +use numpy::{Ix1, PyArray}; use pyo3::prelude::*; use raydeon::SceneLighting; use crate::camera::Camera; +use crate::drawables::{DrawableSegment, DrawableShape}; use crate::light::PointLight; -use crate::linear::{ArbitrarySpace, Point2, Point3}; -use crate::material::Material; -use crate::shapes::Geometry; +use crate::linear::Point3; #[pyclass(frozen)] pub(crate) struct Scene { @@ -18,28 +17,24 @@ pub(crate) struct Scene { #[pymethods] impl Scene { #[new] - #[pyo3(signature = (geometry=None, lights=None, ambient_light=0.0))] + #[pyo3(signature = (geometry, lights=None, ambient_light=0.0))] fn new( - py: Python, - geometry: Option>, + geometry: Option>, lights: Option>, ambient_light: f64, ) -> PyResult { let geometry = geometry.unwrap_or_default(); + let geometry: Vec<_> = geometry + .iter() + .map(DrawableShape::raydeon_drawable) + .collect::<_>(); + 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(|l| Arc::new(l.0) as Arc) .collect(); + let lighting = SceneLighting::new() .with_lights(lights) .with_ambient_lighting(ambient_light); @@ -52,13 +47,10 @@ impl Scene { Ok(Self { scene }) } - fn render(&self, py: Python, camera: &Camera) -> Vec { + fn render(&self, py: Python, camera: &Camera) -> Vec { py.allow_threads(|| { let cam = self.scene.attach_camera(camera.0.clone()); - cam.render() - .into_iter() - .map(|ls| ls.cast_unit().into()) - .collect() + cam.render().into_iter().map(Into::into).collect() }) } @@ -68,7 +60,7 @@ impl Scene { py: Python, camera: &Camera, seed: Option, - ) -> Vec { + ) -> Vec { py.allow_threads(|| { let cam = self.scene.attach_camera(camera.0.clone()); let cam = if let Some(seed) = seed { @@ -77,12 +69,7 @@ impl Scene { 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() + render_result.into_iter().map(Into::into).collect() }) } @@ -92,73 +79,64 @@ impl Scene { } } -pywrap!(LineSegment2D, raydeon::path::LineSegment2D); +#[derive(Debug, Copy, Clone)] +#[pyclass(frozen)] +pub(crate) struct LineSegment3D { + p1: [f64; 3], + p2: [f64; 3], +} #[pymethods] -impl LineSegment2D { +impl LineSegment3D { #[new] - fn new(p1: PyReadonlyArray1, p2: PyReadonlyArray1) -> PyResult { - let p1 = Point2::try_from(p1)?; - let p2 = Point2::try_from(p2)?; - Ok(raydeon::path::LineSegment2D::new(p1.cast_unit(), p2.cast_unit()).into()) + fn new(p1: &Bound<'_, PyAny>, p2: &Bound<'_, PyAny>) -> PyResult { + let p1 = Point3::try_from(p1)?; + let p2 = Point3::try_from(p2)?; + Ok(Self { + p1: p1.to_array(), + p2: p2.to_array(), + }) } #[getter] fn p1<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { - PyArray::from_slice_bound(py, &self.0.p1.to_array()) + PyArray::from_slice_bound(py, &self.p1) } #[getter] fn p2<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { - PyArray::from_slice_bound(py, &self.0.p2.to_array()) + PyArray::from_slice_bound(py, &self.p2) } fn __repr__(slf: &Bound<'_, Self>) -> PyResult { let class_name = slf.get_type().qualname()?; - Ok(format!("{}<{:?}>", class_name, slf.borrow().0)) + Ok(format!("{}<{:?}>", class_name, slf.borrow())) } } -pywrap!(LineSegment3D, raydeon::path::LineSegment3D); - -#[pymethods] -impl LineSegment3D { - #[new] - #[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::new() - .p1(p1.cast_unit()) - .p2(p2.cast_unit()) - .maybe_material(material.map(|i| i.0)) - .build() - .into()) - } - - #[getter] - fn p1<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { - PyArray::from_slice_bound(py, &self.0.p1().to_array()) - } - - #[getter] - fn p2<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { - PyArray::from_slice_bound(py, &self.0.p2().to_array()) +impl From> for LineSegment3D +where + Space: Copy + Clone + std::fmt::Debug, +{ + fn from(value: raydeon::LineSegment3D<'_, Space>) -> Self { + Self { + p1: value.p1().to_array(), + p2: value.p2().to_array(), + } } +} - fn __repr__(slf: &Bound<'_, Self>) -> PyResult { - let class_name = slf.get_type().qualname()?; - Ok(format!("{}<{:?}>", class_name, slf.borrow().0)) +impl From for raydeon::LineSegment3D<'_, Space> +where + Space: Copy + Clone + std::fmt::Debug, +{ + fn from(value: LineSegment3D) -> Self { + raydeon::LineSegment3D::new_segment(value.p1, value.p2) } } pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; - m.add_class::()?; m.add_class::()?; Ok(()) } diff --git a/pyraydeon/src/shapes/mod.rs b/pyraydeon/src/shapes/mod.rs index 119edc4..e2053da 100644 --- a/pyraydeon/src/shapes/mod.rs +++ b/pyraydeon/src/shapes/mod.rs @@ -9,6 +9,7 @@ mod primitive; pub(crate) use primitive::{AxisAlignedCuboid, Tri}; use crate::camera::Camera; +use crate::drawables::{raydeon_geometry_from_py_object, DrawableShape}; use crate::material::Material; use crate::ray::{HitData, Ray, AABB3}; use crate::scene::LineSegment3D; @@ -71,19 +72,40 @@ impl Geometry { match &self.geom { InnerGeometry::Native(geom) => py.allow_threads(|| { let paths = geom.paths(&cam.0); - paths - .into_iter() - .map(raydeon::path::LineSegment3D::cast_unit) - .map(Into::into) - .collect() + paths.into_iter().map(Into::into).collect() }), InnerGeometry::Py => Vec::new(), } } + fn with_material(slf: &Bound<'_, Self>, mat: Material, py: Python) -> PyResult { + let obj_ptr = slf.as_any().as_unbound().clone_ref(py); + let radeon_geom = raydeon_geometry_from_py_object(py, &obj_ptr)?; + + let raydeon_drawable = raydeon::DrawableShape::new() + .geometry(radeon_geom) + .material(mat.0) + .build(); + + Ok(DrawableShape { + raydeon_drawable, + pyobj: obj_ptr, + }) + } + + #[getter] + fn shape(slf: &Bound<'_, Self>, py: Python) -> PyObject { + slf.as_any().as_unbound().clone_ref(py) + } + + #[getter] + fn material(&self) -> Option { + None + } + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { let class_name = slf.get_type().qualname()?; - Ok(format!("{}<{:#?}>", class_name, slf.borrow().geom)) + Ok(format!("{}<{:#?}>", class_name, slf.borrow())) } } @@ -209,27 +231,10 @@ impl raydeon::Shape for PythonGeometry { .extract::>>() .unwrap()?; - Some( - segments - .into_iter() - .map(|segment| segment.0.cast_unit()) - .collect(), - ) + Some(segments.into_iter().map(Into::into).collect()) }); 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 { diff --git a/pyraydeon/src/shapes/primitive.rs b/pyraydeon/src/shapes/primitive.rs index 6b0bca4..d89ed84 100644 --- a/pyraydeon/src/shapes/primitive.rs +++ b/pyraydeon/src/shapes/primitive.rs @@ -1,6 +1,5 @@ use super::{CollisionGeometry, Geometry}; use crate::linear::{Point3, Vec3}; -use crate::material::Material; use numpy::{Ix1, PyArray, PyArrayLike1, PyArrayLike2}; use pyo3::exceptions::PyIndexError; use pyo3::prelude::*; @@ -26,12 +25,8 @@ impl From> for AxisAlignedCuboid { #[pymethods] impl AxisAlignedCuboid { #[new] - #[pyo3(signature = (min, max, material=None))] - fn new( - min: &Bound<'_, PyAny>, - max: &Bound<'_, PyAny>, - material: Option, - ) -> PyResult<(Self, Geometry)> { + #[pyo3(signature = (min, max))] + fn new(min: &Bound<'_, PyAny>, max: &Bound<'_, PyAny>) -> PyResult<(Self, Geometry)> { let min: Vec3 = min.try_into()?; let max: Vec3 = max.try_into()?; @@ -39,7 +34,6 @@ impl AxisAlignedCuboid { raydeon::shapes::AxisAlignedCuboid::new() .min(min.cast_unit()) .max(max.cast_unit()) - .material(material.map(|m| m.0).unwrap_or_default()) .build(), ); let geom = Geometry::native(Arc::clone(&shape) as Arc); @@ -68,12 +62,11 @@ impl From> for Tri { #[pymethods] impl Tri { #[new] - #[pyo3(signature = (p1, p2, p3, material=None))] + #[pyo3(signature = (p1, p2, p3))] 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()?; @@ -84,7 +77,6 @@ impl Tri { .v0(p1.cast_unit()) .v1(p2.cast_unit()) .v2(p3.cast_unit()) - .material(material.map(|m| m.0).unwrap_or_default()) .build(), ); let geom = Geometry::native(Arc::clone(&shape) as Arc); @@ -206,12 +198,11 @@ impl From> for Quad { #[pymethods] impl Quad { #[new] - #[pyo3(signature = (origin, basis, dims, material=None))] + #[pyo3(signature = (origin, basis, dims))] 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 @@ -249,7 +240,6 @@ impl Quad { .origin(origin.0.cast_unit()) .basis(basis) .dims(dims) - .material(material.map(|m| m.0).unwrap_or_default()) .build(), ); let geom = Geometry::native(Arc::clone(&shape) as Arc); diff --git a/raydeon/Cargo.toml b/raydeon/Cargo.toml index a1f3bf5..e589979 100644 --- a/raydeon/Cargo.toml +++ b/raydeon/Cargo.toml @@ -5,9 +5,11 @@ authors = ["cbgbt "] edition = "2021" [dependencies] +anyhow.workspace = true bon.workspace = true cgmath.workspace = true collision.workspace = true +dot_vox.workspace = true euclid.workspace = true float-cmp.workspace = true log.workspace = true @@ -16,6 +18,5 @@ rayon.workspace = true tracing = { workspace = true, features = ["log"] } [dev-dependencies] -anyhow.workspace = true env_logger.workspace = true svg.workspace = true diff --git a/raydeon/examples/lit_cubes.rs b/raydeon/examples/lit_cubes.rs index 5c232a4..f53b3b3 100644 --- a/raydeon/examples/lit_cubes.rs +++ b/raydeon/examples/lit_cubes.rs @@ -1,7 +1,7 @@ use raydeon::lights::PointLight; use raydeon::shapes::AxisAlignedCuboid; -use raydeon::Material; use raydeon::{Camera, Scene, SceneLighting, WPoint3, WVec3}; +use raydeon::{DrawableShape, Material}; use std::sync::Arc; fn main() { @@ -9,29 +9,36 @@ fn main() { .format_timestamp_nanos() .init(); + let cube_material = Material::new_mat(3.0, 2.0, 2.0, 0); let scene = Scene::new() .geometry(vec![ - Arc::new( - AxisAlignedCuboid::new() - .min((-1.0, -1.0, -1.0)) - .max((1.0, 1.0, 1.0)) - .material(Material::new(3.0, 2.0, 2.0, 0)) - .build(), - ), - Arc::new( - AxisAlignedCuboid::new() - .min((1.8, -1.0, -1.0)) - .max((3.8, 1.0, 1.0)) - .material(Material::new(2.0, 2.0, 2.0, 0)) - .build(), - ), - Arc::new( - AxisAlignedCuboid::new() - .min((-1.4, 1.8, -1.0)) - .max((0.6, 3.8, 1.0)) - .material(Material::new(3.0, 2.0, 2.0, 0)) - .build(), - ), + DrawableShape::new() + .geometry(Arc::new( + AxisAlignedCuboid::new() + .min((-1.0, -1.0, -1.0)) + .max((1.0, 1.0, 1.0)) + .build(), + )) + .material(cube_material) + .build(), + DrawableShape::new() + .geometry(Arc::new( + AxisAlignedCuboid::new() + .min((1.8, -1.0, -1.0)) + .max((3.8, 1.0, 1.0)) + .build(), + )) + .material(cube_material) + .build(), + DrawableShape::new() + .geometry(Arc::new( + AxisAlignedCuboid::new() + .min((-1.4, 1.8, -1.0)) + .max((0.6, 3.8, 1.0)) + .build(), + )) + .material(cube_material) + .build(), ]) .lighting( SceneLighting::new() @@ -87,11 +94,7 @@ fn main() { 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()) - { + for path in render_result { let (p1, p2) = (path.p1, path.p2); item_group = item_group.add( svg::node::element::Line::new() diff --git a/raydeon/examples/lit_cubes_expected.svg b/raydeon/examples/lit_cubes_expected.svg index 5ff2f28..b11e4b3 100644 --- a/raydeon/examples/lit_cubes_expected.svg +++ b/raydeon/examples/lit_cubes_expected.svg @@ -29,1122 +29,1187 @@ + + + - - - + + + - + - - - - + + + - - + + - - - + + - - + + + + + + + - - - - - - + + + + + + + - - - - - - - - - - - - + + + + + + + + + + - - + + + + + + + - - - - - + + + + + + + - - - - - - - - - + + + + + + - - - - - - - - - - - - - + + + + + + + + + + - - - - - + + + + + + + + + + - - - - - - - - + + + + + + + + + + + - - - - - - - - - - + + + + - - - - - - - + + + + + + + + + + + + - - - + - + + + + + - - - + + + + + - - - - - - - - + + + + + + + + - - - - + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - + + + + + + - + + + + + + - - - - - - - - - - - + + + + - - - - - + + - - - + + - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + - - - - - + - - - - - - - - - - - - - + + + - - - - - - - - - - - - - - + + + - - - - - - + + + - - - - + + + + - - + - - - - - - - - - - - - - + - - - - + + - - - - - - + - - - - + - + + + - - - - - + + + + - + - - - + - - + + + + + + - - - - - - - - - - - - - + - - + + + + + + + - - - - - - + - - + + + - + + + + + + - - - - - - - + + + + - - + + + + + - - + + + - - - - - - - - - + + + + - - - + + + - + - - - - + - + - - - - + + + + + - + - + - - - + + + + - - - + + - + + - - + - - + + + + + + - - - - - - + - + + + + + + - - - - - - - - - + + - - - + + - - + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - + + - + + + + + + + + + + - - - - - - - + - - - - - - + + + + + + - - - + + + + + - - - + + - - - + + - - - - + + + + - + - + + - - - - + - - + + + + + + + + + + + + - - - - + + + + - - - - + - - - - - - + + + + + + + - - - + - - - - - - - + + - - - - - - - - + + + - + - - + + - - + + + + + + + + - - - - + + + - - - - - - - - + + + + - + - - - - + + + + + + - - - + + + + + + + + + - - - - + + - - - - + + + + + + + - - - - - - - + + - - - + + + - - - - - - + + + + + + + + - - - - - - - - + + + + + + + - + + + - - - + + + + - - - - - - + + + + - - - + - - - + + + + + + + + - - - - - - + + - - - + - - - - + + + + + - - - - + - + + + + - - - - + + - - + + + + + + + + + + + + - - - - - - - - - - - + - + - - - + + + + - + + - + + + - - - - - - + - + + + + + - - - - + + + + - + - + - + + - - - + + + - - - + + + + - - - + + + + + - - - + + + - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - - - + + + + + + + + + + + + - - + + + - - - - - - - + + + + - - - - - - - - - + + + + + + - - + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + - + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + - - - - + + + + + + - - - - - - - + + + + + + + + + + + + + + + - - - - - - - + + + - - - + + + + + + + + + - - - - - - - - - + + + + + - - + + + + - - - - - + + + + + + + + + + - - - - - + - + + + + + + + + + + + + + - + + + + + + + + + + + - - - - + - - - - - - + + + + - - - - - + + + + + + + + + + - - - - - - - - + + + + + + - - - - + + + + + + + + + - - - + + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + - - - - + - - - + + + + + + + + + - - - - + + + @@ -1155,20 +1220,20 @@ - + - + - + diff --git a/raydeon/src/bvh.rs b/raydeon/src/bvh.rs index e8a0e99..b8277a7 100644 --- a/raydeon/src/bvh.rs +++ b/raydeon/src/bvh.rs @@ -1,4 +1,5 @@ -use crate::{CollisionGeometry, HitData, Ray, Shape, WorldSpace, AABB3}; +use crate::ray::HitShape; +use crate::{CollisionGeometry, DrawableShape, Ray, WorldSpace, AABB3}; use euclid::Point3D; use rayon::prelude::*; use std::sync::Arc; @@ -6,12 +7,12 @@ use tracing::info; #[derive(Debug, Clone)] pub(crate) struct Collidable { - shape: Arc, + shape: DrawableShape, collision: Arc, } impl Collidable { - pub(crate) fn new(shape: Arc, collision: Arc) -> Self { + pub(crate) fn new(shape: DrawableShape, collision: Arc) -> Self { Self { shape, collision } } } @@ -64,17 +65,22 @@ impl BVHTree { } impl BVHTree { - pub(crate) fn intersects(&self, ray: Ray) -> Option<(HitData, Arc)> { + pub(crate) fn intersects(&self, ray: Ray) -> Option { 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.hit_data + .dist_to + .partial_cmp(&hit2.hit_data.dist_to) + .unwrap() + }) } - fn intersects_bounded_volume(&self, ray: Ray) -> Option<(HitData, Arc)> { + fn intersects_bounded_volume(&self, ray: Ray) -> Option { let (tmin, tmax) = bounding_box_intersects(self.aabb, ray); if tmax < tmin || tmax <= 0.0 { None @@ -85,16 +91,21 @@ impl BVHTree { } } - fn intersects_unbounded_volume(&self, ray: Ray) -> Option<(HitData, Arc)> { + fn intersects_unbounded_volume(&self, ray: Ray) -> Option { self.unbounded .iter() .filter_map(|collidable| { collidable .collision .hit_by(&ray) - .map(|hit_point| (hit_point, collidable.shape.clone())) + .map(|hit_point| HitShape::new(hit_point, &collidable.shape)) + }) + .min_by(|hit1, hit2| { + hit1.hit_data + .dist_to + .partial_cmp(&hit2.hit_data.dist_to) + .unwrap() }) - .min_by(|(hit1, _), (hit2, _)| hit1.dist_to.partial_cmp(&hit2.dist_to).unwrap()) } } @@ -113,7 +124,7 @@ struct ParentNode { } impl ParentNode { - fn intersects(&self, ray: Ray, tmin: f64, tmax: f64) -> Option<(HitData, Arc)> { + fn intersects(&self, ray: Ray, tmin: f64, tmax: f64) -> Option { let rp: f64; let rd: f64; match self.axis { @@ -145,14 +156,23 @@ impl ParentNode { } else { let h1 = first.intersects(ray, tmin, tsplit); - if h1.as_ref().is_some_and(|(hit, _)| hit.dist_to <= tsplit) { + if h1 + .as_ref() + .is_some_and(|hit| hit.hit_data.dist_to <= tsplit) + { return h1; } - let h1t = h1.as_ref().map(|(hit, _)| hit.dist_to).unwrap_or(f64::MAX); + let h1t = h1 + .as_ref() + .map(|hit| hit.hit_data.dist_to) + .unwrap_or(f64::MAX); let h2 = second.intersects(ray, tsplit, f64::min(tmax, h1t)); - let h2t = h2.as_ref().map(|(hit, _)| hit.dist_to).unwrap_or(f64::MAX); + let h2t = h2 + .as_ref() + .map(|hit| hit.hit_data.dist_to) + .unwrap_or(f64::MAX); if h1t < h2t { h1 @@ -207,7 +227,7 @@ impl LeafNode { } impl LeafNode { - fn intersects(&self, ray: Ray) -> Option<(HitData, Arc)> { + fn intersects(&self, ray: Ray) -> Option { self.shapes .iter() .filter_map(|shape| { @@ -215,9 +235,14 @@ impl LeafNode { .collidable .collision .hit_by(&ray) - .map(|hitpoint| (hitpoint, shape.collidable.shape.clone())) + .map(|hitpoint| HitShape::new(hitpoint, &shape.collidable.shape)) + }) + .min_by(|hit1, hit2| { + hit1.hit_data + .dist_to + .partial_cmp(&hit2.hit_data.dist_to) + .unwrap() }) - .min_by(|(hit1, _), (hit2, _)| hit1.dist_to.partial_cmp(&hit2.dist_to).unwrap()) } } @@ -296,7 +321,7 @@ impl Node { } impl Node { - fn intersects(&self, ray: Ray, tmin: f64, tmax: f64) -> Option<(HitData, Arc)> { + fn intersects(&self, ray: Ray, tmin: f64, tmax: f64) -> Option { match self { Self::Parent(parent_node) => parent_node.intersects(ray, tmin, tmax), Self::Leaf(leaf_node) => leaf_node.intersects(ray), diff --git a/raydeon/src/camera.rs b/raydeon/src/camera.rs index 127fe33..e089863 100644 --- a/raydeon/src/camera.rs +++ b/raydeon/src/camera.rs @@ -68,10 +68,10 @@ impl Camera { } /// Chops a line segment into subsegments based on distance from camera - pub fn chop_segment<'a>( + pub fn chop_segment<'a, 's>( &self, - segment: &'a LineSegment3D, - ) -> Option> { + segment: &'a LineSegment3D<'s, WorldSpace>, + ) -> Option> { let p1 = segment.p1().to_vector(); let p2 = segment.p2().to_vector(); diff --git a/raydeon/src/lib.rs b/raydeon/src/lib.rs index 7474813..1ec0dee 100644 --- a/raydeon/src/lib.rs +++ b/raydeon/src/lib.rs @@ -1,14 +1,19 @@ +use bon::Builder; + #[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 models; pub mod path; pub mod ray; pub mod scene; pub mod shapes; +use std::fmt::Debug; +use std::ops::Deref; use std::sync::Arc; pub use camera::{Camera, CameraOptions}; @@ -49,7 +54,6 @@ pub type CCTransform = Transform3; pub trait Shape: Send + Sync + std::fmt::Debug { fn collision_geometry(&self) -> Option>>; - fn metadata(&self) -> Material; fn paths(&self, cam: &Camera) -> Vec>; } @@ -57,3 +61,50 @@ pub trait CollisionGeometry: Send + Sync + std::fmt::Debug { fn hit_by(&self, ray: &Ray) -> Option; fn bounding_box(&self) -> Option>; } + +#[derive(Debug, Clone, Builder)] +#[builder(start_fn(name = new))] +pub struct DrawableShape { + geometry: Arc, + material: Option, +} + +impl DrawableShape { + pub fn collision_geometry(&self) -> Option>> { + self.geometry.collision_geometry() + } + + pub fn paths(&self, cam: &Camera) -> Vec> { + self.geometry.paths(cam) + } + + pub fn material(&self) -> Option { + self.material + } +} + +#[derive(Debug, Clone, Builder)] +#[builder(start_fn(name = new))] +pub struct DrawableSegment<'s> { + pub segment: crate::path::LineSegment2D<'s, CameraSpace>, + pub kind: SegmentKind, +} + +impl<'s> Deref for DrawableSegment<'s> { + type Target = crate::path::LineSegment2D<'s, CameraSpace>; + + fn deref(&self) -> &Self::Target { + &self.segment + } +} + +#[derive(Debug, Copy, Clone)] +pub enum SegmentKind { + ScreenSpaceHatch(ScreenSpaceHatchKind), + Path, +} +#[derive(Debug, Copy, Clone)] +pub enum ScreenSpaceHatchKind { + Vertical, + Diagonal60, +} diff --git a/raydeon/src/lights.rs b/raydeon/src/lights.rs index 6b4373c..1841736 100644 --- a/raydeon/src/lights.rs +++ b/raydeon/src/lights.rs @@ -1,10 +1,9 @@ -use material::Material; +use ray::HitShape; use crate::*; pub trait Light: std::fmt::Debug + Send + Sync + 'static { - fn compute_illumination(&self, scene: &Scene, hitpoint: HitData, shape: &Arc) - -> f64; + fn compute_illumination<'s>(&self, scene: &'s Scene, hit_shape: HitShape<'s>) -> f64; } #[derive(Debug, Copy, Clone, Default)] @@ -19,26 +18,20 @@ pub struct PointLight { } impl Light for PointLight { - fn compute_illumination( - &self, - scene: &Scene, - hitpoint: HitData, - shape: &Arc, - ) -> f64 { - let _light_hitpoint = match self.light_hitpoint_for_hit(scene, hitpoint, shape) { + fn compute_illumination<'s>(&self, scene: &'s Scene, hit_shape: HitShape<'s>) -> f64 { + let _light_hitpoint = match self.light_hitpoint_for_hit(scene, hit_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); + illum += self.diffuse_illumination(hit_shape); + let specular = self.specular_illumination(hit_shape); tracing::debug!("specular: {}", specular); illum += specular; - let atten = self.attenuation(hitpoint); + let atten = self.attenuation(hit_shape); tracing::debug!("pre-attenuated illum: {}", illum); tracing::debug!("atten: {}", atten); let illum = illum * atten; @@ -92,13 +85,17 @@ impl PointLight { self.quadratic_attenuation } - fn diffuse_illumination(&self, hitpoint: HitData, material: &Material) -> f64 { + fn diffuse_illumination(&self, hit_shape: HitShape) -> f64 { + let hitpoint = &hit_shape.hit_data; + let material = hit_shape.hit_shape.material().unwrap_or_default(); 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 { + fn specular_illumination(&self, hit_shape: HitShape) -> f64 { + let hitpoint = hit_shape.hit_data; + let material = hit_shape.hit_shape.material().unwrap_or_default(); let to_light = (self.position - hitpoint.hit_point).normalize(); let v = hitpoint.hit_point.to_vector() * -1.0; @@ -111,20 +108,20 @@ impl PointLight { ps * blinn_phong } - fn attenuation(&self, hitpoint: HitData) -> f64 { - let distance = (self.position - hitpoint.hit_point).length(); + fn attenuation(&self, hitpoint: HitShape) -> f64 { + let distance = (self.position - hitpoint.hit_data.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( + fn light_hitpoint_for_hit<'s>( &self, - scene: &Scene, - hitpoint: HitData, - shape: &Arc, - ) -> Option { + scene: &'s Scene, + hit_shape: HitShape<'s>, + ) -> Option> { + let hitpoint = hit_shape.hit_data; let to_light = (self.position - hitpoint.hit_point).normalize(); if to_light.dot(hitpoint.normal) < 0.0 { return None; @@ -132,10 +129,12 @@ impl PointLight { 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) - }) + scene.intersects(light_ray).and_then(|light_hitpoint| { + Arc::ptr_eq( + &light_hitpoint.hit_shape.geometry, + &hit_shape.hit_shape.geometry, + ) + .then_some(light_hitpoint) + }) } } diff --git a/raydeon/src/material.rs b/raydeon/src/material.rs index d0c675f..d22e0ca 100644 --- a/raydeon/src/material.rs +++ b/raydeon/src/material.rs @@ -1,16 +1,22 @@ use crate::EPSILON; +use bon::{builder, Builder}; use float_cmp::{approx_eq, ApproxEq}; -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy, Default, Builder)] +#[builder(start_fn(name = new))] pub struct Material { + #[builder(default)] pub diffuse: f64, + #[builder(default)] pub specular: f64, + #[builder(default)] pub shininess: f64, + #[builder(default)] pub tag: usize, } impl Material { - pub fn new(diffuse: f64, specular: f64, shininess: f64, tag: usize) -> Self { + pub fn new_mat(diffuse: f64, specular: f64, shininess: f64, tag: usize) -> Self { Self { diffuse, specular, diff --git a/raydeon/src/path.rs b/raydeon/src/path.rs index e2e5144..b043cc4 100644 --- a/raydeon/src/path.rs +++ b/raydeon/src/path.rs @@ -1,11 +1,11 @@ -use crate::Material; +use crate::DrawableShape; use bon::Builder; use euclid::*; use std::collections::{BTreeSet, HashSet}; #[derive(Debug, Copy, Clone, Builder)] #[builder(start_fn(name = new))] -pub struct LineSegment3D +pub struct LineSegment3D<'s, Space> where Space: Copy + Clone + std::fmt::Debug, { @@ -18,29 +18,30 @@ where norm_dir: Vector3D, #[builder(skip = (p2 - p1).length())] length: f64, - material: Option, + + shape: Option<&'s DrawableShape>, } -impl LineSegment3D +impl<'s, Space> LineSegment3D<'s, Space> where Space: Copy + Clone + std::fmt::Debug, { + pub fn new_segment( + p1: impl Into>, + p2: impl Into>, + ) -> Self { + Self::new().p1(p1).p2(p2).build() + } + pub fn from_points( pairs: Vec<( impl Into>, impl Into>, )>, - mat: Option, - ) -> Vec> { + ) -> Vec { pairs .into_iter() - .map(|(p1, p2)| { - LineSegment3D::new() - .p1(p1.into()) - .p2(p2.into()) - .maybe_material(mat) - .build() - }) + .map(|(p1, p2)| LineSegment3D::new_segment(p1, p2)) .collect() } @@ -57,10 +58,6 @@ where self.p1 + (self.p2 - self.p1) / 2.0 } - pub fn material(&self) -> Material { - self.material.unwrap_or_default() - } - #[must_use] pub fn dir(&self) -> Vector3D { self.norm_dir @@ -71,33 +68,45 @@ where self.length } - pub fn cast_unit(self) -> LineSegment3D + pub fn cast_unit(self) -> LineSegment3D<'s, U> where U: Copy + Clone + std::fmt::Debug, { LineSegment3D::new() .p1(self.p1.cast_unit()) .p2(self.p2.cast_unit()) - .maybe_material(self.material) + .maybe_shape(self.shape) .build() } - pub fn xy(self) -> LineSegment2D { - LineSegment2D::new(self.p1.xy(), self.p2.xy()) + pub fn xy(self) -> LineSegment2D<'s, Space> { + LineSegment2D::new() + .p1(self.p1.xy()) + .p2(self.p2.xy()) + .maybe_shape(self.shape) + .build() } - pub fn yz(self) -> LineSegment2D { - LineSegment2D::new(self.p1.yz(), self.p2.yz()) + pub fn yz(self) -> LineSegment2D<'s, Space> { + LineSegment2D::new() + .p1(self.p1.yz()) + .p2(self.p2.yz()) + .maybe_shape(self.shape) + .build() } - pub fn xz(self) -> LineSegment2D { - LineSegment2D::new(self.p1.xz(), self.p2.xz()) + pub fn xz(self) -> LineSegment2D<'s, Space> { + LineSegment2D::new() + .p1(self.p1.xz()) + .p2(self.p2.xz()) + .maybe_shape(self.shape) + .build() } pub fn transform( &self, transformation: &Transform3D, - ) -> Option> + ) -> Option> where Dst: Copy + Clone + std::fmt::Debug, { @@ -109,41 +118,36 @@ where LineSegment3D::new() .p1(p1) .p2(p2) - .maybe_material(self.material) + .maybe_shape(self.shape) .build() }) }) } - pub fn transform_without_metadata( - &self, - transformation: &Transform3D, - ) -> Option> - where - Dst: Copy + Clone + std::fmt::Debug, - { - let (p1, p2) = (self.p1, self.p2); - let p1t = transformation.transform_point3d(p1); - let p2t = transformation.transform_point3d(p2); - p1t.and_then(|p1| p2t.map(|p2| LineSegment3D::new().p1(p1).p2(p2).build())) + pub fn set_shape(&mut self, shape: &'s DrawableShape) { + self.shape = Some(shape); + } + + pub fn get_shape(&self) -> Option<&DrawableShape> { + self.shape } } /// Created when a segment is chopped into several smaller pieces -pub struct SlicedSegment3D<'parent, Space> +pub struct SlicedSegment3D<'parent, 's, Space> where Space: Copy + Clone + std::fmt::Debug, { num_chops: usize, included: BTreeSet, - parent: &'parent LineSegment3D, + parent: &'parent LineSegment3D<'s, Space>, } -impl<'parent, Space> SlicedSegment3D<'parent, Space> +impl<'parent, 's, Space> SlicedSegment3D<'parent, 's, Space> where Space: Copy + Clone + std::fmt::Debug, { - pub fn new(num_chops: usize, parent: &'parent LineSegment3D) -> Self { + pub fn new(num_chops: usize, parent: &'parent LineSegment3D<'s, Space>) -> Self { let included = (0..num_chops).collect(); Self { num_chops, @@ -177,12 +181,15 @@ where self.included.remove(&ndx); } - pub fn join_slices(&self) -> Vec> { + 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> { + pub fn join_slices_with_forgiveness( + &self, + forgiveness: usize, + ) -> Vec> { if self.included.is_empty() { return Vec::new(); } @@ -232,43 +239,43 @@ where .map(|ndx_group| { let start = self.get_subsegment(*ndx_group.start()).p1; let end = self.get_subsegment(*ndx_group.end()).p2; - LineSegment3D::new() - .p1(start) - .p2(end) - .maybe_material(self.parent.material) - .build() + LineSegment3D::new().p1(start).p2(end).build() }) .collect() } } -#[derive(Debug, Copy, Clone)] -pub struct LineSegment2D +#[derive(Debug, Copy, Clone, Builder)] +#[builder(start_fn(name = new))] +pub struct LineSegment2D<'s, Space> where Space: Copy + Clone + std::fmt::Debug, { + #[builder(into)] pub p1: Point2D, + #[builder(into)] pub p2: Point2D, - pub tag: usize, + + shape: Option<&'s DrawableShape>, } -impl LineSegment2D +impl<'s, Space> LineSegment2D<'s, Space> where Space: Copy + Clone + std::fmt::Debug, { - pub fn new(p1: Point2D, p2: Point2D) -> Self { - Self::tagged(p1, p2, 0) - } - - pub fn tagged(p1: Point2D, p2: Point2D, tag: usize) -> Self { - Self { p1, p2, tag } + pub fn new_segment(p1: Point2D, p2: Point2D) -> Self { + Self::new().p1(p1).p2(p2).build() } - pub fn cast_unit(self) -> LineSegment2D + pub fn cast_unit(self) -> LineSegment2D<'s, U> where U: Copy + Clone + std::fmt::Debug, { - LineSegment2D::new(self.p1.cast_unit(), self.p2.cast_unit()) + LineSegment2D::new() + .p1(self.p1.cast_unit()) + .p2(self.p2.cast_unit()) + .maybe_shape(self.shape) + .build() } pub fn transform( @@ -278,11 +285,26 @@ where where Dst: Copy + Clone + std::fmt::Debug, { - let (p1, p2) = (self.p1, self.p2); - LineSegment2D::tagged( - transformation.transform_point(p1), - transformation.transform_point(p2), - self.tag, - ) + LineSegment2D::new() + .p1(transformation.transform_point(self.p1)) + .p2(transformation.transform_point(self.p2)) + .maybe_shape(self.shape) + .build() + } + + pub fn set_shape(&mut self, shape: &'s DrawableShape) { + self.shape = Some(shape); + } + + pub fn to_3d(&self) -> LineSegment3D<'s, Space> { + LineSegment3D::new() + .p1(self.p1.to_3d()) + .p2(self.p2.to_3d()) + .maybe_shape(self.shape) + .build() + } + + pub fn get_shape(&self) -> Option<&DrawableShape> { + self.shape } } diff --git a/raydeon/src/ray.rs b/raydeon/src/ray.rs index 41e9f68..68c2397 100644 --- a/raydeon/src/ray.rs +++ b/raydeon/src/ray.rs @@ -1,4 +1,4 @@ -use crate::{WPoint3, WVec3}; +use crate::{DrawableShape, WPoint3, WVec3}; #[cfg(test)] use euclid::approxeq::ApproxEq as EuclidApproxEq; @@ -37,6 +37,21 @@ pub struct HitData { pub normal: WVec3, } +#[derive(Debug, Clone, Copy)] +pub struct HitShape<'s> { + pub hit_data: HitData, + pub hit_shape: &'s DrawableShape, +} + +impl<'s> HitShape<'s> { + pub fn new(hit_data: HitData, hit_shape: &'s DrawableShape) -> HitShape<'s> { + HitShape { + hit_data, + hit_shape, + } + } +} + impl HitData { pub fn new(hit_point: impl Into, dist_to: f64, normal: impl Into) -> HitData { let hit_point = hit_point.into(); diff --git a/raydeon/src/scene.rs b/raydeon/src/scene.rs index 7b37a02..a8ebc87 100644 --- a/raydeon/src/scene.rs +++ b/raydeon/src/scene.rs @@ -5,6 +5,7 @@ use euclid::{Point2D, Vector2D}; use path::{LineSegment2D, SlicedSegment3D}; use rand::distributions::Distribution; use rand::SeedableRng; +use ray::HitShape; use rayon::prelude::*; use std::sync::Arc; use tracing::info; @@ -22,7 +23,7 @@ pub struct Scene { #[derive(Debug)] pub struct SceneGeometry { - geometry: Vec>, + geometry: Vec, bvh: BVHTree, } @@ -31,26 +32,26 @@ impl SceneGeometry { Default::default() } - pub fn with_geometry(mut self, geometry: Vec>) -> Self { + 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 { + pub fn push_geometry(mut self, geometry: DrawableShape) -> Self { self.geometry.push(geometry); self.bvh = Self::create_bvh(&self.geometry); self } - pub fn concat_geometry(mut self, geometry: &[Arc]) -> Self { + pub fn concat_geometry(mut self, geometry: &[DrawableShape]) -> Self { self.geometry.extend_from_slice(geometry); self.bvh = Self::create_bvh(&self.geometry); self } - fn create_bvh(geometry: &[Arc]) -> BVHTree { + fn create_bvh(geometry: &[DrawableShape]) -> BVHTree { let collision_geometry: Vec<_> = geometry .iter() .filter_map(|s| { @@ -82,14 +83,25 @@ impl From>> for SceneGeometry { SceneGeometry::new().with_geometry( geometry .into_iter() - .map(|s| s as Arc) + .map(|s| DrawableShape::new().geometry(s as Arc).build()) .collect::>(), ) } } impl From>> for SceneGeometry { - fn from(geometry: Vec>) -> Self { + fn from(shapes: Vec>) -> Self { + SceneGeometry::new().with_geometry( + shapes + .into_iter() + .map(|shapes| DrawableShape::new().geometry(shapes).build()) + .collect(), + ) + } +} + +impl From> for SceneGeometry { + fn from(geometry: Vec) -> Self { SceneGeometry::new().with_geometry(geometry) } } @@ -148,7 +160,7 @@ impl Scene { } /// Find's the closest intersection point to geometry in the scene, if any - pub(crate) fn intersects(&self, ray: Ray) -> Option<(HitData, Arc)> { + pub(crate) fn intersects(&self, ray: Ray) -> Option { self.geometry.bvh.intersects(ray) } @@ -158,8 +170,8 @@ impl Scene { let r = Ray::new(point, v.normalize()); match self.intersects(r) { - Some((hitdata, _)) => { - let diff = (hitdata.dist_to - v.length()).abs(); + Some(hit_shape) => { + let diff = (hit_shape.hit_data.dist_to - v.length()).abs(); diff < 1.0e-1 } None => true, @@ -174,8 +186,8 @@ pub struct SceneCamera<'s> { seed: Option, } -impl<'a> SceneCamera<'a> { - pub fn new(camera: Camera, scene: &'a Scene) -> Self { +impl<'s> SceneCamera<'s> { + pub fn new(camera: Camera, scene: &'s Scene) -> Self { SceneCamera { camera, scene, @@ -183,25 +195,32 @@ impl<'a> SceneCamera<'a> { } } - pub fn with_seed(mut self, seed: u64) -> Self { + pub fn with_seed(mut self, seed: u64) -> SceneCamera<'s> { self.seed = Some(seed); self } + fn geometry_paths(&self) -> Vec> { + self.scene + .geometry + .geometry + .iter() + .flat_map(|shape| { + let mut paths = shape.paths(&self.camera); + paths.iter_mut().for_each(|path| path.set_shape(shape)); + paths + }) + .collect() + } + fn clip_filter(&self, path: &LineSegment3D) -> bool { self.scene .visible(self.camera.observation.eye, path.midpoint()) } - pub fn render(&self) -> Vec> { + pub fn render(&self) -> Vec> { info!("Querying geometry for subpaths"); - let parent_paths: Vec> = self - .scene - .geometry - .geometry - .iter() - .flat_map(|s| s.paths(&self.camera)) - .collect(); + let parent_paths = self.geometry_paths(); info!( "Caching line segment chunks based on camera position, starting with {} segments", @@ -210,11 +229,11 @@ impl<'a> SceneCamera<'a> { let mut paths: Vec> = parent_paths .iter() - .filter_map(|path| self.camera.chop_segment(path)) + .filter_map(|segment| self.camera.chop_segment(segment)) .collect(); let path_count: usize = paths - .par_iter() + .iter() .map(|subsegments| subsegments.num_subsegments()) .sum(); @@ -228,7 +247,7 @@ impl<'a> SceneCamera<'a> { let transformation: Transform3 = self.camera.camera_transformation(); - let paths: Vec<_> = paths + let paths: Vec> = paths .par_iter_mut() .flat_map(|path_group| { let to_remove: Vec = path_group @@ -246,23 +265,23 @@ impl<'a> SceneCamera<'a> { .for_each(|ndx| path_group.remove_subsegment(ndx)); path_group.join_slices() }) - .filter_map(|path| path.transform(&transformation)) - .map(LineSegment3D::xy) + .filter_map(|path| path.transform(&transformation).map(LineSegment3D::xy)) .collect(); info!("{} paths remain after clipping", paths.len()); paths + .into_iter() + .map(|segment| { + DrawableSegment::new() + .segment(segment) + .kind(SegmentKind::Path) + .build() + }) + .collect() } -} - -pub struct LitScene { - pub geometry_paths: Vec>, - pub hatch_paths: Vec>, -} -impl<'a> SceneCamera<'a> { - pub fn render_with_lighting(&self) -> LitScene { + pub fn render_with_lighting(&self) -> Vec> { let geometry_paths = self.render(); let mut rng = match self.seed { Some(seed) => rand::rngs::StdRng::seed_from_u64(seed), @@ -270,40 +289,53 @@ impl<'a> SceneCamera<'a> { }; 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 * self.camera.render_options.vert_hatch_brightness_scaling) - }); + let vert_lines = self + .filter_hatch_lines_by(&self.vertical_hatch_lines(), |brightness| { + let threshold: f64 = rand::distributions::Standard.sample(&mut rng); + brightness > (threshold * self.camera.render_options.vert_hatch_brightness_scaling) + }) + .into_iter() + .map(|vert_segment| { + DrawableSegment::new() + .segment(vert_segment) + .kind(SegmentKind::ScreenSpaceHatch( + ScreenSpaceHatchKind::Vertical, + )) + .build() as DrawableSegment<'s> + }) + .collect::>(); + 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 * self.camera.render_options.diag_hatch_brightness_scaling) - }); + let diag_lines = self + .filter_hatch_lines_by(&self.diagonal_hatch_lines(), |brightness| { + let threshold: f64 = rand::distributions::Standard.sample(&mut rng); + brightness > (threshold * self.camera.render_options.diag_hatch_brightness_scaling) + }) + .into_iter() + .map(|diag_segment| { + DrawableSegment::new() + .segment(diag_segment) + .kind(SegmentKind::ScreenSpaceHatch( + ScreenSpaceHatchKind::Diagonal60, + )) + .build() as DrawableSegment<'s> + }) + .collect::>(); info!("Generated {} diagonal hatch lines.", diag_lines.len()); - let hatch_paths = [vert_lines, diag_lines].concat(); - - LitScene { - geometry_paths, - hatch_paths, - } + [geometry_paths, vert_lines, diag_lines].concat() } fn filter_hatch_lines_by( &self, - segments: &[LineSegment2D], + segments: &[LineSegment2D<'s, CameraSpace>], mut filter: impl FnMut(f64) -> bool, - ) -> Vec> { + ) -> Vec> { let segments = segments .iter() - .map(|segment| { - LineSegment3D::new() - .p1(segment.p1.to_3d()) - .p2(segment.p2.to_3d()) - .build() - }) + .map(LineSegment2D::to_3d) .collect::>(); let mut split_segments = segments .iter() @@ -336,15 +368,15 @@ impl<'a> SceneCamera<'a> { self.camera.render_options.hatch_slice_forgiveness, ) }) - .map(|path| LineSegment2D::new(path.p1().to_2d(), path.p2().to_2d())) + .map(LineSegment3D::xy) .collect::>(); paths } fn lighting_for_ray(&self, ray: Ray) -> Option { - let lighting = self.scene.intersects(ray).and_then(|(hitpoint, shape)| { - if hitpoint.dist_to > self.camera.perspective.zfar { + let lighting = self.scene.intersects(ray).and_then(|hit_shape| { + if hit_shape.hit_data.dist_to > self.camera.perspective.zfar { return None; } Some( @@ -352,7 +384,7 @@ impl<'a> SceneCamera<'a> { .lighting .lights .iter() - .map(|light| light.compute_illumination(self.scene, hitpoint, &shape)) + .map(|light| light.compute_illumination(self.scene, hit_shape)) .sum::() + self.scene.lighting.ambient, ) @@ -362,7 +394,7 @@ impl<'a> SceneCamera<'a> { } // https://smashingpencilsart.com/how-do-you-hatch-with-a-pen/ - fn vertical_hatch_lines(&self) -> Vec> { + fn vertical_hatch_lines(&self) -> Vec> { let initial_offset = self.camera.render_options.hatch_pixel_spacing / 2.0; let mut segments = Vec::new(); @@ -372,13 +404,13 @@ impl<'a> SceneCamera<'a> { 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)); + segments.push(LineSegment2D::new_segment(start, end)); x += self.camera.render_options.hatch_pixel_spacing; } segments } - fn diagonal_hatch_lines(&self) -> Vec> { + fn diagonal_hatch_lines(&self) -> Vec> { let initial_offset = self.camera.render_options.hatch_pixel_spacing / 2.0; let mut segments = Vec::new(); @@ -424,7 +456,7 @@ impl<'a> SceneCamera<'a> { let p1 = coll_aabb.intersection(&r1).unwrap(); let p2 = coll_aabb.intersection(&r2).unwrap(); - segments.push(LineSegment2D::new( + segments.push(LineSegment2D::new_segment( Point2D::new(p1.x, p1.y), Point2D::new(p2.x, p2.y), )); diff --git a/raydeon/src/shapes/aacuboid.rs b/raydeon/src/shapes/aacuboid.rs index 5d74f56..227b513 100644 --- a/raydeon/src/shapes/aacuboid.rs +++ b/raydeon/src/shapes/aacuboid.rs @@ -5,9 +5,7 @@ use euclid::Vector3D; use std::sync::Arc; use crate::path::LineSegment3D; -use crate::{ - Camera, CollisionGeometry, HitData, Material, Ray, Shape, WPoint3, WVec3, WorldSpace, AABB3, -}; +use crate::{Camera, CollisionGeometry, HitData, Ray, Shape, WPoint3, WVec3, WorldSpace, AABB3}; #[derive(Debug, Copy, Clone, Builder)] #[builder(start_fn(name = new))] @@ -17,7 +15,6 @@ pub struct AxisAlignedCuboid { pub min: WVec3, #[builder(into)] pub max: WVec3, - pub material: Option, } impl From> for AxisAlignedCuboid { @@ -30,10 +27,6 @@ impl From> for AxisAlignedCuboid { } impl Shape for AxisAlignedCuboid { - fn metadata(&self) -> Material { - self.material.unwrap_or_default() - } - fn collision_geometry(&self) -> Option>> { Some(vec![Arc::new(*self)]) } @@ -56,23 +49,20 @@ impl Shape for AxisAlignedCuboid { let p7 = WPoint3::new(x2, y2, z2); let p8 = WPoint3::new(x1, y2, z2); - LineSegment3D::from_points( - vec![ - (p1, p2), - (p2, p3), - (p3, p4), - (p4, p1), - (p5, p6), - (p6, p7), - (p7, p8), - (p8, p5), - (p1, p5), - (p2, p6), - (p3, p7), - (p4, p8), - ], - self.material, - ) + LineSegment3D::from_points(vec![ + (p1, p2), + (p2, p3), + (p3, p4), + (p4, p1), + (p5, p6), + (p6, p7), + (p7, p8), + (p8, p5), + (p1, p5), + (p2, p6), + (p3, p7), + (p4, p8), + ]) } } diff --git a/raydeon/src/shapes/quad.rs b/raydeon/src/shapes/quad.rs index 9514aef..0886c39 100644 --- a/raydeon/src/shapes/quad.rs +++ b/raydeon/src/shapes/quad.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use super::Triangle; use crate::path::LineSegment3D; -use crate::{Camera, CollisionGeometry, Material, Shape, WPoint3, WVec3, WorldSpace}; +use crate::{Camera, CollisionGeometry, Shape, WPoint3, WVec3, WorldSpace}; #[derive(Debug, Copy, Clone, Builder)] #[builder(start_fn(name = new))] @@ -18,7 +18,6 @@ pub struct Quad { })] pub basis: [WVec3; 2], pub dims: [f64; 2], - material: Option, #[builder(skip = [ origin, @@ -30,10 +29,6 @@ pub struct Quad { } impl Shape for Quad { - fn metadata(&self) -> Material { - self.material.unwrap_or_default() - } - fn collision_geometry(&self) -> Option>> { Some(vec![ Arc::new( @@ -67,9 +62,6 @@ impl Shape for Quad { .map(|v| v + (v.to_vector() - centroid).normalize() * 0.0015) .collect::>(); - LineSegment3D::from_points( - vec![(v[0], v[1]), (v[1], v[2]), (v[2], v[3]), (v[3], v[0])], - self.material, - ) + LineSegment3D::from_points(vec![(v[0], v[1]), (v[1], v[2]), (v[2], v[3]), (v[3], v[0])]) } } diff --git a/raydeon/src/shapes/triangle.rs b/raydeon/src/shapes/triangle.rs index 544c7bd..00a4da4 100644 --- a/raydeon/src/shapes/triangle.rs +++ b/raydeon/src/shapes/triangle.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use super::plane::Plane; use crate::path::LineSegment3D; -use crate::{Camera, CollisionGeometry, HitData, Material, Ray, Shape, WPoint3, WVec3, WorldSpace}; +use crate::{Camera, CollisionGeometry, HitData, Ray, Shape, WPoint3, WVec3, WorldSpace}; #[derive(Debug, Copy, Clone, Builder)] #[builder(start_fn(name = new))] @@ -29,15 +29,9 @@ pub struct Triangle { .build() )] pub plane: Plane, - - pub material: Option, } impl Shape for Triangle { - fn metadata(&self) -> Material { - self.material.unwrap_or_default() - } - fn collision_geometry(&self) -> Option>> { Some(vec![Arc::new(*self)]) } @@ -52,7 +46,7 @@ impl Shape for Triangle { let v1 = v1 + (v1 - centroid).normalize() * 0.015; let v2 = v2 + (v2 - centroid).normalize() * 0.015; - LineSegment3D::from_points(vec![(v0, v1), (v1, v2), (v2, v0)], self.material) + LineSegment3D::from_points(vec![(v0, v1), (v1, v2), (v2, v0)]) } }