diff --git a/pyraydeon/examples/py_cubes.py b/pyraydeon/examples/py_cubes.py index 2d62a8d..664dcf3 100644 --- a/pyraydeon/examples/py_cubes.py +++ b/pyraydeon/examples/py_cubes.py @@ -188,7 +188,7 @@ def bounding_box(self): znear = 0.1 zfar = 10.0 -cam = Camera.look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar) +cam = Camera().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar) paths = scene.render(cam) diff --git a/pyraydeon/examples/py_rhombohedron.py b/pyraydeon/examples/py_rhombohedron.py index e93b100..acb01c6 100644 --- a/pyraydeon/examples/py_rhombohedron.py +++ b/pyraydeon/examples/py_rhombohedron.py @@ -122,7 +122,7 @@ def paths(self, cam): znear = 0.1 zfar = 20.0 -cam = Camera.look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar) +cam = Camera().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar) paths = scene.render(cam) diff --git a/pyraydeon/examples/triangles.py b/pyraydeon/examples/triangles.py index 275d38c..12325cf 100644 --- a/pyraydeon/examples/triangles.py +++ b/pyraydeon/examples/triangles.py @@ -45,7 +45,7 @@ def bounding_box(self): znear = 0.1 zfar = 10.0 -cam = Camera.look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar) +cam = Camera().look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar) paths = scene.render(cam) diff --git a/pyraydeon/src/scene.rs b/pyraydeon/src/scene.rs index 5d9cc6c..51b8cf7 100644 --- a/pyraydeon/src/scene.rs +++ b/pyraydeon/src/scene.rs @@ -7,32 +7,30 @@ use raydeon::WorldSpace; use crate::linear::{ArbitrarySpace, Point2, Point3, Vec3}; use crate::shapes::Geometry; -pywrap!(Camera, raydeon::Camera); +pywrap!(Camera, raydeon::Camera); #[pymethods] impl Camera { - #[staticmethod] + #[new] + fn new() -> Self { + raydeon::Camera::default().into() + } + fn look_at( + &self, eye: &Bound<'_, PyAny>, center: &Bound<'_, PyAny>, up: &Bound<'_, PyAny>, - ) -> PyResult { + ) -> PyResult { let eye = Point3::try_from(eye)?; let center = Vec3::try_from(center)?; let up = Vec3::try_from(up)?; - Ok(raydeon::Camera::look_at(eye.cast_unit(), center.cast_unit(), up.cast_unit()).into()) + Ok(self + .0 + .look_at(eye.cast_unit(), center.cast_unit(), up.cast_unit()) + .into()) } - fn __repr__(slf: &Bound<'_, Self>) -> PyResult { - let class_name = slf.get_type().qualname()?; - Ok(format!("{}<{:?}>", class_name, slf.borrow().0)) - } -} - -pywrap!(LookingCamera, raydeon::scene::LookingCamera); - -#[pymethods] -impl LookingCamera { fn perspective(&self, fovy: f64, width: f64, height: f64, znear: f64, zfar: f64) -> Camera { self.0.perspective(fovy, width, height, znear, zfar).into() } @@ -137,7 +135,6 @@ impl LineSegment3D { pub(crate) fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; - // `LookingCamera` remains "private" m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/pyraydeon/src/shapes/mod.rs b/pyraydeon/src/shapes/mod.rs index f0df791..52bf726 100644 --- a/pyraydeon/src/shapes/mod.rs +++ b/pyraydeon/src/shapes/mod.rs @@ -197,7 +197,10 @@ impl raydeon::Shape for PythonGeometry { collision_geometry } - fn paths(&self, cam: &raydeon::Camera) -> Vec> { + fn paths( + &self, + cam: &raydeon::Camera, + ) -> Vec> { let segments: Option<_> = Python::with_gil(|py| { let inner = self.slf.bind(py); let cam = Camera::from(*cam); diff --git a/raydeon/examples/cube.rs b/raydeon/examples/cube.rs index 2f6e248..274639f 100644 --- a/raydeon/examples/cube.rs +++ b/raydeon/examples/cube.rs @@ -22,7 +22,9 @@ fn main() { let znear = 0.1; let zfar = 10.0; - let camera = Camera::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/raydeon/examples/geom_perf.rs b/raydeon/examples/geom_perf.rs index 9f08ad3..74a1984 100644 --- a/raydeon/examples/geom_perf.rs +++ b/raydeon/examples/geom_perf.rs @@ -22,7 +22,9 @@ fn main() { let scene = Scene::new(generate_scene()); - let camera = Camera::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/raydeon/examples/triangles.rs b/raydeon/examples/triangles.rs index 44cacfa..eaf0518 100644 --- a/raydeon/examples/triangles.rs +++ b/raydeon/examples/triangles.rs @@ -33,7 +33,9 @@ fn main() { let znear = 0.1; let zfar = 10.0; - let camera = Camera::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/raydeon/src/camera.rs b/raydeon/src/camera.rs new file mode 100644 index 0000000..78528b8 --- /dev/null +++ b/raydeon/src/camera.rs @@ -0,0 +1,265 @@ +use euclid::Transform3D; + +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, Copy, Clone)] +pub struct Perspective { + pub fovy: f64, + pub width: f64, + pub height: f64, + pub aspect: f64, + pub znear: f64, + pub zfar: f64, +} + +impl Default for Perspective { + fn default() -> Self { + Self::new(45.0, 1920.0, 1080.0, 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: f64, height: f64, znear: f64, zfar: f64) -> Self { + let aspect = width / height; + Self { + fovy, + width, + height, + aspect, + znear, + zfar, + } + } +} + +#[derive(Debug, Copy, Clone)] +pub struct Camera { + pub observation: O, + pub perspective: P, +} + +impl Camera { + pub fn new() -> Self { + Self { + observation: NoObservation, + perspective: NoPerspective, + } + } +} + +impl Default for Camera { + fn default() -> Self { + Self { + observation: Observation::default(), + perspective: Perspective::default(), + } + } +} + +impl Camera { + pub fn look_at( + self, + eye: impl Into, + center: impl Into, + up: impl Into, + ) -> Camera { + let Camera { perspective, .. } = self; + let observation = Observation::new(eye.into(), center.into(), up.into()); + Camera { + observation, + perspective, + } + } + + pub fn perspective( + self, + fovy: f64, + width: f64, + height: f64, + znear: f64, + zfar: f64, + ) -> Camera { + let Camera { observation, .. } = self; + let perspective = Perspective::new(fovy, width, height, znear, zfar); + Camera { + observation, + perspective, + } + } +} + +impl Camera { + #[must_use] + pub fn canvas_transformation(&self) -> Transform3D { + let p = &self.perspective; + let ymax = p.znear * (p.fovy * std::f64::consts::PI / 360.0).tan(); + let xmax = ymax * p.aspect; + + let frustum = frustum(-xmax, xmax, -ymax, ymax, p.znear, p.zfar); + self.observation.look_matrix().then(&frustum) + } + + #[must_use] + pub fn camera_transformation(&self) -> Transform3D { + 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, + ) + .with_destination() + } + + /// Chops a line segment into subsegments based on distance from camera + pub fn chop_segment( + &self, + segment: &LineSegment3D, + ) -> Vec> { + let p1 = segment.p1.to_vector(); + let p2 = segment.p2.to_vector(); + + // Transform the points to camera space, then chop based on the pixel length + let transformation = self.camera_transformation(); + let canvas_points = transformation + .transform_point3d(p1.to_point()) + .and_then(|p1t| { + transformation + .transform_point3d(p2.to_point()) + .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); + rough_chop_size.round_ties_even() as u32 + }) + .unwrap_or_else(|| { + let rough_chop_size = self.min_step_size(); + ((p2 - p1).length() / rough_chop_size).round_ties_even() as u32 + }); + + if chunk_count == 0 { + return vec![]; + } + if chunk_count == 1 { + return vec![*segment]; + } + + let segment_diff = p2 - p1; + let segment_length = segment_diff.length(); + + let true_chunk_len = segment_length / chunk_count as f64; + let true_chunk_len = f64::min(true_chunk_len, segment_length); + + let segment_dir = segment_diff.normalize(); + let chunk_vec = segment_dir * true_chunk_len; + (0..chunk_count) + .map(|segment_ndx| { + let p1 = segment.p1 + (chunk_vec * (segment_ndx as f64)); + let p2 = p1 + chunk_vec; + LineSegment3D::tagged(p1, p2, segment.tag) + }) + .collect() + } +} + +impl Camera { + #[must_use] + fn min_step_size(&self) -> f64 { + let p = &self.perspective; + let ymax = p.znear * (p.fovy * std::f64::consts::PI / 360.0).tan(); + 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 znear_dims = Vec2::new(xmax, ymax) * 2.0; + let est_min_pix = znear_dims.component_div(effective_dims); + f64::min(est_min_pix.x, est_min_pix.y) + } +} + +#[rustfmt::skip] +fn frustum( + l: f64, + r: f64, + b: f64, + t: f64, + n: f64, + f: f64, +) -> Transform3D { + let t1 = 2.0 * n; + let t2 = r - l; + let t3 = t - b; + let t4 = f - n; + + // euclid used to let us specify things in column major order and now it doesn't. + // So we're just transposing it here. + Transform3D::from_array( + Transform3D::::new( + t1 / t2, 0.0, (r + l) / t2, 0.0, + 0.0, t1 / t3, (t + b) / t3, 0.0, + 0.0, 0.0, (-f - n) / t4,(-t1 * f) / t4, + 0.0, 0.0, -1.0, 0.0, + ) + .to_array_transposed(), + ) +} diff --git a/raydeon/src/lib.rs b/raydeon/src/lib.rs index 6247e23..02a8048 100644 --- a/raydeon/src/lib.rs +++ b/raydeon/src/lib.rs @@ -1,4 +1,5 @@ pub(crate) mod bvh; +pub mod camera; pub mod path; pub mod ray; pub mod scene; @@ -9,7 +10,8 @@ use std::sync::Arc; use path::LineSegment3D; pub use ray::{HitData, Ray}; -pub use scene::{Camera, Scene}; +pub use camera::{Camera, NoObservation, NoPerspective, Observation, Perspective}; +pub use scene::Scene; #[cfg(test)] pub(crate) static EPSILON: f64 = 0.004; @@ -46,7 +48,7 @@ where Space: Sized + Send + Sync + std::fmt::Debug + Copy + Clone, { fn collision_geometry(&self) -> Option>>>; - fn paths(&self, cam: &Camera) -> Vec>; + fn paths(&self, cam: &Camera) -> Vec>; } pub trait CollisionGeometry: Send + Sync + std::fmt::Debug diff --git a/raydeon/src/scene.rs b/raydeon/src/scene.rs index 3a521a7..98fa953 100644 --- a/raydeon/src/scene.rs +++ b/raydeon/src/scene.rs @@ -1,5 +1,5 @@ use bvh::BVHTree; -use euclid::{Transform3D, Vector3D}; +use camera::{Observation, Perspective}; use path::{simplify_3d_segments, LineSegment2D}; use rayon::prelude::*; use std::sync::Arc; @@ -7,193 +7,8 @@ use tracing::info; use crate::*; -#[derive(Debug, Copy, Clone)] -pub struct LookingCamera { - eye: WPoint3, - center: WVec3, - up: WVec3, - matrix: WCTransform, -} - -#[derive(Debug, Copy, Clone)] -pub struct Camera { - eye: WPoint3, - _center: WVec3, - _up: WVec3, - - _fovy: f64, - width: f64, - height: f64, - _aspect: f64, - znear: f64, - zfar: f64, - - min_step_size: f64, - max_step_size: f64, - - matrix: Transform3D, -} - -impl Camera { - // TODO testme - #[rustfmt::skip] - pub fn look_at(eye: WPoint3, center: WVec3, up: WVec3) -> LookingCamera { - let up = up.normalize(); - let f = (center - eye.to_vector()).normalize(); - let s = f.cross(up).normalize(); - let u = s.cross(f).normalize(); - - let look_at_matrix = 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(); - - let matrix = look_at_matrix; - LookingCamera { - eye, - center, - up, - matrix, - } - } - - /// Chops a line segment into subsegments based on distance from camera - pub fn chop_segment( - &self, - segment: &LineSegment3D, - ) -> Vec> { - // linearly interpolate step_size based on closest point to the camera - let p1 = segment.p1.to_vector(); - let p2 = segment.p2.to_vector(); - let segment_diff = p2 - p1; - let midpoint = p1 + (segment_diff / 2.0); - - let eye = self.eye.to_vector(); - - let t1 = (p1 - eye).length(); - let t2 = (p2 - eye).length(); - let t3 = (midpoint - eye).length(); - - let closest = f64::min(f64::min(t1, t2), t3); - - let plane_dist = self.zfar - self.znear; - let scale = (closest - self.znear) / plane_dist; - - let rough_step_size = - self.min_step_size + (scale * (self.max_step_size - self.min_step_size)); - - // Slice segment into equal-sized chunks of approx `rough_step_size` length - let segment_length = segment_diff.length(); - if segment_length < rough_step_size { - return Vec::new(); - } - - let chunk_count = usize::max( - (segment_length / rough_step_size).round_ties_even() as usize, - 1, - ); - if chunk_count == 1 { - return vec![*segment]; - } - - let true_chunk_len = segment_length / chunk_count as f64; - let true_chunk_len = f64::min(true_chunk_len, segment_length); - - let segment_dir = segment_diff.normalize(); - let chunk_vec = segment_dir * true_chunk_len; - (0..chunk_count) - .map(|segment_ndx| { - let p1 = segment.p1 + (chunk_vec * (segment_ndx as f64)); - let p2 = p1 + chunk_vec; - LineSegment3D::tagged(p1, p2, segment.tag) - }) - .collect() - } -} - -#[rustfmt::skip] -fn frustum( - l: f64, - r: f64, - b: f64, - t: f64, - n: f64, - f: f64, -) -> Transform3D { - let t1 = 2.0 * n; - let t2 = r - l; - let t3 = t - b; - let t4 = f - n; - - // euclid used to let us specify things in column major order and now it doesn't. - // So we're just transposing it here. - Transform3D::from_array( - Transform3D::::new( - t1 / t2, 0.0, (r + l) / t2, 0.0, - 0.0, t1 / t3, (t + b) / t3, 0.0, - 0.0, 0.0, (-f - n) / t4,(-t1 * f) / t4, - 0.0, 0.0, -1.0, 0.0, - ) - .to_array_transposed(), - ) -} - -impl LookingCamera { - pub fn perspective(self, fovy: f64, width: f64, height: f64, znear: f64, zfar: f64) -> Camera { - let aspect = width / height; - let ymax = znear * (fovy * std::f64::consts::PI / 360.0).tan(); - let xmax = ymax * aspect; - - let frustum = frustum(-xmax, xmax, -ymax, ymax, znear, zfar); - let matrix = self.matrix.then(&frustum); - - let effective_width = width; - let effective_height = height; - let znear_width = 2.0 * xmax; - let znear_height = 2.0 * ymax; - let est_min_pix_height = znear_height / effective_height; - let est_min_pix_width = znear_width / effective_width; - - let min_step_size = f64::min(est_min_pix_height, est_min_pix_width); - - let zfar_ymax = zfar * (fovy * std::f64::consts::PI / 360.0).tan(); - let zfar_xmax = zfar_ymax * aspect; - - let zfar_width = 2.0 * zfar_xmax; - let zfar_height = 2.0 * zfar_ymax; - let est_max_pix_height = zfar_height / effective_height; - let est_max_pix_width = zfar_width / effective_width; - - let max_step_size = f64::min(est_max_pix_height, est_max_pix_width); - - Camera { - eye: self.eye, - _center: self.center, - _up: self.up, - _fovy: fovy, - width, - height, - _aspect: aspect, - znear, - zfar, - min_step_size, - max_step_size, - matrix, - } - } -} - pub struct SceneCamera<'s> { - camera: Camera, + camera: Camera, scene: &'s Scene, paths: Vec>>, path_count: usize, @@ -203,7 +18,7 @@ impl<'a> SceneCamera<'a> { fn clip_filter(&self, path: &LineSegment3D) -> bool { let (p1, p2) = (path.p1, path.p2); let midpoint = p1 + ((p2 - p1) / 2.0); - self.scene.visible(self.camera.eye, midpoint) + self.scene.visible(self.camera.observation.eye, midpoint) } pub fn render(&self) -> Vec> { @@ -212,12 +27,8 @@ impl<'a> SceneCamera<'a> { self.path_count ); - let transformation: Transform3 = self - .camera - .matrix - .then_translate(Vector3D::new(1.0, 1.0, 0.0)) - .then_scale(self.camera.width / 2.0, self.camera.height / 2.0, 0.0) - .with_destination(); + let transformation: Transform3 = + self.camera.camera_transformation(); let paths: Vec<_> = self .paths @@ -226,9 +37,10 @@ impl<'a> SceneCamera<'a> { path_group .par_iter() .filter(|path| { - let close_enough = (path.p1.to_vector() - self.camera.eye.to_vector()) - .length() - < self.camera.zfar; + let close_enough = (path.p1.to_vector() + - self.camera.observation.eye.to_vector()) + .length() + < self.camera.perspective.zfar; close_enough && self.clip_filter(path) }) .cloned() @@ -262,7 +74,7 @@ impl Scene { Scene { geometry, bvh } } - pub fn attach_camera(&self, camera: Camera) -> SceneCamera { + pub fn attach_camera(&self, camera: Camera) -> SceneCamera { info!("Caching line segment chunks based on new camera attachment"); let paths: Vec>> = self .geometry diff --git a/raydeon/src/shapes/aacuboid.rs b/raydeon/src/shapes/aacuboid.rs index 3605634..5cddd66 100644 --- a/raydeon/src/shapes/aacuboid.rs +++ b/raydeon/src/shapes/aacuboid.rs @@ -4,7 +4,10 @@ use std::sync::Arc; use collision::Continuous; use crate::path::LineSegment3D; -use crate::{Camera, CollisionGeometry, HitData, Ray, Shape, WPoint3, WVec3, WorldSpace, AABB3}; +use crate::{ + Camera, CollisionGeometry, HitData, Observation, Perspective, Ray, Shape, WPoint3, WVec3, + WorldSpace, AABB3, +}; #[derive(Debug, Copy, Clone)] #[cfg_attr(test, derive(PartialEq))] @@ -35,7 +38,7 @@ impl Shape for AxisAlignedCuboid { Some(vec![Arc::new(*self)]) } - fn paths(&self, _cam: &Camera) -> Vec> { + fn paths(&self, _cam: &Camera) -> Vec> { let expand = (self.max - self.min).normalize() * 0.0015; let pathmin = self.min - expand; let pathmax = self.max + expand; diff --git a/raydeon/src/shapes/quad.rs b/raydeon/src/shapes/quad.rs index 654a4b0..aeeca30 100644 --- a/raydeon/src/shapes/quad.rs +++ b/raydeon/src/shapes/quad.rs @@ -2,7 +2,9 @@ use std::sync::Arc; use super::Triangle; use crate::path::LineSegment3D; -use crate::{Camera, CollisionGeometry, Shape, WPoint3, WVec3, WorldSpace}; +use crate::{ + Camera, CollisionGeometry, Observation, Perspective, Shape, WPoint3, WVec3, WorldSpace, +}; #[derive(Debug, Copy, Clone)] #[cfg_attr(test, derive(PartialEq))] @@ -46,7 +48,7 @@ impl Shape for Quad { ]) } - fn paths(&self, _cam: &Camera) -> Vec> { + fn paths(&self, _cam: &Camera) -> Vec> { let centroid = self .verts .into_iter() diff --git a/raydeon/src/shapes/triangle.rs b/raydeon/src/shapes/triangle.rs index dc7d6f0..2ad7aa1 100644 --- a/raydeon/src/shapes/triangle.rs +++ b/raydeon/src/shapes/triangle.rs @@ -3,7 +3,10 @@ use std::sync::Arc; use super::plane::Plane; use crate::path::LineSegment3D; -use crate::{Camera, CollisionGeometry, HitData, Ray, Shape, WPoint3, WVec3, WorldSpace}; +use crate::{ + Camera, CollisionGeometry, HitData, Observation, Perspective, Ray, Shape, WPoint3, WVec3, + WorldSpace, +}; #[derive(Debug, Copy, Clone)] #[cfg_attr(test, derive(PartialEq))] @@ -38,7 +41,7 @@ impl Shape for Triangle { Some(vec![Arc::new(*self)]) } - fn paths(&self, _cam: &Camera) -> Vec> { + fn paths(&self, _cam: &Camera) -> Vec> { let v0 = self.verts[0]; let v1 = self.verts[1]; let v2 = self.verts[2];