diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6e0a44b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.vox filter=lfs diff=lfs merge=lfs -text +*.png filter=lfs diff=lfs merge=lfs -text diff --git a/README.md b/README.md index 9ed0260..87c8de0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ steal his words (because this port isn't theft enough): This repository has added support for screen-space hatching based on lights placed within the scene. -![](/raydeon/examples/cityscape.png) +![](/raydeon/examples/cityscape.png) ![](/raydeon/examples/castle_expected.svg) ## Example diff --git a/pyraydeon/Cargo.toml b/pyraydeon/Cargo.toml index aeeee4f..e4276ae 100644 --- a/pyraydeon/Cargo.toml +++ b/pyraydeon/Cargo.toml @@ -9,6 +9,7 @@ name = "pyraydeon" crate-type = ["cdylib"] [dependencies] +euclid.workspace = true pyo3 = { workspace = true, features = ["extension-module"] } raydeon.workspace = true numpy.workspace = true diff --git a/pyraydeon/examples/py_sphere.py b/pyraydeon/examples/py_sphere.py index d4f5b8a..2c991a7 100644 --- a/pyraydeon/examples/py_sphere.py +++ b/pyraydeon/examples/py_sphere.py @@ -90,8 +90,8 @@ def paths(self, cam): ) -eye = Point3(0, 0, 5) -focus = Vec3(0, 0, 0) +eye = Point3(0, 0, 0) +focus = Point3(0, 0, -1) up = Vec3(0, 1, 0) fovy = 50.0 @@ -110,6 +110,7 @@ def paths(self, cam): .perspective(fovy, width, height, znear, zfar) .render_options(render_opts) ) +cam.translate((0, 0, 5)) paths = scene.render_with_lighting(cam, seed=5) diff --git a/pyraydeon/src/camera.rs b/pyraydeon/src/camera.rs index 9e8a1ae..e2044ac 100644 --- a/pyraydeon/src/camera.rs +++ b/pyraydeon/src/camera.rs @@ -4,7 +4,7 @@ use pyo3::prelude::*; use crate::linear::{Point3, Vec3}; #[derive(Debug, Clone)] -#[pyclass(frozen)] +#[pyclass] pub(crate) struct Camera(pub(crate) raydeon::Camera); impl ::std::ops::Deref for Camera { @@ -55,19 +55,45 @@ impl Camera { ncam.into() } + fn translate(&mut self, trans: &Bound<'_, PyAny>) -> PyResult<()> { + let trans = Vec3::try_from(trans)?; + self.0.translate(trans.0.cast_unit()); + Ok(()) + } + + fn adjust_yaw(&mut self, yaw: f64) -> PyResult<()> { + self.0.adjust_yaw(euclid::Angle::degrees(yaw)); + Ok(()) + } + + fn adjust_pitch(&mut self, pitch: f64) -> PyResult<()> { + self.0.adjust_pitch(euclid::Angle::degrees(pitch)); + Ok(()) + } + + fn adjust_roll(&mut self, roll: f64) -> PyResult<()> { + self.0.adjust_roll(euclid::Angle::degrees(roll)); + Ok(()) + } + #[getter] fn eye<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { - PyArray::from_slice_bound(py, &self.0.observation.eye.to_array()) + 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()) + fn up<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.0.observation.up().to_array()) } #[getter] - fn up<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { - PyArray::from_slice_bound(py, &self.0.observation.up.to_array()) + fn right<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.0.observation.right().to_array()) + } + + #[getter] + fn look<'py>(&self, py: Python<'py>) -> Bound<'py, PyArray> { + PyArray::from_slice_bound(py, &self.0.observation.look().to_array()) } #[getter] @@ -102,7 +128,7 @@ impl Camera { fn __repr__(slf: &Bound<'_, Self>) -> PyResult { let class_name = slf.get_type().qualname()?; - Ok(format!("{}<{:?}>", class_name, slf.borrow().0)) + Ok(format!("{}<{:#?}>", class_name, slf.borrow().0)) } } diff --git a/raydeon/Cargo.toml b/raydeon/Cargo.toml index e589979..7f82aee 100644 --- a/raydeon/Cargo.toml +++ b/raydeon/Cargo.toml @@ -5,7 +5,6 @@ authors = ["cbgbt "] edition = "2021" [dependencies] -anyhow.workspace = true bon.workspace = true cgmath.workspace = true collision.workspace = true @@ -18,5 +17,6 @@ rayon.workspace = true tracing = { workspace = true, features = ["log"] } [dev-dependencies] +dot_vox.workspace = true env_logger.workspace = true svg.workspace = true diff --git a/raydeon/examples/assets/castle.vox b/raydeon/examples/assets/castle.vox new file mode 100755 index 0000000..840de03 --- /dev/null +++ b/raydeon/examples/assets/castle.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c348a638b094730ca5bfd362735ba8b4aa5b65e0c29ea12243bcf1ac972a60a +size 33209 diff --git a/raydeon/examples/castle.rs b/raydeon/examples/castle.rs new file mode 100644 index 0000000..ed55f4c --- /dev/null +++ b/raydeon/examples/castle.rs @@ -0,0 +1,112 @@ +use dot_vox; +use euclid::Angle; +use raydeon::lights::PointLight; +use raydeon::shapes::AxisAlignedCuboid; +use raydeon::{Camera, CameraOptions, DrawableShape, Material, Scene, SceneLighting}; +use std::sync::Arc; + +const CASTLE_VOX: &[u8] = include_bytes!("./assets/castle.vox"); + +fn main() { + env_logger::Builder::from_default_env() + .format_timestamp_nanos() + .init(); + + let castle_vox = dot_vox::load_bytes(CASTLE_VOX).expect("Could not load castle.vox"); + + let geometry: Vec<_> = castle_vox.models[0] + .voxels + .iter() + .map(|v| { + let x = v.x as f64; + let y = v.y as f64; + let z = v.z as f64; + DrawableShape::new() + .geometry(Arc::new( + AxisAlignedCuboid::new() + .min((x + 0.05, y + 0.05, z + 0.05)) + .max((x + 0.95, y + 0.95, z + 0.95)) + .build(), + )) + .material(Material::new().diffuse(5.0).build()) + .build() + }) + .collect(); + + let eye = (10.0, -20.0, 0.0); + let focus = (10.0, 0.0, 0.0); + let up = (0.0, 0.0, 1.0); + + let fovy = 40.0; + let width = 2048; + let height = 2048; + let znear = 0.1; + let zfar = 200.0; + + let mut camera = Camera::configure() + .observation(Camera::look_at(eye, focus, up)) + .perspective(Camera::perspective(fovy, width, height, znear, zfar)) + .render_options(CameraOptions::configure().pen_px_size(4.0).build()) + .build(); + + camera.translate((-15.0, 10.75, 0.0)); + camera.adjust_yaw(Angle::degrees(-45.0)); + camera.translate((-10.5, 0.0, 12.0)); + + let scene = Scene::new() + .geometry(geometry) + .lighting( + SceneLighting::new() + .with_ambient_lighting(0.37) + .with_lights(vec![Arc::new(PointLight::new( + 55.0, + 10.0, + (-10.81, -20.0, 30.0), + 0.0, + 0.13, + 0.19, + ))]), + ) + .construct(); + + let paths = 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::Group::new().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 paths { + 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/cityscape.png b/raydeon/examples/cityscape.png index 3abaab3..5ebdf96 100644 Binary files a/raydeon/examples/cityscape.png and b/raydeon/examples/cityscape.png differ diff --git a/raydeon/src/camera.rs b/raydeon/src/camera.rs index e089863..ffbf660 100644 --- a/raydeon/src/camera.rs +++ b/raydeon/src/camera.rs @@ -1,5 +1,5 @@ use bon::Builder; -use euclid::{Point3D, Transform3D}; +use euclid::{Point3D, Transform3D, Vector3D}; use path::SlicedSegment3D; use self::view_matrix_settings::*; @@ -45,6 +45,22 @@ impl Default for CameraOptions { } impl Camera { + pub fn adjust_yaw(&mut self, yaw: euclid::Angle) { + self.observation.adjust_yaw(yaw); + } + + pub fn adjust_pitch(&mut self, pitch: euclid::Angle) { + self.observation.adjust_pitch(pitch); + } + + pub fn adjust_roll(&mut self, roll: euclid::Angle) { + self.observation.adjust_roll(roll); + } + + pub fn translate(&mut self, trans: impl Into>) { + self.observation.translate(trans); + } + #[must_use] pub fn canvas_transformation(&self) -> Transform3D { let p = &self.perspective; @@ -52,7 +68,7 @@ impl Camera { let xmax = ymax * p.aspect; let frustum = frustum(-xmax, xmax, -ymax, ymax, p.znear, p.zfar); - self.observation.look_matrix().then(&frustum) + self.observation.world_to_camera_transform().then(&frustum) } #[must_use] @@ -114,8 +130,8 @@ impl Camera { .unwrap(); Ray { - point: self.observation.eye, - dir: (world_coord.to_vector() - self.observation.eye.to_vector()).normalize(), + point: self.observation.eye(), + dir: (world_coord.to_vector() - self.observation.eye().to_vector()).normalize(), } } @@ -141,11 +157,7 @@ impl Camera { center: impl Into, up: impl Into, ) -> Observation { - let eye = eye.into(); - let center = center.into(); - let up = up.into().normalize(); - - Observation { eye, center, up } + Observation::look_at(eye, center, up) } pub fn perspective( @@ -172,9 +184,7 @@ mod view_matrix_settings { #[derive(Debug, Copy, Clone)] pub struct Observation { - pub eye: WPoint3, - pub center: WVec3, - pub up: WVec3, + view_mat: CWTransform, } impl Default for Observation { @@ -189,34 +199,81 @@ mod view_matrix_settings { center: impl Into, up: impl Into, ) -> Self { + let view_mat = Self::create_view_matrix(eye, center, up); + Self { view_mat } + } + + pub fn eye(&self) -> WPoint3 { + (self.view_mat.m41, self.view_mat.m42, self.view_mat.m43).into() + } + + pub fn right(&self) -> WVec3 { + (self.view_mat.m11, self.view_mat.m12, self.view_mat.m13).into() + } + + pub fn up(&self) -> WVec3 { + (self.view_mat.m21, self.view_mat.m22, self.view_mat.m23).into() + } + + pub fn look(&self) -> WVec3 { + (-self.view_mat.m31, -self.view_mat.m32, -self.view_mat.m33).into() + } + + fn rotate_around_axis(&mut self, axis: impl Into, angle: euclid::Angle) { + let axis = axis.into(); + self.view_mat = self + .view_mat + .pre_rotate(axis.x, axis.y, axis.z, angle) + .with_destination(); + } + + pub fn adjust_yaw(&mut self, yaw: euclid::Angle) { + self.rotate_around_axis((0.0, 1.0, 0.0), yaw); + } + + pub fn adjust_pitch(&mut self, pitch: euclid::Angle) { + self.rotate_around_axis((1.0, 0.0, 0.0), pitch); + } + + pub fn adjust_roll(&mut self, roll: euclid::Angle) { + self.rotate_around_axis((0.0, 0.0, 1.0), roll); + } + + pub fn translate(&mut self, trans: impl Into>) { + // input is a camera translation, but we're describing a + // world translation, so we negate + let trans = trans.into(); + self.view_mat = self + .view_mat + .pre_translate(trans.cast_unit()) + .with_destination(); + } + + #[rustfmt::skip] + fn create_view_matrix( + eye: impl Into, + center: impl Into, + up: impl Into, + ) -> Transform3D { let eye = eye.into(); let center = center.into(); let up = up.into().normalize(); - Self { eye, center, up } - } + let f = (center - eye.to_vector()).normalize(); + let s = f.cross(up).normalize(); + let u = s.cross(f).normalize(); - #[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, + s.x, s.y, s.z, 0.0, + u.x, u.y, u.z, 0.0, + -f.x, -f.y, -f.z, 0.0, + eye.x, eye.y, eye.z, 1.0 ) - .to_array_transposed(), - ) - .inverse() - .unwrap() - } + } + + pub(super) fn world_to_camera_transform(&self) -> WCTransform { + self.view_mat.inverse().unwrap() + } } #[derive(Debug, Copy, Clone)] diff --git a/raydeon/src/scene.rs b/raydeon/src/scene.rs index a8ebc87..a025f2e 100644 --- a/raydeon/src/scene.rs +++ b/raydeon/src/scene.rs @@ -1,7 +1,7 @@ use bon::Builder; use bvh::{BVHTree, Collidable}; use collision::Continuous; -use euclid::{Point2D, Vector2D}; +use euclid::{Point2D, Vector2D, Vector3D}; use path::{LineSegment2D, SlicedSegment3D}; use rand::distributions::Distribution; use rand::SeedableRng; @@ -200,6 +200,22 @@ impl<'s> SceneCamera<'s> { self } + pub fn adjust_yaw(&mut self, yaw: euclid::Angle) { + self.camera.adjust_yaw(yaw); + } + + pub fn adjust_pitch(&mut self, pitch: euclid::Angle) { + self.camera.adjust_pitch(pitch); + } + + pub fn adjust_roll(&mut self, roll: euclid::Angle) { + self.camera.adjust_roll(roll); + } + + pub fn translate(&mut self, trans: impl Into>) { + self.camera.translate(trans); + } + fn geometry_paths(&self) -> Vec> { self.scene .geometry @@ -215,7 +231,7 @@ impl<'s> SceneCamera<'s> { fn clip_filter(&self, path: &LineSegment3D) -> bool { self.scene - .visible(self.camera.observation.eye, path.midpoint()) + .visible(self.camera.observation.eye(), path.midpoint()) } pub fn render(&self) -> Vec> { @@ -254,7 +270,7 @@ impl<'s> SceneCamera<'s> { .subsegments() .enumerate() .filter_map(|(ndx, path)| { - let from_cam = path.midpoint() - self.camera.observation.eye; + let from_cam = path.midpoint() - self.camera.observation.eye(); let close_enough = from_cam.length() < self.camera.perspective.zfar; let visible = close_enough && self.clip_filter(&path); (!visible).then_some(ndx)