diff --git a/Cargo.toml b/Cargo.toml index 0eb7026..7cb1c17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ raydeon = { path = "./raydeon", version = "0.2" } pyrayeon = { path = "./pyraydeon", version = "0.1.0-alpha" } anyhow = "1" +bon = "3" cgmath = "0.17" collision = "0.20" env_logger = "0.11" diff --git a/pyraydeon/src/lib.rs b/pyraydeon/src/lib.rs index 991bb6c..61570fb 100644 --- a/pyraydeon/src/lib.rs +++ b/pyraydeon/src/lib.rs @@ -2,7 +2,7 @@ use pyo3::prelude::*; macro_rules! pywrap { ($name:ident, $wraps:ty) => { - #[derive(Debug, Clone, Copy)] + #[derive(Debug, Clone)] #[pyclass(frozen)] pub(crate) struct $name(pub(crate) $wraps); diff --git a/pyraydeon/src/scene.rs b/pyraydeon/src/scene.rs index 82c09bb..0ea1172 100644 --- a/pyraydeon/src/scene.rs +++ b/pyraydeon/src/scene.rs @@ -9,7 +9,23 @@ use crate::linear::{ArbitrarySpace, Point2, Point3, Vec3}; use crate::material::Material; use crate::shapes::Geometry; -pywrap!(Camera, raydeon::Camera); +#[derive(Debug, Clone)] +#[pyclass(frozen)] +pub(crate) struct Camera(pub(crate) raydeon::Camera); + +impl ::std::ops::Deref for Camera { + type Target = raydeon::Camera; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for Camera { + fn from(value: raydeon::Camera) -> Self { + Self(value) + } +} #[pymethods] impl Camera { @@ -29,12 +45,16 @@ impl Camera { let up = Vec3::try_from(up)?; Ok(self .0 + .clone() .look_at(eye.cast_unit(), center.cast_unit(), up.cast_unit()) .into()) } fn perspective(&self, fovy: f64, width: usize, height: usize, znear: f64, zfar: f64) -> Camera { - self.0.perspective(fovy, width, height, znear, zfar).into() + self.0 + .clone() + .perspective(fovy, width, height, znear, zfar) + .into() } #[getter] @@ -137,7 +157,7 @@ impl Scene { fn render(&self, py: Python, camera: &Camera) -> Vec { py.allow_threads(|| { - let cam = self.scene.attach_camera(camera.0); + let cam = self.scene.attach_camera(camera.0.clone()); cam.render() .into_iter() .map(|ls| ls.cast_unit().into()) @@ -153,7 +173,7 @@ impl Scene { seed: Option, ) -> Vec { py.allow_threads(|| { - let cam = self.scene.attach_camera(camera.0); + let cam = self.scene.attach_camera(camera.0.clone()); let cam = if let Some(seed) = seed { cam.with_seed(seed) } else { diff --git a/pyraydeon/src/shapes/mod.rs b/pyraydeon/src/shapes/mod.rs index 8d18d08..c571691 100644 --- a/pyraydeon/src/shapes/mod.rs +++ b/pyraydeon/src/shapes/mod.rs @@ -206,7 +206,7 @@ impl raydeon::Shape for PythonGeometry ) -> Vec> { let segments: Option<_> = Python::with_gil(|py| { let inner = self.slf.bind(py); - let cam = Camera::from(*cam); + let cam = Camera::from(cam.clone()); let call_result = inner.call_method1("paths", (cam,)).unwrap(); let segments = call_result @@ -238,7 +238,7 @@ impl raydeon::Shape for PythonGeometry impl raydeon::CollisionGeometry for PythonGeometry { fn hit_by(&self, ray: &raydeon::Ray) -> Option { - if let PythonGeometryKind::Collision { aabb: Some(aabb) } = self.kind { + if let PythonGeometryKind::Collision { aabb: Some(aabb) } = &self.kind { raydeon::shapes::AxisAlignedCuboid::from(aabb.0.cast_unit()).hit_by(ray)?; } Python::with_gil(|py| { diff --git a/raydeon/Cargo.toml b/raydeon/Cargo.toml index ada7f5a..7632d77 100644 --- a/raydeon/Cargo.toml +++ b/raydeon/Cargo.toml @@ -5,6 +5,7 @@ authors = ["cbgbt "] edition = "2021" [dependencies] +bon.workspace = true cgmath.workspace = true collision.workspace = true euclid.workspace = true diff --git a/raydeon/src/camera.rs b/raydeon/src/camera.rs index 9b793f8..48a1268 100644 --- a/raydeon/src/camera.rs +++ b/raydeon/src/camera.rs @@ -3,100 +3,48 @@ use path::SlicedSegment3D; use crate::*; -#[derive(Debug, Copy, Clone)] -pub struct Observation { - pub eye: WPoint3, - pub center: WVec3, - pub up: WVec3, -} - -impl Default for Observation { - fn default() -> Self { - Self::new((0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0)) - } -} - -/// Type parameter for a camera that isn't yet looking anywhere -#[derive(Debug, Copy, Clone)] -pub struct NoObservation; - -impl Observation { - pub fn new(eye: impl Into, center: impl Into, up: impl Into) -> Self { - let eye = eye.into(); - let center = center.into(); - let up = up.into().normalize(); - - Self { eye, center, up } - } - - #[rustfmt::skip] - pub fn look_matrix(&self) -> WCTransform { - let Observation { eye, center, up, .. } = *self; - let f = (center - eye.to_vector()).normalize(); - let s = f.cross(up).normalize(); - let u = s.cross(f).normalize(); - - CWTransform::from_array( - // euclid used to let us specify things in column major order and now it doesn't. - // So we're just transposing it here. - CWTransform::new( - s.x, u.x, -f.x, eye.x, - s.y, u.y, -f.y, eye.y, - s.z, u.z, -f.z, eye.z, - 0.0, 0.0, 0.0, 1.0, - ) - .to_array_transposed(), - ) - .inverse() - .unwrap() - } +#[derive(Debug, Clone)] +pub struct Camera { + pub observation: O, + pub perspective: P, + pub render_options: CameraOptions, } -#[derive(Debug, Copy, Clone)] -pub struct Perspective { - pub fovy: f64, - pub width: usize, - pub height: usize, - pub aspect: f64, - pub znear: f64, - pub zfar: f64, +#[derive(Debug, Clone)] +pub struct CameraOptions { + pub pen_px_size: f64, + pub hatch_pixel_spacing: f64, + pub hatch_pixel_chop_factor: f64, + pub hatch_slice_forgiveness: usize, + pub vert_hatch_brightness_scaling: f64, + pub diag_hatch_brightness_scaling: f64, } -impl Default for Perspective { +impl Default for CameraOptions { fn default() -> Self { - Self::new(45.0, 1920, 1080, 0.1, 100.0) + Self::defaults_for_pen_px_size(4.0) } } -/// Type parameter for a camera that doesn't yet have a defined perspective -#[derive(Debug, Copy, Clone)] -pub struct NoPerspective; - -impl Perspective { - pub fn new(fovy: f64, width: usize, height: usize, znear: f64, zfar: f64) -> Self { - let aspect = width as f64 / height as f64; +impl CameraOptions { + pub fn defaults_for_pen_px_size(px_size: f64) -> Self { Self { - fovy, - width, - height, - aspect, - znear, - zfar, + pen_px_size: px_size, + hatch_pixel_spacing: px_size * 2.0, + hatch_pixel_chop_factor: px_size / 3.0, + hatch_slice_forgiveness: 1, + vert_hatch_brightness_scaling: 0.8, + diag_hatch_brightness_scaling: 0.46, } } } -#[derive(Debug, Copy, Clone)] -pub struct Camera { - pub observation: O, - pub perspective: P, -} - impl Camera { pub fn new() -> Self { Self { observation: NoObservation, perspective: NoPerspective, + render_options: Default::default(), } } } @@ -106,6 +54,7 @@ impl Default for Camera { Self { observation: Observation::default(), perspective: Perspective::default(), + render_options: Default::default(), } } } @@ -117,11 +66,16 @@ impl Camera { center: impl Into, up: impl Into, ) -> Camera { - let Camera { perspective, .. } = self; + let Camera { + perspective, + render_options, + .. + } = self; let observation = Observation::new(eye.into(), center.into(), up.into()); Camera { observation, perspective, + render_options, } } @@ -133,11 +87,16 @@ impl Camera { znear: f64, zfar: f64, ) -> Camera { - let Camera { observation, .. } = self; + let Camera { + observation, + render_options, + .. + } = self; let perspective = Perspective::new(fovy, width, height, znear, zfar); Camera { observation, perspective, + render_options, } } } @@ -185,7 +144,8 @@ impl Camera { let chunk_count = canvas_points .map(|(p1t, p2t)| { - let rough_chop_size = (p2t - p1t).length() / (PEN_PX_SIZE / 2.0); + let rough_chop_size = + (p2t - p1t).length() / (self.render_options.pen_px_size / 2.0); rough_chop_size.round_ties_even() as usize }) .unwrap_or_else(|| { @@ -233,6 +193,89 @@ impl Camera { } } +#[derive(Debug, Copy, Clone)] +pub struct Observation { + pub eye: WPoint3, + pub center: WVec3, + pub up: WVec3, +} + +impl Default for Observation { + fn default() -> Self { + Self::new((0.0, 0.0, 1.0), (0.0, 0.0, 0.0), (0.0, 1.0, 0.0)) + } +} + +/// Type parameter for a camera that isn't yet looking anywhere +#[derive(Debug, Copy, Clone)] +pub struct NoObservation; + +impl Observation { + pub fn new(eye: impl Into, center: impl Into, up: impl Into) -> Self { + let eye = eye.into(); + let center = center.into(); + let up = up.into().normalize(); + + Self { eye, center, up } + } + + #[rustfmt::skip] + pub fn look_matrix(&self) -> WCTransform { + let Observation { eye, center, up, .. } = *self; + let f = (center - eye.to_vector()).normalize(); + let s = f.cross(up).normalize(); + let u = s.cross(f).normalize(); + + CWTransform::from_array( + // euclid used to let us specify things in column major order and now it doesn't. + // So we're just transposing it here. + CWTransform::new( + s.x, u.x, -f.x, eye.x, + s.y, u.y, -f.y, eye.y, + s.z, u.z, -f.z, eye.z, + 0.0, 0.0, 0.0, 1.0, + ) + .to_array_transposed(), + ) + .inverse() + .unwrap() + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Perspective { + pub fovy: f64, + pub width: usize, + pub height: usize, + pub aspect: f64, + pub znear: f64, + pub zfar: f64, +} + +impl Default for Perspective { + fn default() -> Self { + Self::new(45.0, 1920, 1080, 0.1, 100.0) + } +} + +/// Type parameter for a camera that doesn't yet have a defined perspective +#[derive(Debug, Copy, Clone)] +pub struct NoPerspective; + +impl Perspective { + 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, + height, + aspect, + znear, + zfar, + } + } +} + #[rustfmt::skip] fn frustum( l: f64, diff --git a/raydeon/src/lib.rs b/raydeon/src/lib.rs index c14076d..1095bad 100644 --- a/raydeon/src/lib.rs +++ b/raydeon/src/lib.rs @@ -17,10 +17,6 @@ pub use path::{LineSegment3D, PathMeta}; pub use ray::{HitData, Ray}; pub use scene::{Scene, SceneGeometry, SceneLighting}; -// 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; diff --git a/raydeon/src/scene.rs b/raydeon/src/scene.rs index 4c17480..a60e007 100644 --- a/raydeon/src/scene.rs +++ b/raydeon/src/scene.rs @@ -12,12 +12,6 @@ use tracing::info; use crate::*; -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, @@ -210,6 +204,7 @@ where } } +#[derive(Debug)] pub struct SceneCamera<'s, P, L> where P: PathMeta, @@ -324,14 +319,14 @@ impl<'a> SceneCamera<'a, Material, SceneLighting> { 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) + brightness > (threshold * self.camera.render_options.vert_hatch_brightness_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) + brightness > (threshold * self.camera.render_options.diag_hatch_brightness_scaling) }); info!("Generated {} diagonal hatch lines.", diag_lines.len()); @@ -357,7 +352,9 @@ impl<'a> SceneCamera<'a, Material, SceneLighting> { let mut split_segments = segments .iter() .map(|segment| { - let num_chops = (segment.length().ceil() / HATCH_PIXEL_CHOP_SIZE) as usize; + let num_chops = (segment.length().ceil() + / self.camera.render_options.hatch_pixel_chop_factor) + as usize; SlicedSegment3D::new(num_chops, segment) }) .collect::>(); @@ -379,7 +376,9 @@ impl<'a> SceneCamera<'a, Material, SceneLighting> { to_remove .into_iter() .for_each(|ndx| path_group.remove_subsegment(ndx)); - path_group.join_slices_with_forgiveness(HATCHING_SLICE_FORGIVENESS) + path_group.join_slices_with_forgiveness( + self.camera.render_options.hatch_slice_forgiveness, + ) }) .map(|path| LineSegment2D::new(path.p1().to_2d(), path.p2().to_2d())) .collect::>(); @@ -408,7 +407,7 @@ impl<'a> SceneCamera<'a, Material, SceneLighting> { // https://smashingpencilsart.com/how-do-you-hatch-with-a-pen/ fn vertical_hatch_lines(&self) -> Vec> { - let initial_offset = HATCH_SPACING / 2.0; + let initial_offset = self.camera.render_options.hatch_pixel_spacing / 2.0; let mut segments = Vec::new(); @@ -418,13 +417,13 @@ impl<'a> SceneCamera<'a, Material, SceneLighting> { 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; + x += self.camera.render_options.hatch_pixel_spacing; } segments } fn diagonal_hatch_lines(&self) -> Vec> { - let initial_offset = HATCH_SPACING / 2.0; + let initial_offset = self.camera.render_options.hatch_pixel_spacing / 2.0; let mut segments = Vec::new(); @@ -474,7 +473,7 @@ impl<'a> SceneCamera<'a, Material, SceneLighting> { Point2D::new(p2.x, p2.y), )); - dist += HATCH_SPACING; + dist += self.camera.render_options.hatch_pixel_spacing; curr_point = diagonal * dist; }