From 368198af4fa730c0527c0aab85337148aac9c5ef Mon Sep 17 00:00:00 2001 From: Kevin Reid Date: Sat, 6 Jul 2024 21:54:04 -0700 Subject: [PATCH] Move `render_orthographic()` code to render crate. It's still very much a special case, but we can now start on more integration or using it elsewhere than the UI test. --- all-is-cubes-render/src/raytracer/mod.rs | 3 + all-is-cubes-render/src/raytracer/ortho.rs | 223 +++++++++++++++++++++ test-renderers/tests/ui.rs | 203 +------------------ 3 files changed, 231 insertions(+), 198 deletions(-) create mode 100644 all-is-cubes-render/src/raytracer/ortho.rs diff --git a/all-is-cubes-render/src/raytracer/mod.rs b/all-is-cubes-render/src/raytracer/mod.rs index f05526264..e237fc644 100644 --- a/all-is-cubes-render/src/raytracer/mod.rs +++ b/all-is-cubes-render/src/raytracer/mod.rs @@ -18,3 +18,6 @@ pub use all_is_cubes::raytracer::{ mod renderer; pub use renderer::{RtRenderer, RtScene}; + +#[doc(hidden)] // experimental/internal, used only by test-renderers right now +pub mod ortho; diff --git a/all-is-cubes-render/src/raytracer/ortho.rs b/all-is-cubes-render/src/raytracer/ortho.rs new file mode 100644 index 000000000..511cc7cd1 --- /dev/null +++ b/all-is-cubes-render/src/raytracer/ortho.rs @@ -0,0 +1,223 @@ +//! Axis-aligned orthographic raytracing as a special case. + +#[cfg(feature = "auto-threads")] +use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _}; + +use all_is_cubes::euclid::{point2, vec2, vec3, Point2D, Scale, Transform3D}; +use all_is_cubes::math::{ + Axis, Cube, Face6, FreeVector, GridAab, GridRotation, GridSizeCoord, Gridgid, Rgba, +}; +use all_is_cubes::raycast; +use all_is_cubes::space::Space; +use all_is_cubes::universe::Handle; +use all_is_cubes::{block, raytracer}; + +use crate::camera::{self, GraphicsOptions, ImagePixel}; +use crate::{Flaws, Rendering}; + +/// Special-purpose renderer which uses a pixel-perfect orthographic projection and adapts to the +/// size of the space input. +pub fn render_orthographic(space: &Handle) -> Rendering { + // TODO: Figure out how to make this less of a from-scratch reimplementation and share + // more components with the regular raytracer. + // To do this we will need to add ortho support to `Camera` or some other special case. + + // TODO: Given this special orthographic pixel-aligned projection, and an accumulator + // for "what's the highest resolution that was tested along this ray", we can do adaptive + // sampling so we can trace whole blocks at once when they're simple, and also detect if + // the image scale is too low to accurately capture the scene. + let space = &*space.read().expect("failed to read space to render"); + let camera = &MultiOrthoCamera::new(block::Resolution::R32, space.bounds()); + let rt = &raytracer::SpaceRaytracer::new(space, GraphicsOptions::UNALTERED_COLORS, ()); + + #[cfg(feature = "auto-threads")] + let data = (0..camera.image_size.height) + .into_par_iter() + .flat_map(|y| { + (0..camera.image_size.width).into_par_iter().map(move |x| { + match camera.project_pixel_into_world(point2(x, y)) { + Some(ray) => { + let (pixel, _): (raytracer::ColorBuf, _) = + rt.trace_axis_aligned_ray(ray, true); + Rgba::from(pixel) + } + None => Rgba::TRANSPARENT, + } + .to_srgb8() + }) + }) + .collect(); + + #[cfg(not(feature = "auto-threads"))] + let data = (0..camera.image_size.height) + .flat_map(|y| { + (0..camera.image_size.width).map(move |x| { + match camera.project_pixel_into_world(point2(x, y)) { + Some(ray) => { + let (pixel, _): (raytracer::ColorBuf, _) = + rt.trace_axis_aligned_ray(ray, true); + Rgba::from(pixel) + } + None => Rgba::TRANSPARENT, + } + .to_srgb8() + }) + }) + .collect(); + + Rendering { + size: camera.image_size, + data, + flaws: Flaws::empty(), // TODO: wrong + } +} + +/// A view of a `Space` from multiple directions at a chosen pixel-perfect resolution. +#[derive(Debug)] +pub struct MultiOrthoCamera { + pub image_size: camera::ImageSize, + views: [(OrthoCamera, Point2D); 5], +} + +impl MultiOrthoCamera { + pub fn new(resolution: block::Resolution, bounds: GridAab) -> Self { + let top = OrthoCamera::new(resolution, bounds, Face6::PY); + let left = OrthoCamera::new(resolution, bounds, Face6::NX); + let front = OrthoCamera::new(resolution, bounds, Face6::PZ); + let right = OrthoCamera::new(resolution, bounds, Face6::PX); + let bottom = OrthoCamera::new(resolution, bounds, Face6::NY); + let views = [ + (top, point2(left.image_size.width + 1, 0)), + (left, point2(0, top.image_size.height + 1)), + ( + front, + point2(left.image_size.width + 1, top.image_size.height + 1), + ), + ( + right, + point2( + left.image_size.width + front.image_size.width + 2, + top.image_size.height + 1, + ), + ), + ( + bottom, + point2( + left.image_size.width + 1, + top.image_size.height + front.image_size.height + 2, + ), + ), + ]; + + let mut bottom_corner = point2(0, 0); + for &(ref cam, origin) in views.iter() { + bottom_corner = bottom_corner.max(origin + cam.image_size.to_vector()); + } + + Self { + image_size: bottom_corner.to_vector().to_size(), + views, + } + } + + pub fn project_pixel_into_world( + &self, + point: Point2D, + ) -> Option { + // Find which camera's rectangle contains the point + for &(ref cam, origin) in self.views.iter() { + if let (Some(x), Some(y)) = + (point.x.checked_sub(origin.x), point.y.checked_sub(origin.y)) + { + if x < cam.image_size.width && y < cam.image_size.height { + return cam.project_pixel_into_world(point2(x, y)); + } + } + } + None + } +} + +/// A view of a `Space` from an axis-aligned direction at a chosen pixel-perfect resolution. +#[derive(Clone, Copy, Debug)] +pub struct OrthoCamera { + image_size: camera::ImageSize, + transform: Transform3D, + ray_direction: FreeVector, +} + +impl OrthoCamera { + pub fn new(resolution: block::Resolution, bounds: GridAab, viewed_face: Face6) -> Self { + let cube_to_pixel_scale: Scale = + Scale::new(resolution.into()); + let pixel_to_cube_scale: Scale = + cube_to_pixel_scale.cast::().inverse(); + + let image_size = cube_to_pixel_scale + .transform_size( + { + let sizevec = bounds.size().to_vector(); + match viewed_face.axis() { + Axis::X => vec2(sizevec.z, sizevec.y), + Axis::Y => sizevec.xz(), + Axis::Z => sizevec.xy(), + } + } + .to_size(), + ) + .to_u32(); + let origin_translation: FreeVector = { + let lb = bounds.lower_bounds(); + let ub = bounds.upper_bounds(); + match viewed_face { + // note Y flip — this is the world point that should be the top left corner of each view + Face6::NX => vec3(lb.x, ub.y, lb.z), + Face6::NY => vec3(lb.x, lb.y, ub.z), + Face6::NZ => vec3(ub.x, ub.y, lb.z), + Face6::PX => vec3(ub.x, ub.y, ub.z), + Face6::PY => vec3(lb.x, ub.y, lb.z), + Face6::PZ => vec3(lb.x, ub.y, ub.z), + } + } + .to_f64(); + let rotation = Gridgid::from_rotation_about_origin(match viewed_face { + Face6::NX => GridRotation::CLOCKWISE, + Face6::NY => GridRotation::RXZy, + Face6::NZ => GridRotation::CLOCKWISE * GridRotation::CLOCKWISE, + Face6::PX => GridRotation::COUNTERCLOCKWISE, + Face6::PY => GridRotation::RXzY, + Face6::PZ => GridRotation::IDENTITY, + }) + .to_matrix() + .to_free(); + + let transform = Transform3D::translation(0.5, 0.5, 0.0) // pixel centers + .then_scale(1., -1., 1.) // Y flip + .then(&Transform3D::from_scale(pixel_to_cube_scale)) // overall scale + .then(&rotation) + .then_translate(origin_translation); // image origin to world origin + + // TODO: Add side and top/bottom orthographic views. + + Self { + image_size, + transform, + ray_direction: transform.transform_vector3d(vec3(0., 0., -1.)), + } + } + + pub fn project_pixel_into_world( + &self, + point: Point2D, + ) -> Option { + raycast::Ray { + origin: self + .transform + .transform_point3d(point.to_f64().to_3d()) + .unwrap(), + direction: self.ray_direction, + } + .try_into() + .ok() + } +} diff --git a/test-renderers/tests/ui.rs b/test-renderers/tests/ui.rs index 53cf3ada1..b27091c18 100644 --- a/test-renderers/tests/ui.rs +++ b/test-renderers/tests/ui.rs @@ -4,24 +4,19 @@ use std::sync::Arc; use clap::Parser as _; -use rayon::iter::{IntoParallelIterator as _, ParallelIterator as _}; use all_is_cubes::arcstr::literal; -use all_is_cubes::euclid::{point2, vec2, vec3, Point2D, Scale, Transform3D}; use all_is_cubes::linking::BlockProvider; use all_is_cubes::listen::{ListenableCell, ListenableSource}; -use all_is_cubes::math::{ - Axis, Cube, Face6, FreeVector, GridAab, GridRotation, GridSizeCoord, Gridgid, Rgba, -}; -use all_is_cubes::raycast; -use all_is_cubes::space::Space; +use all_is_cubes::math::Face6; use all_is_cubes::time::NoTime; use all_is_cubes::transaction::Transaction as _; use all_is_cubes::universe::{Handle, Name, Universe, UniverseTransaction}; use all_is_cubes::util::YieldProgress; -use all_is_cubes::{block, raytracer, space, transaction}; -use all_is_cubes_render::camera::{self, GraphicsOptions, ImagePixel, Viewport}; -use all_is_cubes_render::{Flaws, Rendering}; +use all_is_cubes::{space, transaction}; +use all_is_cubes_render::camera::Viewport; +use all_is_cubes_render::raytracer::ortho::render_orthographic; +use all_is_cubes_render::Rendering; use all_is_cubes_ui::apps::{Key, Session}; use all_is_cubes_ui::notification::NotificationContent; @@ -209,191 +204,3 @@ fn render_widget(widget: &vui::WidgetTree, gravity: vui::Gravity) -> Rendering { .unwrap(), )) } - -/// Special-purpose renderer which uses a pixel-perfect orthographic projectiuon and adapts to the -/// size of the space input. -fn render_orthographic(space: &Handle) -> Rendering { - // TODO: Fold this into [`RtRenderer`] so it can benefit from using the usual implementations. - // To do this we will need to add ortho support to `Camera` or some other special case. - - // TODO: Given this special orthographic pixel-aligned projection, and an accumulator - // for "what's the highest resolution that was tested along this ray", we can do adaptive - // sampling so we can trace whole blocks at once when they're simple, and also detect if - // the image scale is too low to accurately capture the scene. - let space = &*space.read().expect("failed to read space to render"); - let camera = MultiOrthoCamera::new(block::Resolution::R32, space.bounds()); - let rt = &raytracer::SpaceRaytracer::new(space, GraphicsOptions::UNALTERED_COLORS, ()); - - let camera = &camera; - let data = (0..camera.image_size.height) - .into_par_iter() - .flat_map(|y| { - (0..camera.image_size.width).into_par_iter().map(move |x| { - match camera.project_pixel_into_world(point2(x, y)) { - Some(ray) => { - let (pixel, _): (raytracer::ColorBuf, _) = - rt.trace_axis_aligned_ray(ray, true); - Rgba::from(pixel) - } - None => Rgba::TRANSPARENT, - } - .to_srgb8() - }) - }) - .collect(); - - Rendering { - size: camera.image_size, - data, - flaws: Flaws::empty(), // TODO: wrong - } -} - -/// A view of a `Space` from multiple directions at a chosen pixel-perfect resolution. -struct MultiOrthoCamera { - pub image_size: camera::ImageSize, - views: [(OrthoCamera, Point2D); 5], -} - -impl MultiOrthoCamera { - pub fn new(resolution: block::Resolution, bounds: GridAab) -> Self { - let top = OrthoCamera::new(resolution, bounds, Face6::PY); - let left = OrthoCamera::new(resolution, bounds, Face6::NX); - let front = OrthoCamera::new(resolution, bounds, Face6::PZ); - let right = OrthoCamera::new(resolution, bounds, Face6::PX); - let bottom = OrthoCamera::new(resolution, bounds, Face6::NY); - let views = [ - (top, point2(left.image_size.width + 1, 0)), - (left, point2(0, top.image_size.height + 1)), - ( - front, - point2(left.image_size.width + 1, top.image_size.height + 1), - ), - ( - right, - point2( - left.image_size.width + front.image_size.width + 2, - top.image_size.height + 1, - ), - ), - ( - bottom, - point2( - left.image_size.width + 1, - top.image_size.height + front.image_size.height + 2, - ), - ), - ]; - - let mut bottom_corner = point2(0, 0); - for &(ref cam, origin) in views.iter() { - bottom_corner = bottom_corner.max(origin + cam.image_size.to_vector()); - } - - Self { - image_size: bottom_corner.to_vector().to_size(), - views, - } - } - - pub fn project_pixel_into_world( - &self, - point: Point2D, - ) -> Option { - // Find which camera's rectangle contains the point - for &(ref cam, origin) in self.views.iter() { - if let (Some(x), Some(y)) = - (point.x.checked_sub(origin.x), point.y.checked_sub(origin.y)) - { - if x < cam.image_size.width && y < cam.image_size.height { - return cam.project_pixel_into_world(point2(x, y)); - } - } - } - None - } -} - -/// A view of a `Space` from the +Z direction at a chosen pixel-perfect resolution. -#[derive(Clone, Copy, Debug)] -struct OrthoCamera { - image_size: camera::ImageSize, - transform: Transform3D, - ray_direction: FreeVector, -} - -impl OrthoCamera { - pub fn new(resolution: block::Resolution, bounds: GridAab, viewed_face: Face6) -> Self { - let cube_to_pixel_scale: Scale = - Scale::new(resolution.into()); - let pixel_to_cube_scale: Scale = - cube_to_pixel_scale.cast::().inverse(); - - let image_size = cube_to_pixel_scale - .transform_size( - { - let sizevec = bounds.size().to_vector(); - match viewed_face.axis() { - Axis::X => vec2(sizevec.z, sizevec.y), - Axis::Y => sizevec.xz(), - Axis::Z => sizevec.xy(), - } - } - .to_size(), - ) - .to_u32(); - let origin_translation: FreeVector = { - let lb = bounds.lower_bounds(); - let ub = bounds.upper_bounds(); - match viewed_face { - // note Y flip — this is the world point that should be the top left corner of each view - Face6::NX => vec3(lb.x, ub.y, lb.z), - Face6::NY => vec3(lb.x, lb.y, ub.z), - Face6::NZ => vec3(ub.x, ub.y, lb.z), - Face6::PX => vec3(ub.x, ub.y, ub.z), - Face6::PY => vec3(lb.x, ub.y, lb.z), - Face6::PZ => vec3(lb.x, ub.y, ub.z), - } - } - .to_f64(); - let rotation = Gridgid::from_rotation_about_origin(match viewed_face { - Face6::NX => GridRotation::CLOCKWISE, - Face6::NY => GridRotation::RXZy, - Face6::NZ => GridRotation::CLOCKWISE * GridRotation::CLOCKWISE, - Face6::PX => GridRotation::COUNTERCLOCKWISE, - Face6::PY => GridRotation::RXzY, - Face6::PZ => GridRotation::IDENTITY, - }) - .to_matrix() - .to_free(); - - let transform = Transform3D::translation(0.5, 0.5, 0.0) // pixel centers - .then_scale(1., -1., 1.) // Y flip - .then(&Transform3D::from_scale(pixel_to_cube_scale)) // overall scale - .then(&rotation) - .then_translate(origin_translation); // image origin to world origin - - // TODO: Add side and top/bottom orthographic views. - - Self { - image_size, - transform, - ray_direction: transform.transform_vector3d(vec3(0., 0., -1.)), - } - } - - pub fn project_pixel_into_world( - &self, - point: Point2D, - ) -> Option { - raycast::Ray { - origin: self - .transform - .transform_point3d(point.to_f64().to_3d()) - .unwrap(), - direction: self.ray_direction, - } - .try_into() - .ok() - } -}