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..d2b3e2f --- /dev/null +++ b/pyraydeon/examples/py_sphere.py @@ -0,0 +1,140 @@ +import math +import svg +import numpy as np + +from pyraydeon import ( + Camera, + Point3, + Scene, + Sphere, + Vec3, + Geometry, + Material, + PointLight, + Plane, + LineSegment3D, +) + + +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): + hyp = np.linalg.norm(self.sphere.center - cam.eye) + opp = self.sphere.radius + + theta = math.asin(opp / hyp) + adj = opp / math.tan(theta) + d = math.cos(theta) * adj + r = math.sin(theta) * adj + + w = self.sphere.center - cam.eye + w = w / np.linalg.norm(w) + + u = np.cross(w, cam.up) + u = u / np.linalg.norm(u) + + v = np.cross(w, u) + v = v / np.linalg.norm(v) + + points = [] + c = cam.eye + d * w + for i in range(0, 180): + a = math.radians(float(i)) + p = c + p = p + u * (math.cos(a) * r) + p = p + v * (math.sin(a) * r) + + push = p - self.sphere.center + push = push / np.linalg.norm(push) + + p += push * 0.00015 + points.append(p) + + paths = [] + for i in range(0, len(points) - 1): + paths.append(LineSegment3D(points[i], points[(i + 1)])) + + return paths + + +class PyPlane(Geometry): + def __init__(self, point, normal, material=None): + if material is not None: + self._material = material + + self.plane = Plane(point, normal) + + @property + def material(self): + return self._material + + def collision_geometry(self): + return [self.plane] + + def paths(self, cam): + return [] + + +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)), + ], + lights=[PointLight((4, 3, 10), 4.0, 2.0, 0.15, 0.4, 0.11)], +) + + +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 = 100.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..0eaf889 --- /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..b82286d 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,10 +33,55 @@ 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() } + #[getter] + fn eye<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.0.observation.eye.to_array()) + } + + #[getter] + fn focus<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.0.observation.center.to_array()) + } + + #[getter] + fn up<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.0.observation.up.to_array()) + } + + #[getter] + fn fovy(&self) -> f64 { + self.0.perspective.fovy + } + + #[getter] + fn width(&self) -> usize { + self.0.perspective.width + } + + #[getter] + fn height(&self) -> usize { + self.0.perspective.height + } + + #[getter] + fn aspect(&self) -> f64 { + self.0.perspective.aspect + } + + #[getter] + fn znear(&self) -> f64 { + self.0.perspective.znear + } + + #[getter] + fn zfar(&self) -> f64 { + self.0.perspective.zfar + } + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { let class_name = slf.get_type().qualname()?; Ok(format!("{}<{:?}>", class_name, slf.borrow().0)) @@ -44,23 +90,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 +141,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 +198,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..b064b25 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 numpy::{PyArrayLike1, PyArrayLike2}; +use crate::material::Material; +use numpy::{Ix1, PyArray, 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)) } } @@ -122,21 +130,72 @@ impl Plane { ); Ok((Self(shape), geom)) } + + #[getter] + fn point<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.0.point.to_array()) + } + + #[getter] + fn normal<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.0.normal.to_array()) + } +} + +#[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)) + } + + #[getter] + fn center<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.0.center.to_array()) + } + + #[getter] + fn radius(&self) -> f64 { + self.0.radius + } } #[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 +203,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 +245,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..88af416 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,175 @@ 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); + (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).and_then(|(hitpoint, shape)| { + if hitpoint.dist_to > self.camera.perspective.zfar { + return None; + } + Some( + 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..e0769da 100644 --- a/raydeon/src/shapes/sphere.rs +++ b/raydeon/src/shapes/sphere.rs @@ -6,9 +6,9 @@ use crate::{CollisionGeometry, HitData, Ray, WPoint3, WVec3, WorldSpace}; /// A sphere at an arbitrary location in 3d space. pub struct Sphere { /// The location of the center of the sphere. - center: WPoint3, + pub center: WPoint3, /// The radius of the sphere. - radius: f64, + pub radius: f64, /// Precomputed radius squared. radius2: f64, } @@ -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) + )) ); } }