diff --git a/demos/cli/src/main.rs b/demos/cli/src/main.rs index 053728eb..1a31a924 100644 --- a/demos/cli/src/main.rs +++ b/demos/cli/src/main.rs @@ -119,10 +119,10 @@ fn run3d( if !isometric { *mat.matrix_mut().get_mut((3, 2)).unwrap() = 0.3; } - let cfg = fidget::render::RenderConfig { + let cfg = fidget::render::VoxelRenderConfig { image_size: fidget::render::VoxelSize::from(settings.size), tile_sizes: F::tile_sizes_3d(), - threads: settings.threads, + threads: settings.threads.into(), ..Default::default() }; let shape = shape.apply_transform(mat.into()); @@ -197,10 +197,10 @@ fn run2d( .flat_map(|i| i.into_iter()) .collect() } else { - let cfg = fidget::render::RenderConfig { + let cfg = fidget::render::ImageRenderConfig { image_size: fidget::render::ImageSize::from(settings.size), tile_sizes: F::tile_sizes_2d(), - threads: settings.threads, + threads: settings.threads.into(), ..Default::default() }; if sdf { @@ -241,7 +241,7 @@ fn run_mesh( for _ in 0..settings.n { let settings = fidget::mesh::Settings { - threads: settings.threads, + threads: settings.threads.into(), depth: settings.depth, ..Default::default() }; diff --git a/demos/viewer/src/main.rs b/demos/viewer/src/main.rs index d12f62c2..d38cba2d 100644 --- a/demos/viewer/src/main.rs +++ b/demos/viewer/src/main.rs @@ -3,11 +3,12 @@ use clap::Parser; use crossbeam_channel::{unbounded, Receiver, Sender}; use eframe::egui; use env_logger::Env; -use fidget::render::RenderConfig; use log::{debug, error, info, warn}; use nalgebra::{Point2, Vector3}; use notify::Watcher; +use fidget::render::{ImageRenderConfig, View2, View3, VoxelRenderConfig}; + use std::{error::Error, path::Path}; /// Minimal viewer, using Fidget to render a Rhai script @@ -161,12 +162,12 @@ fn render( pixels: &mut [egui::Color32], ) { match mode { - RenderMode::TwoD { camera, mode, .. } => { - let config = RenderConfig { + RenderMode::TwoD { view, mode, .. } => { + let config = ImageRenderConfig { image_size, tile_sizes: F::tile_sizes_2d(), - camera: *camera, - ..RenderConfig::default() + view: *view, + ..Default::default() }; match mode { @@ -212,18 +213,18 @@ fn render( } RenderMode::ThreeD(camera, mode) => { // XXX allow selection of depth? - let config = RenderConfig { + let config = VoxelRenderConfig { image_size: fidget::render::VoxelSize::new( image_size.width(), image_size.height(), 512, ), tile_sizes: F::tile_sizes_3d(), - camera: fidget::render::Camera::from_center_and_scale( + view: View3::from_center_and_scale( Vector3::new(camera.offset.x, camera.offset.y, 0.0), camera.scale, ), - ..RenderConfig::default() + ..Default::default() }; let (depth, color) = fidget::render::render3d(shape, &config); match mode { @@ -387,7 +388,7 @@ enum ThreeDMode { #[derive(Copy, Clone)] enum RenderMode { TwoD { - camera: fidget::render::Camera<2>, + view: View2, /// Drag start position (in model coordinates) drag_start: Option>, @@ -407,7 +408,7 @@ impl RenderMode { RenderMode::ThreeD(..) => { *self = RenderMode::TwoD { // TODO get parameters from 3D camera here? - camera: fidget::render::Camera::default(), + view: Default::default(), drag_start: None, mode: new_mode, }; @@ -465,7 +466,7 @@ impl ViewerApp { image_rx, mode: RenderMode::TwoD { - camera: fidget::render::Camera::default(), + view: Default::default(), drag_start: None, mode: Mode2D::Color, }, @@ -662,19 +663,18 @@ impl eframe::App for ViewerApp { // Handle pan and zoom match &mut self.mode { RenderMode::TwoD { - camera, drag_start, .. + view, drag_start, .. } => { let image_size = fidget::render::ImageSize::new( rect.width() as u32, rect.height() as u32, ); + let mat = view.world_to_model() * image_size.screen_to_world(); if let Some(pos) = r.interact_pointer_pos() { - let mat = - camera.world_to_model() * image_size.screen_to_world(); let pos = mat.transform_point(&Point2::new(pos.x, pos.y)); if let Some(prev) = *drag_start { - camera.translate(prev - pos); + view.translate(prev - pos); render_changed |= prev != pos; } else { *drag_start = Some(pos); @@ -685,14 +685,11 @@ impl eframe::App for ViewerApp { if r.hovered() { let scroll = ctx.input(|i| i.smooth_scroll_delta.y); - let mouse_pos = - ctx.input(|i| i.pointer.hover_pos()).map(|p| { - image_size - .screen_to_world() - .transform_point(&Point2::new(p.x, p.y)) - }); + let mouse_pos = ctx + .input(|i| i.pointer.hover_pos()) + .map(|p| mat.transform_point(&Point2::new(p.x, p.y))); if scroll != 0.0 { - camera.zoom((scroll / 100.0).exp2(), mouse_pos); + view.zoom((scroll / 100.0).exp2(), mouse_pos); render_changed = true; } } diff --git a/fidget/benches/mesh.rs b/fidget/benches/mesh.rs index 098a25aa..45eb63dc 100644 --- a/fidget/benches/mesh.rs +++ b/fidget/benches/mesh.rs @@ -1,6 +1,7 @@ use criterion::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion, }; +use fidget::render::ThreadCount; const COLONNADE: &str = include_str!("../../models/colonnade.vm"); @@ -12,12 +13,15 @@ pub fn colonnade_octree_thread_sweep(c: &mut Criterion) { let mut group = c.benchmark_group("speed vs threads (colonnade, octree) (depth 6)"); - for threads in [1, 4, 8] { + for threads in std::iter::once(ThreadCount::One) + .chain([1, 4, 8].map(|i| ThreadCount::Many(i.try_into().unwrap()))) + { let cfg = &fidget::mesh::Settings { depth: 6, - threads: threads.try_into().unwrap(), + threads, ..Default::default() }; + #[cfg(feature = "jit")] group.bench_function(BenchmarkId::new("jit", threads), move |b| { b.iter(|| { @@ -45,11 +49,10 @@ pub fn colonnade_mesh(c: &mut Criterion) { let mut group = c.benchmark_group("speed vs threads (colonnade, meshing) (depth 8)"); - for threads in [1, 4, 8] { - let cfg = &fidget::mesh::Settings { - threads: threads.try_into().unwrap(), - ..cfg - }; + for threads in std::iter::once(ThreadCount::One) + .chain([1, 4, 8].map(|i| ThreadCount::Many(i.try_into().unwrap()))) + { + let cfg = &fidget::mesh::Settings { threads, ..cfg }; group.bench_function( BenchmarkId::new("walk_dual", threads), move |b| { diff --git a/fidget/benches/render.rs b/fidget/benches/render.rs index 5ce6376f..f7225a3a 100644 --- a/fidget/benches/render.rs +++ b/fidget/benches/render.rs @@ -1,7 +1,10 @@ use criterion::{ black_box, criterion_group, criterion_main, BenchmarkId, Criterion, }; -use fidget::{render::ImageSize, shape::RenderHints}; +use fidget::{ + render::{ImageSize, ThreadCount}, + shape::RenderHints, +}; const PROSPERO: &str = include_str!("../../models/prospero.vm"); @@ -15,7 +18,7 @@ pub fn prospero_size_sweep(c: &mut Criterion) { let mut group = c.benchmark_group("speed vs image size (prospero, 2d) (8 threads)"); for size in [256, 512, 768, 1024, 1280, 1546, 1792, 2048] { - let cfg = &fidget::render::RenderConfig { + let cfg = &fidget::render::ImageRenderConfig { image_size: fidget::render::ImageSize::from(size), tile_sizes: fidget::vm::VmFunction::tile_sizes_2d(), ..Default::default() @@ -32,7 +35,7 @@ pub fn prospero_size_sweep(c: &mut Criterion) { #[cfg(feature = "jit")] { - let cfg = &fidget::render::RenderConfig { + let cfg = &fidget::render::ImageRenderConfig { image_size: fidget::render::ImageSize::from(size), tile_sizes: fidget::jit::JitFunction::tile_sizes_2d(), ..Default::default() @@ -59,11 +62,13 @@ pub fn prospero_thread_sweep(c: &mut Criterion) { let mut group = c.benchmark_group("speed vs threads (prospero, 2d) (1024 x 1024)"); - for threads in [1, 2, 4, 8, 16] { - let cfg = &fidget::render::RenderConfig { + for threads in std::iter::once(ThreadCount::One).chain( + [1, 2, 4, 8, 16].map(|i| ThreadCount::Many(i.try_into().unwrap())), + ) { + let cfg = &fidget::render::ImageRenderConfig { image_size: ImageSize::from(1024), tile_sizes: fidget::vm::VmFunction::tile_sizes_2d(), - threads: threads.try_into().unwrap(), + threads, ..Default::default() }; group.bench_function(BenchmarkId::new("vm", threads), move |b| { @@ -77,10 +82,10 @@ pub fn prospero_thread_sweep(c: &mut Criterion) { }); #[cfg(feature = "jit")] { - let cfg = &fidget::render::RenderConfig { + let cfg = &fidget::render::ImageRenderConfig { image_size: ImageSize::from(1024), tile_sizes: fidget::jit::JitFunction::tile_sizes_2d(), - threads: threads.try_into().unwrap(), + threads, ..Default::default() }; group.bench_function(BenchmarkId::new("jit", threads), move |b| { diff --git a/fidget/src/core/shape/mod.rs b/fidget/src/core/shape/mod.rs index 2c953d8a..b70da20e 100644 --- a/fidget/src/core/shape/mod.rs +++ b/fidget/src/core/shape/mod.rs @@ -33,7 +33,7 @@ use crate::{ var::{Var, VarIndex, VarMap}, Error, }; -use nalgebra::{Matrix4, Point3}; +use nalgebra::{Matrix4, Point2, Point3}; use std::collections::HashMap; /// A shape represents an implicit surface @@ -333,6 +333,20 @@ impl TileSizes { pub fn get(&self, i: usize) -> Option { self.0.get(i).copied() } + + /// Returns the data offset of a global pixel position within a root tile + /// + /// The root tile is implicit: it's set by the largest tile size and aligned + /// to multiples of that size. + #[inline] + pub(crate) fn pixel_offset(&self, pos: Point2) -> usize { + // Find the relative position within the root tile + let x = pos.x % self.0[0]; + let y = pos.y % self.0[0]; + + // Apply the relative offset and find the data index + x + y * self.0[0] + } } /// Hints for how to render this particular type diff --git a/fidget/src/lib.rs b/fidget/src/lib.rs index 14185c61..01317488 100644 --- a/fidget/src/lib.rs +++ b/fidget/src/lib.rs @@ -209,16 +209,16 @@ //! ``` //! use fidget::{ //! context::{Tree, Context}, -//! render::{BitRenderMode, ImageSize, RenderConfig}, +//! render::{BitRenderMode, ImageSize, ImageRenderConfig}, //! vm::VmShape, //! }; //! //! let x = Tree::x(); //! let y = Tree::y(); //! let tree = (x.square() + y.square()).sqrt() - 1.0; -//! let cfg = RenderConfig::<2> { +//! let cfg = ImageRenderConfig { //! image_size: ImageSize::from(32), -//! ..RenderConfig::default() +//! ..Default::default() //! }; //! let shape = VmShape::from(tree); //! let out = cfg.run::<_, BitRenderMode>(shape)?; diff --git a/fidget/src/mesh/mod.rs b/fidget/src/mesh/mod.rs index eacaca60..d645adf4 100644 --- a/fidget/src/mesh/mod.rs +++ b/fidget/src/mesh/mod.rs @@ -39,8 +39,6 @@ //! # Ok::<(), fidget::Error>(()) //! ``` -use crate::render::Camera; - mod builder; mod cell; mod dc; @@ -50,6 +48,8 @@ mod octree; mod output; mod qef; +use crate::render::{ThreadCount, View3}; + #[cfg(not(target_arch = "wasm32"))] mod mt; @@ -83,37 +83,28 @@ pub struct Settings { /// Depth to recurse in the octree pub depth: u8, - /// Camera to provide a world-to-model transform - pub camera: Camera<3>, + /// Viewport to provide a world-to-model transform + pub view: View3, /// Number of threads to use /// /// 1 indicates to use the single-threaded evaluator; other values will /// spin up _N_ threads to perform octree construction in parallel. - #[cfg(not(target_arch = "wasm32"))] - pub threads: std::num::NonZeroUsize, + pub threads: ThreadCount, } impl Default for Settings { fn default() -> Self { Self { depth: 3, - camera: Default::default(), - - #[cfg(not(target_arch = "wasm32"))] - threads: std::num::NonZeroUsize::new(8).unwrap(), + view: Default::default(), + threads: ThreadCount::default(), } } } -impl Settings { - #[cfg(not(target_arch = "wasm32"))] - fn threads(&self) -> usize { - self.threads.get() - } - - #[cfg(target_arch = "wasm32")] - fn threads(&self) -> usize { - 1 - } +/// Strong type for multithreaded settings +pub(crate) struct MultithreadedSettings { + pub depth: u8, + pub threads: std::num::NonZeroUsize, } diff --git a/fidget/src/mesh/mt/dc.rs b/fidget/src/mesh/mt/dc.rs index 021f742d..13cffec6 100644 --- a/fidget/src/mesh/mt/dc.rs +++ b/fidget/src/mesh/mt/dc.rs @@ -7,7 +7,10 @@ use crate::mesh::{ types::{X, Y, Z}, Mesh, Octree, }; -use std::sync::atomic::{AtomicU64, Ordering}; +use std::{ + num::NonZeroUsize, + sync::atomic::{AtomicU64, Ordering}, +}; #[derive(Debug)] enum Task { @@ -52,7 +55,7 @@ pub struct DcWorker<'a> { } impl<'a> DcWorker<'a> { - pub fn scheduler(octree: &Octree, threads: usize) -> Mesh { + pub fn scheduler(octree: &Octree, threads: NonZeroUsize) -> Mesh { let queues = QueuePool::new(threads); let map = octree diff --git a/fidget/src/mesh/mt/octree.rs b/fidget/src/mesh/mt/octree.rs index c9331ab7..54b623e4 100644 --- a/fidget/src/mesh/mt/octree.rs +++ b/fidget/src/mesh/mt/octree.rs @@ -6,7 +6,7 @@ use crate::{ cell::{Cell, CellData, CellIndex}, octree::{BranchResult, CellResult, EvalGroup, OctreeBuilder}, types::Corner, - Octree, Settings, + MultithreadedSettings, Octree, }, shape::RenderHints, }; @@ -118,10 +118,14 @@ pub struct OctreeWorker { } impl OctreeWorker { - pub fn scheduler(eval: Arc>, settings: Settings) -> Octree { - let task_queues = QueuePool::new(settings.threads()); + pub fn scheduler( + eval: Arc>, + settings: MultithreadedSettings, + ) -> Octree { + let thread_count = settings.threads.get(); + let task_queues = QueuePool::new(settings.threads); let done_queues = std::iter::repeat_with(std::sync::mpsc::channel) - .take(settings.threads()) + .take(thread_count) .collect::>(); let friend_done = done_queues.iter().map(|t| t.0.clone()).collect::>(); @@ -144,7 +148,7 @@ impl OctreeWorker { .collect::>(); let root = CellIndex::default(); - let r = workers[0].octree.eval_cell(&eval, root, settings); + let r = workers[0].octree.eval_cell(&eval, root, settings.depth); let c = match r { CellResult::Done(cell) => Some(cell), CellResult::Recurse(eval) => { @@ -157,11 +161,11 @@ impl OctreeWorker { workers[0].octree.record(0, c.into()); workers.into_iter().next().unwrap().octree.into() } else { - let pool = &ThreadPool::new(settings.threads()); + let pool = &ThreadPool::new(settings.threads); let out: Vec = std::thread::scope(|s| { let mut handles = vec![]; for w in workers { - handles.push(s.spawn(move || w.run(pool, settings))); + handles.push(s.spawn(move || w.run(pool, settings.depth))); } handles.into_iter().map(|h| h.join().unwrap()).collect() }); @@ -170,7 +174,7 @@ impl OctreeWorker { } /// Runs a single worker to completion as part of a worker group - pub fn run(mut self, threads: &ThreadPool, settings: Settings) -> Octree { + pub fn run(mut self, threads: &ThreadPool, max_depth: u8) -> Octree { let mut ctx = threads.start(self.thread_index); loop { // First, check to see if anyone has finished a task and sent us @@ -205,7 +209,7 @@ impl OctreeWorker { for i in Corner::iter() { let sub_cell = task.target_cell.child(index, i); - match self.octree.eval_cell(&task.eval, sub_cell, settings) + match self.octree.eval_cell(&task.eval, sub_cell, max_depth) { // If this child is finished, then record it locally. // If it's a branching cell, then we'll let a caller diff --git a/fidget/src/mesh/mt/pool.rs b/fidget/src/mesh/mt/pool.rs index 89a2331c..fe4fc5ff 100644 --- a/fidget/src/mesh/mt/pool.rs +++ b/fidget/src/mesh/mt/pool.rs @@ -1,5 +1,8 @@ //! Minimal utilities for thread pooling -use std::sync::atomic::{AtomicUsize, Ordering}; +use std::{ + num::NonZeroUsize, + sync::atomic::{AtomicUsize, Ordering}, +}; /// Stores data used to synchronize a thread pool pub struct ThreadPool { @@ -9,9 +12,12 @@ pub struct ThreadPool { impl ThreadPool { /// Builds thread pool storage for `n` threads - pub fn new(n: usize) -> Self { + pub fn new(n: NonZeroUsize) -> Self { Self { - threads: std::sync::RwLock::new(vec![std::thread::current(); n]), + threads: std::sync::RwLock::new(vec![ + std::thread::current(); + n.get() + ]), counter: AtomicUsize::new(0), } } @@ -196,8 +202,8 @@ pub struct QueuePool { impl QueuePool { /// Builds a new set of queues for `n` threads - pub fn new(n: usize) -> Vec { - let task_queues = (0..n) + pub fn new(n: NonZeroUsize) -> Vec { + let task_queues = (0..n.get()) .map(|_| crossbeam_deque::Worker::::new_lifo()) .collect::>(); @@ -260,7 +266,7 @@ mod test { #[test] fn queue_pool() { - let mut queues = QueuePool::new(2); + let mut queues = QueuePool::new(2.try_into().unwrap()); let mut counters = [0i32; 2]; const DEPTH: usize = 6; queues[0].push(DEPTH); @@ -298,7 +304,7 @@ mod test { #[test] fn thread_ctx() { const N: usize = 8; - let pool = &ThreadPool::new(N); + let pool = &ThreadPool::new(N.try_into().unwrap()); let done = &AtomicUsize::new(0); std::thread::scope(|s| { @@ -329,8 +335,8 @@ mod test { #[test] fn queue_and_thread_pool() { const N: usize = 8; - let mut queues = QueuePool::new(N); - let pool = &ThreadPool::new(N); + let mut queues = QueuePool::new(N.try_into().unwrap()); + let pool = &ThreadPool::new(N.try_into().unwrap()); let mut counters = [0i32; N]; const DEPTH: usize = 16; queues[0].push(DEPTH); diff --git a/fidget/src/mesh/octree.rs b/fidget/src/mesh/octree.rs index 243e5149..bd4ebd11 100644 --- a/fidget/src/mesh/octree.rs +++ b/fidget/src/mesh/octree.rs @@ -8,10 +8,11 @@ use super::{ gen::CELL_TO_VERT_TO_EDGES, qef::QuadraticErrorSolver, types::{Axis, Corner, Edge}, - Mesh, Settings, + Mesh, MultithreadedSettings, Settings, }; use crate::{ eval::{BulkEvaluator, Function, TracingEvaluator}, + render::ThreadCount, shape::{RenderHints, Shape, ShapeBulkEval, ShapeTape, ShapeTracingEval}, types::Grad, }; @@ -100,7 +101,7 @@ impl Octree { settings: Settings, ) -> Self { // Transform the shape given our world-to-model matrix - let t = settings.camera.world_to_model(); + let t = settings.view.world_to_model(); if t == nalgebra::Matrix4::identity() { Self::build_inner(shape, settings) } else { @@ -123,16 +124,21 @@ impl Octree { ) -> Self { let eval = Arc::new(EvalGroup::new(shape.clone())); - if settings.threads() == 1 { - let mut out = OctreeBuilder::new(); - out.recurse(&eval, CellIndex::default(), settings); - out.into() - } else { - #[cfg(target_arch = "wasm32")] - unreachable!("cannot use multithreaded evaluator on wasm32"); + match settings.threads { + ThreadCount::One => { + let mut out = OctreeBuilder::new(); + out.recurse(&eval, CellIndex::default(), settings.depth); + out.into() + } #[cfg(not(target_arch = "wasm32"))] - OctreeWorker::scheduler(eval.clone(), settings) + ThreadCount::Many(threads) => OctreeWorker::scheduler( + eval.clone(), + MultithreadedSettings { + depth: settings.depth, + threads, + }, + ), } } @@ -140,15 +146,13 @@ impl Octree { pub fn walk_dual(&self, settings: Settings) -> Mesh { let mut mesh = MeshBuilder::default(); - if settings.threads() == 1 { - mesh.cell(self, CellIndex::default()); - mesh.take() - } else { - #[cfg(target_arch = "wasm32")] - unreachable!("cannot use multithreaded evaluator on wasm32"); - + match settings.threads { + ThreadCount::One => { + mesh.cell(self, CellIndex::default()); + mesh.take() + } #[cfg(not(target_arch = "wasm32"))] - DcWorker::scheduler(self, settings.threads()) + ThreadCount::Many(threads) => DcWorker::scheduler(self, threads), } } @@ -350,7 +354,7 @@ impl OctreeBuilder { &mut self, eval: &Arc>, cell: CellIndex, - settings: Settings, + max_depth: u8, ) -> CellResult { let (i, r) = self .eval_interval @@ -376,7 +380,7 @@ impl OctreeBuilder { } else { None }; - if cell.depth == settings.depth as usize { + if cell.depth == max_depth as usize { let eval = sub_tape.unwrap_or_else(|| eval.clone()); let out = CellResult::Done(self.leaf(&eval, cell)); if let Ok(t) = Arc::try_unwrap(eval) { @@ -429,9 +433,9 @@ impl OctreeBuilder { &mut self, eval: &Arc>, cell: CellIndex, - settings: Settings, + max_depth: u8, ) { - match self.eval_cell(eval, cell, settings) { + match self.eval_cell(eval, cell, max_depth) { CellResult::Done(c) => self.o[cell] = c.into(), CellResult::Recurse(sub_eval) => { let index = self.o.cells.len(); @@ -440,7 +444,7 @@ impl OctreeBuilder { } for i in Corner::iter() { let cell = cell.child(index, i); - self.recurse(&sub_eval, cell, settings); + self.recurse(&sub_eval, cell, max_depth); } if let Ok(t) = Arc::try_unwrap(sub_eval) { @@ -1171,7 +1175,7 @@ mod test { use crate::{ context::Tree, mesh::types::{Edge, X, Y, Z}, - render::Camera, + render::{ThreadCount, View3}, shape::EzShape, vm::{VmFunction, VmShape}, }; @@ -1181,16 +1185,16 @@ mod test { fn depth0_single_thread() -> Settings { Settings { depth: 0, - camera: Camera::default(), - threads: std::num::NonZeroUsize::new(1).unwrap(), + threads: ThreadCount::One, + ..Default::default() } } fn depth1_single_thread() -> Settings { Settings { depth: 1, - camera: Camera::default(), - threads: std::num::NonZeroUsize::new(1).unwrap(), + threads: ThreadCount::One, + ..Default::default() } } @@ -1345,20 +1349,16 @@ mod test { fn test_sphere_manifold() { let shape = VmShape::from(sphere([0.0; 3], 0.85)); - for threads in [1, 8] { + for threads in + [ThreadCount::One, ThreadCount::Many(8.try_into().unwrap())] + { let settings = Settings { depth: 5, - threads: threads.try_into().unwrap(), + threads, ..Default::default() }; let octree = Octree::build(&shape, settings); let sphere_mesh = octree.walk_dual(settings); - sphere_mesh - .write_stl( - &mut std::fs::File::create(format!("sphere{threads}.stl")) - .unwrap(), - ) - .unwrap(); if let Err(e) = check_for_vertex_dupes(&sphere_mesh) { panic!("{e} (with {threads} threads)"); @@ -1478,7 +1478,7 @@ mod test { } } - fn test_mesh_manifold_inner(threads: usize, mask: u8) { + fn test_mesh_manifold_inner(threads: ThreadCount, mask: u8) { let mut shape = vec![]; for j in Corner::iter() { if mask & (1 << j.index()) != 0 { @@ -1500,7 +1500,7 @@ mod test { let shape = VmShape::from(shape); let settings = Settings { depth: 2, - threads: threads.try_into().unwrap(), + threads, ..Default::default() }; let octree = Octree::build(&shape, settings); @@ -1522,14 +1522,17 @@ mod test { #[test] fn test_mesh_manifold_single_thread() { for mask in 0..=255 { - test_mesh_manifold_inner(1, mask) + test_mesh_manifold_inner(ThreadCount::One, mask) } } #[test] fn test_mesh_manifold_multi_thread() { for mask in 0..=255 { - test_mesh_manifold_inner(8, mask) + test_mesh_manifold_inner( + ThreadCount::Many(8.try_into().unwrap()), + mask, + ) } } @@ -1542,7 +1545,7 @@ mod test { let shape = VmShape::from(shape); let eval = Arc::new(EvalGroup::new(shape)); let mut out = OctreeBuilder::new(); - out.recurse(&eval, CellIndex::default(), settings); + out.recurse(&eval, CellIndex::default(), settings.depth); out } @@ -1569,17 +1572,20 @@ mod test { fn test_empty_collapse() { // Make a very smol sphere that won't be sampled let shape = VmShape::from(sphere([0.1; 3], 0.05)); - for threads in [1, 4] { + for threads in + [ThreadCount::One, ThreadCount::Many(4.try_into().unwrap())] + { let settings = Settings { depth: 1, - threads: threads.try_into().unwrap(), + threads, ..Default::default() }; let octree = Octree::build(&shape, settings); + let tc = threads.get().unwrap_or(1); assert_eq!( octree.cells[0], Cell::Empty.into(), - "failed to collapse octree with {threads} threads" + "failed to collapse octree with {tc} threads" ); } } @@ -1590,10 +1596,12 @@ mod test { let (ctx, root) = crate::Context::from_text(COLONNADE.as_bytes()).unwrap(); let tape = VmShape::new(&ctx, root).unwrap(); - for threads in [1, 8] { + for threads in + [ThreadCount::One, ThreadCount::Many(8.try_into().unwrap())] + { let settings = Settings { depth: 5, - threads: threads.try_into().unwrap(), + threads, ..Default::default() }; let octree = Octree::build(&tape, settings); @@ -1688,7 +1696,7 @@ mod test { let settings = Settings { depth: 4, - threads: 1.try_into().unwrap(), + threads: ThreadCount::One, ..Default::default() }; @@ -1706,8 +1714,8 @@ mod test { let center = Vector3::new(1.0, 1.0, 1.0); let settings = Settings { depth: 4, - threads: 1.try_into().unwrap(), - camera: Camera::from_center_and_scale(center, 0.5), + threads: ThreadCount::One, + view: View3::from_center_and_scale(center, 0.5), }; let octree = Octree::build(&shape, settings).walk_dual(settings); diff --git a/fidget/src/render/camera.rs b/fidget/src/render/camera.rs deleted file mode 100644 index 0483f341..00000000 --- a/fidget/src/render/camera.rs +++ /dev/null @@ -1,280 +0,0 @@ -//! Helper types and functions for camera and viewport manipulation -use nalgebra::{ - allocator::Allocator, Const, DefaultAllocator, DimNameAdd, DimNameSub, - DimNameSum, OMatrix, OPoint, OVector, Point2, Point3, U1, -}; - -/// Helper object for a camera in 2D or 3D space -/// -/// Rendering and meshing happen in the ±1 square or cube; these are referred to -/// as _world_ coordinates. A `Camera` generates a homogeneous transform matrix -/// that maps from positions in world coordinates to _model_ coordinates, which -/// can be whatever you want. -/// -/// Here's an example of using a `Camera` to focus on the region `[4, 6]`: -/// -/// ``` -/// # use nalgebra::{Vector2, Point2}; -/// # use fidget::render::{Camera, TransformPoint}; -/// let camera = Camera::<2>::from_center_and_scale( -/// Vector2::new(5.0, 5.0), 1.0 -/// ); -/// -/// // -------d------- -/// // | | -/// // | | -/// // c a b -/// // | | -/// // | | -/// // -------e------- -/// let a = camera.transform_point(&Point2::new(0.0, 0.0)); -/// assert_eq!(a, Point2::new(5.0, 5.0)); -/// -/// let b = camera.transform_point(&Point2::new(1.0, 0.0)); -/// assert_eq!(b, Point2::new(6.0, 5.0)); -/// -/// let c = camera.transform_point(&Point2::new(-1.0, 0.0)); -/// assert_eq!(c, Point2::new(4.0, 5.0)); -/// -/// let d = camera.transform_point(&Point2::new(0.0, 1.0)); -/// assert_eq!(d, Point2::new(5.0, 6.0)); -/// -/// let e = camera.transform_point(&Point2::new(0.0, -1.0)); -/// assert_eq!(e, Point2::new(5.0, 4.0)); -/// ``` -/// -/// See also -/// [`RegionSize::screen_to_world`](crate::render::RegionSize::screen_to_world), -/// which converts from screen to world coordinates. -/// -/// Apologies for the terrible trait bounds; they're necessary to persuade the -/// internals to type-check, but shouldn't be noticeable to library users. -#[derive(Copy, Clone, Debug)] -pub struct Camera -where - Const: DimNameAdd, - DefaultAllocator: - Allocator, U1>, DimNameSum, U1>>, - DefaultAllocator: - Allocator< - < as DimNameAdd>>::Output as DimNameSub< - Const<1>, - >>::Output, - >, - as DimNameAdd>>::Output: DimNameSub>, - OMatrix< - f32, - as DimNameAdd>>::Output, - as DimNameAdd>>::Output, - >: Copy, - Self: TransformPoint, -{ - mat: OMatrix< - f32, - as DimNameAdd>>::Output, - as DimNameAdd>>::Output, - >, -} - -impl Camera -where - Const: DimNameAdd, - DefaultAllocator: - Allocator, U1>, DimNameSum, U1>>, - DefaultAllocator: - Allocator< - < as DimNameAdd>>::Output as DimNameSub< - Const<1>, - >>::Output, - >, - as DimNameAdd>>::Output: DimNameSub>, - OMatrix< - f32, - as DimNameAdd>>::Output, - as DimNameAdd>>::Output, - >: Copy, - Self: TransformPoint, -{ - /// Builds a camera from a center (in world coordinates) and a scale - /// - /// The resulting camera will point at the center, and the viewport will be - /// ± `scale` in size. - pub fn from_center_and_scale( - center: OVector< - f32, - < as DimNameAdd>>::Output as DimNameSub< - Const<1>, - >>::Output, - >, - scale: f32, - ) -> Self { - let mut mat = OMatrix::< - f32, - as DimNameAdd>>::Output, - as DimNameAdd>>::Output, - >::identity(); - mat.append_scaling_mut(scale); - mat.append_translation_mut(¢er); - Self { mat } - } - - /// Returns the world-to-model transform matrix - pub fn world_to_model( - &self, - ) -> OMatrix< - f32, - as DimNameAdd>>::Output, - as DimNameAdd>>::Output, - > { - self.mat - } - - /// Applies a translation (in model units) to the current camera position - pub fn translate( - &mut self, - dt: OVector< - f32, - < as DimNameAdd>>::Output as DimNameSub< - Const<1>, - >>::Output, - >, - ) { - self.mat.append_translation_mut(&dt); - } - - /// Zooms the camera about a particular position in world space - pub fn zoom(&mut self, amount: f32, pos: Option>>) { - match pos { - Some(p) => { - let pos_before = self.transform_point(&p); - self.mat.append_scaling_mut(amount); - let pos_after = self.transform_point(&p); - self.mat.append_translation_mut(&(pos_before - pos_after)); - } - None => { - self.mat.append_scaling_mut(amount); - } - } - } - - fn transform_point( - &self, - pt: &OPoint>, - ) -> OPoint as DimNameAdd>>::Output as DimNameSub>>::Output>{ - TransformPoint::::transform_point(self, pt) - } -} - -/// Helper trait for being able to transform a point -/// -/// `transform_point` is only implemented for specific matrix sizes in -/// `nalgebra`, so we can't make it a function on every `Camera`. -pub trait TransformPoint -where - Const: DimNameAdd, - DefaultAllocator: - Allocator< - < as DimNameAdd>>::Output as DimNameSub< - Const<1>, - >>::Output, - >, - as DimNameAdd>>::Output: DimNameSub>, -{ - /// Transforms a point from one coordinate system to another - fn transform_point( - &self, - pt: &OPoint>, - ) -> OPoint as DimNameAdd>>::Output as DimNameSub>>::Output>; -} - -impl TransformPoint<2> for Camera<2> { - fn transform_point(&self, pt: &Point2) -> Point2 { - self.mat.transform_point(pt) - } -} - -impl TransformPoint<3> for Camera<3> { - fn transform_point(&self, pt: &Point3) -> Point3 { - self.mat.transform_point(pt) - } -} - -impl Default for Camera -where - Const: DimNameAdd, - DefaultAllocator: - Allocator, U1>, DimNameSum, U1>>, - DefaultAllocator: - Allocator< - < as DimNameAdd>>::Output as DimNameSub< - Const<1>, - >>::Output, - >, - as DimNameAdd>>::Output: DimNameSub>, - OMatrix< - f32, - as DimNameAdd>>::Output, - as DimNameAdd>>::Output, - >: Copy, - Self: TransformPoint, -{ - fn default() -> Self { - Self { - mat: OMatrix::< - f32, - as DimNameAdd>>::Output, - as DimNameAdd>>::Output, - >::identity(), - } - } -} - -#[cfg(test)] -mod test { - use super::*; - use nalgebra::{Point3, Vector3}; - - #[test] - fn test_camera_from_center_and_scale() { - let c = Camera::<3>::from_center_and_scale( - Vector3::new(1.0, 2.0, 3.0), - 5.0, - ); - let mat = c.world_to_model(); - - let pt = mat.transform_point(&Point3::new(0.0, 0.0, 0.0)); - assert_eq!(pt.x, 1.0); - assert_eq!(pt.y, 2.0); - assert_eq!(pt.z, 3.0); - - let pt = mat.transform_point(&Point3::new(1.0, 0.0, 0.0)); - assert_eq!(pt.x, 1.0 + 5.0); - assert_eq!(pt.y, 2.0); - assert_eq!(pt.z, 3.0); - - let pt = mat.transform_point(&Point3::new(-1.0, 0.0, 0.0)); - assert_eq!(pt.x, 1.0 - 5.0); - assert_eq!(pt.y, 2.0); - assert_eq!(pt.z, 3.0); - - let pt = mat.transform_point(&Point3::new(0.0, 1.0, 0.0)); - assert_eq!(pt.x, 1.0); - assert_eq!(pt.y, 2.0 + 5.0); - assert_eq!(pt.z, 3.0); - - let pt = mat.transform_point(&Point3::new(0.0, -1.0, 0.0)); - assert_eq!(pt.x, 1.0); - assert_eq!(pt.y, 2.0 - 5.0); - assert_eq!(pt.z, 3.0); - - let pt = mat.transform_point(&Point3::new(0.0, 0.0, 1.0)); - assert_eq!(pt.x, 1.0); - assert_eq!(pt.y, 2.0); - assert_eq!(pt.z, 3.0 + 5.0); - - let pt = mat.transform_point(&Point3::new(0.0, 0.0, -1.0)); - assert_eq!(pt.x, 1.0); - assert_eq!(pt.y, 2.0); - assert_eq!(pt.z, 3.0 - 5.0); - } -} diff --git a/fidget/src/render/config.rs b/fidget/src/render/config.rs index 3dac5936..8c293a76 100644 --- a/fidget/src/render/config.rs +++ b/fidget/src/render/config.rs @@ -1,119 +1,174 @@ use crate::{ eval::Function, - render::{Camera, RegionSize, RenderMode, TransformPoint}, + render::{RegionSize, RenderMode, View2, View3}, shape::{Shape, TileSizes}, Error, }; -use nalgebra::{ - allocator::Allocator, Const, DefaultAllocator, DimNameAdd, DimNameSub, - DimNameSum, OMatrix, OPoint, OVector, U1, -}; +use nalgebra::{Const, Matrix3, Matrix4, OPoint, Point2, Vector2}; use std::sync::atomic::{AtomicUsize, Ordering}; -/// Container to store render configuration (resolution, etc) -pub struct RenderConfig -where - Const: DimNameAdd, - DefaultAllocator: Allocator, U1>, DimNameSum, U1>>, - DefaultAllocator: Allocator<< as DimNameAdd>>::Output as DimNameSub>>::Output>, - as DimNameAdd>>::Output: DimNameSub>, - OVector as DimNameAdd>>::Output as DimNameSub>>::Output>: Copy, - OMatrix as DimNameAdd>>::Output, as DimNameAdd>>::Output>: Copy, - Camera: TransformPoint, -{ - /// Image size (provides screen-to-world transform) - pub image_size: RegionSize, - - /// Camera (provides world-to-model transform) - pub camera: Camera, +/// Number of threads to use during evaluation +/// +/// In a WebAssembly build, only the [`ThreadCount::One`] variant is available. +#[derive(Copy, Clone, Debug)] +pub enum ThreadCount { + /// Perform all evaluation in the main thread, not spawning any workers + One, + + /// Spawn some number of worker threads for evaluation + #[cfg(not(target_arch = "wasm32"))] + Many(std::num::NonZeroUsize), +} + +impl From for ThreadCount { + fn from(v: std::num::NonZeroUsize) -> Self { + match v.get() { + 0 => unreachable!(), + 1 => ThreadCount::One, + _ => ThreadCount::Many(v), + } + } +} + +impl std::fmt::Display for ThreadCount { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ThreadCount::One => write!(f, "-"), + ThreadCount::Many(n) => write!(f, "{n}"), + } + } +} + +impl ThreadCount { + /// Gets the thread count + /// + /// Returns `None` if we are required to be single-threaded + pub fn get(&self) -> Option { + match self { + ThreadCount::One => None, + #[cfg(not(target_arch = "wasm32"))] + ThreadCount::Many(v) => Some(v.get()), + } + } +} + +impl Default for ThreadCount { + #[cfg(target_arch = "wasm32")] + fn default() -> Self { + Self::One + } + + #[cfg(not(target_arch = "wasm32"))] + fn default() -> Self { + Self::Many(std::num::NonZeroUsize::new(8).unwrap()) + } +} + +/// Settings for 2D rendering +pub struct ImageRenderConfig { + /// Render size + pub image_size: RegionSize<2>, + + /// World-to-model transform + pub view: View2, /// Tile sizes to use during evaluation. /// /// You'll likely want to use /// [`RenderHints::tile_sizes_2d`](crate::shape::RenderHints::tile_sizes_2d) - /// or - /// [`RenderHints::tile_sizes_3d`](crate::shape::RenderHints::tile_sizes_3d) /// to select this based on evaluator type. pub tile_sizes: TileSizes, - /// Number of threads to use; 8 by default - #[cfg(not(target_arch = "wasm32"))] - pub threads: std::num::NonZeroUsize, + /// Number of worker threads + pub threads: ThreadCount, } -impl Default for RenderConfig -where - Const: DimNameAdd, - DefaultAllocator: Allocator, U1>, DimNameSum, U1>>, - DefaultAllocator: Allocator<< as DimNameAdd>>::Output as DimNameSub>>::Output>, - as DimNameAdd>>::Output: DimNameSub>, - OVector as DimNameAdd>>::Output as DimNameSub>>::Output>: Copy, - OMatrix as DimNameAdd>>::Output, as DimNameAdd>>::Output>: Copy, - as DimNameAdd>>::Output as DimNameSub>>::Output>>::Buffer: std::marker::Copy, - Camera: TransformPoint, -{ +impl Default for ImageRenderConfig { fn default() -> Self { Self { image_size: RegionSize::from(512), - tile_sizes: match N { - 2 => TileSizes::new(&[128, 32, 8]).unwrap(), - _ => TileSizes::new(&[128, 64, 32, 16, 8]).unwrap(), - }, - camera: Camera::default(), - - #[cfg(not(target_arch = "wasm32"))] - threads: std::num::NonZeroUsize::new(8).unwrap(), + tile_sizes: TileSizes::new(&[128, 32, 8]).unwrap(), + view: View2::default(), + threads: ThreadCount::default(), } } } -impl RenderConfig -where - Const: DimNameAdd, - DefaultAllocator: Allocator, U1>, DimNameSum, U1>>, - DefaultAllocator: Allocator<< as DimNameAdd>>::Output as DimNameSub>>::Output>, - as DimNameAdd>>::Output: DimNameSub>, - OVector as DimNameAdd>>::Output as DimNameSub>>::Output>: Copy, - OMatrix as DimNameAdd>>::Output, as DimNameAdd>>::Output>: Copy, - as DimNameAdd>>::Output as DimNameSub>>::Output>>::Buffer: Copy, - Camera: TransformPoint, -{ - /// Returns the number of threads to use when rendering +impl ImageRenderConfig { + /// High-level API for rendering shapes in 2D /// - /// This is always 1 for WebAssembly builds - pub fn threads(&self) -> usize { - #[cfg(target_arch = "wasm32")] - let out = 1; - - #[cfg(not(target_arch = "wasm32"))] - let out = self.threads.get(); - - out + /// Under the hood, this delegates to + /// [`fidget::render::render2d`](crate::render::render2d()) + pub fn run( + &self, + shape: Shape, + ) -> Result::Output>, Error> { + Ok(crate::render::render2d::(shape, self)) } /// Returns the combined screen-to-model transform matrix - pub fn mat(&self) -> OMatrix< - f32, - as DimNameAdd>>::Output, - as DimNameAdd>>::Output - > { - self.camera.world_to_model() * self.image_size.screen_to_world() + pub fn mat(&self) -> Matrix3 { + self.view.world_to_model() * self.image_size.screen_to_world() } +} - /// Returns the data offset of a position within a subtile +/// Settings for 3D rendering +pub struct VoxelRenderConfig { + /// Render size /// - /// The position within the subtile is given by `x` and `y`, which are - /// relative coordinates (in the range `0..self.tile_sizes[n]`). + /// The resulting image will have the given width and height; depth sets the + /// number of voxels to evaluate within each pixel of the image (stacked + /// into a column going into the screen). + pub image_size: RegionSize<3>, + + /// World-to-model transform + pub view: View3, + + /// Tile sizes to use during evaluation. /// - /// The root tile is assumed to be of size `self.tile_sizes[0]` and aligned. - #[inline] - pub(crate) fn tile_to_offset(&self, tile: Tile, x: usize, y: usize) -> usize { - // Find the relative position within the root tile - let tx = tile.corner[0] % self.tile_sizes[0]; - let ty = tile.corner[1] % self.tile_sizes[0]; + /// You'll likely want to use + /// [`RenderHints::tile_sizes_3d`](crate::shape::RenderHints::tile_sizes_3d) + /// to select this based on evaluator type. + pub tile_sizes: TileSizes, - // Apply the relative offset and find the data index - tx + x + (ty + y) * self.tile_sizes[0] + /// Number of worker threads + pub threads: ThreadCount, +} + +impl Default for VoxelRenderConfig { + fn default() -> Self { + Self { + image_size: RegionSize::from(512), + tile_sizes: TileSizes::new(&[128, 64, 32, 16, 8]).unwrap(), + view: View3::default(), + + threads: ThreadCount::default(), + } + } +} + +impl VoxelRenderConfig { + /// High-level API for rendering shapes in 2D + /// + /// Under the hood, this delegates to + /// [`fidget::render::render3d`](crate::render::render3d()) + /// + /// Returns a tuple of heightmap, RGB image. + pub fn run( + &self, + shape: Shape, + ) -> Result<(Vec, Vec<[u8; 3]>), Error> { + Ok(crate::render::render3d::(shape, self)) + } + + /// Returns the combined screen-to-model transform matrix + pub fn mat(&self) -> Matrix4 { + self.view.world_to_model() * self.image_size.screen_to_world() + } + + /// Returns the data offset of a row within a subtile + pub(crate) fn tile_row_offset(&self, tile: Tile<3>, row: usize) -> usize { + self.tile_sizes.pixel_offset(tile.add(Vector2::new(0, row))) } } @@ -131,6 +186,14 @@ impl Tile { pub(crate) fn new(corner: OPoint>) -> Tile { Tile { corner } } + + /// Converts a relative position within the tile into a global position + /// + /// This function operates in pixel space, using the `.xy` coordinates + pub(crate) fn add(&self, pos: Vector2) -> Point2 { + let corner = Point2::new(self.corner[0], self.corner[1]); + corner + pos + } } /// Worker queue @@ -152,34 +215,6 @@ impl Queue { } } -impl RenderConfig<2> { - /// High-level API for rendering shapes in 2D - /// - /// Under the hood, this delegates to - /// [`fidget::render::render2d`](crate::render::render2d()) - pub fn run( - &self, - shape: Shape, - ) -> Result::Output>, Error> { - Ok(crate::render::render2d::(shape, self)) - } -} - -impl RenderConfig<3> { - /// High-level API for rendering shapes in 2D - /// - /// Under the hood, this delegates to - /// [`fidget::render::render3d`](crate::render::render3d()) - /// - /// Returns a tuple of heightmap, RGB image. - pub fn run( - &self, - shape: Shape, - ) -> Result<(Vec, Vec<[u8; 3]>), Error> { - Ok(crate::render::render3d::(shape, self)) - } -} - //////////////////////////////////////////////////////////////////////////////// #[cfg(test)] @@ -190,7 +225,7 @@ mod test { #[test] fn test_default_render_config() { - let config = RenderConfig::<2> { + let config = ImageRenderConfig { image_size: ImageSize::from(512), ..Default::default() }; @@ -208,7 +243,7 @@ mod test { Point2::new(1.0, -1.0) ); - let config: RenderConfig<2> = RenderConfig { + let config = ImageRenderConfig { image_size: ImageSize::from(575), ..Default::default() }; @@ -235,13 +270,13 @@ mod test { #[test] fn test_camera_render_config() { - let config = RenderConfig::<2> { + let config = ImageRenderConfig { image_size: ImageSize::from(512), - camera: Camera::from_center_and_scale( + view: View2::from_center_and_scale( nalgebra::Vector2::new(0.5, 0.5), 0.5, ), - ..RenderConfig::default() + ..Default::default() }; let mat = config.mat(); assert_eq!( @@ -257,13 +292,13 @@ mod test { Point2::new(1.0, 0.0) ); - let config = RenderConfig::<2> { + let config = ImageRenderConfig { image_size: ImageSize::from(512), - camera: Camera::from_center_and_scale( + view: View2::from_center_and_scale( nalgebra::Vector2::new(0.5, 0.5), 0.25, ), - ..RenderConfig::default() + ..Default::default() }; let mat = config.mat(); assert_eq!( diff --git a/fidget/src/render/mod.rs b/fidget/src/render/mod.rs index e55cfeff..7dfa62cd 100644 --- a/fidget/src/render/mod.rs +++ b/fidget/src/render/mod.rs @@ -10,16 +10,16 @@ use crate::{ }; use std::sync::Arc; -mod camera; mod config; mod region; mod render2d; mod render3d; +mod view; -pub use camera::{Camera, TransformPoint}; +pub use config::{ImageRenderConfig, ThreadCount, VoxelRenderConfig}; pub use region::{ImageSize, RegionSize, VoxelSize}; +pub use view::{View2, View3}; -pub use config::RenderConfig; pub use render2d::render as render2d; pub use render3d::render as render3d; diff --git a/fidget/src/render/render2d.rs b/fidget/src/render/render2d.rs index d007ad74..c226535c 100644 --- a/fidget/src/render/render2d.rs +++ b/fidget/src/render/render2d.rs @@ -2,7 +2,7 @@ use super::RenderHandle; use crate::{ eval::Function, - render::config::{Queue, RenderConfig, Tile}, + render::config::{ImageRenderConfig, Queue, Tile}, shape::{Shape, ShapeBulkEval, ShapeTracingEval}, types::Interval, }; @@ -202,7 +202,7 @@ impl Scratch { /// Per-thread worker struct Worker<'a, F: Function, M: RenderMode> { - config: &'a RenderConfig<2>, + config: &'a ImageRenderConfig, scratch: Scratch, eval_float_slice: ShapeBulkEval, @@ -247,7 +247,10 @@ impl Worker<'_, F, M> { match M::interval(i, depth) { IntervalAction::Fill(fill) => { for y in 0..tile_size { - let start = self.config.tile_to_offset(tile, 0, y); + let start = self + .config + .tile_sizes + .pixel_offset(tile.add(Vector2::new(0, y))); self.image[start..][..tile_size].fill(fill); } return; @@ -268,7 +271,10 @@ impl Worker<'_, F, M> { let v0 = vs[0] * (1.0 - y_frac) + vs[1] * y_frac; let v1 = vs[2] * (1.0 - y_frac) + vs[3] * y_frac; - let mut i = self.config.tile_to_offset(tile, 0, y); + let mut i = self + .config + .tile_sizes + .pixel_offset(tile.add(Vector2::new(0, y))); for x in 0..tile_size { // X interpolation let x_frac = (x as f32 - 1.0) / (tile_size as f32); @@ -340,7 +346,10 @@ impl Worker<'_, F, M> { let mut index = 0; for j in 0..tile_size { - let o = self.config.tile_to_offset(tile, 0, j); + let o = self + .config + .tile_sizes + .pixel_offset(tile.add(Vector2::new(0, j))); for i in 0..tile_size { self.image[o + i] = M::pixel(out[index]); index += 1; @@ -354,7 +363,7 @@ impl Worker<'_, F, M> { fn worker( mut shape: RenderHandle, queue: &Queue<2>, - config: &RenderConfig<2>, + config: &ImageRenderConfig, ) -> Vec<(Tile<2>, Vec)> { let mut out = vec![]; let scratch = Scratch::new(config.tile_sizes.last().pow(2)); @@ -391,7 +400,7 @@ fn worker( /// resulting pixels). pub fn render( shape: Shape, - config: &RenderConfig<2>, + config: &ImageRenderConfig, ) -> Vec { // Convert to a 4x4 matrix and apply to the shape let mat = config.mat(); @@ -404,7 +413,7 @@ pub fn render( fn render_inner( shape: Shape, - config: &RenderConfig<2>, + config: &ImageRenderConfig, ) -> Vec { let mut tiles = vec![]; let t = config.tile_sizes[0]; @@ -420,14 +429,12 @@ fn render_inner( } let queue = Queue::new(tiles); - let threads = config.threads(); + let threads = config.threads.get(); let mut rh = RenderHandle::new(shape); let _ = rh.i_tape(&mut vec![]); // populate i_tape before cloning - let out: Vec<_> = if threads == 1 { - worker::(rh, &queue, config).into_iter().collect() - } else { + let out: Vec<_> = if let Some(threads) = threads { #[cfg(target_arch = "wasm32")] unreachable!("multithreaded rendering is not supported on wasm32"); @@ -444,6 +451,8 @@ fn render_inner( } out }) + } else { + worker::(rh, &queue, config).into_iter().collect() }; let mut image = vec![M::Output::default(); width * height]; @@ -468,7 +477,7 @@ mod test { use super::*; use crate::{ eval::{Function, MathFunction}, - render::{Camera, ImageSize}, + render::{ImageSize, View2}, shape::Shape, vm::{GenericVmFunction, VmFunction}, Context, @@ -481,17 +490,17 @@ mod test { "/../models/quarter.vm" )); - fn render_and_compare_with_camera( + fn render_and_compare_with_view( shape: Shape, expected: &'static str, - camera: Camera<2>, + view: View2, wide: bool, ) { let width = if wide { 64 } else { 32 }; - let cfg = RenderConfig::<2> { + let cfg = ImageRenderConfig { image_size: ImageSize::new(width, 32), - camera, - ..RenderConfig::default() + view, + ..Default::default() }; let out = cfg.run::<_, BitRenderMode>(shape).unwrap(); let mut img_str = String::new(); @@ -516,19 +525,14 @@ mod test { shape: Shape, expected: &'static str, ) { - render_and_compare_with_camera( - shape, - expected, - Camera::default(), - false, - ) + render_and_compare_with_view(shape, expected, View2::default(), false) } fn render_and_compare_wide( shape: Shape, expected: &'static str, ) { - render_and_compare_with_camera(shape, expected, Camera::default(), true) + render_and_compare_with_view(shape, expected, View2::default(), true) } fn check_hi() { @@ -688,13 +692,10 @@ mod test { .XXX...........XXX......XXX..... .XXX...........XXX......XXX..... ................................"; - render_and_compare_with_camera( + render_and_compare_with_view( shape, EXPECTED, - Camera::from_center_and_scale( - nalgebra::Vector2::new(0.5, 0.5), - 0.5, - ), + View2::from_center_and_scale(nalgebra::Vector2::new(0.5, 0.5), 0.5), false, ); } diff --git a/fidget/src/render/render3d.rs b/fidget/src/render/render3d.rs index 9e621d29..7a4deac6 100644 --- a/fidget/src/render/render3d.rs +++ b/fidget/src/render/render3d.rs @@ -2,12 +2,12 @@ use super::RenderHandle; use crate::{ eval::Function, - render::config::{Queue, RenderConfig, Tile}, + render::config::{Queue, ThreadCount, Tile, VoxelRenderConfig}, shape::{Shape, ShapeBulkEval, ShapeTracingEval}, types::{Grad, Interval}, }; -use nalgebra::{Point3, Vector3}; +use nalgebra::{Point3, Vector2, Vector3}; use std::collections::HashMap; //////////////////////////////////////////////////////////////////////////////// @@ -46,7 +46,7 @@ impl Scratch { //////////////////////////////////////////////////////////////////////////////// struct Worker<'a, F: Function> { - config: &'a RenderConfig<3>, + config: &'a VoxelRenderConfig, /// Reusable workspace for evaluation, to minimize allocation scratch: Scratch, @@ -75,7 +75,7 @@ impl Worker<'_, F> { let tile_size = self.config.tile_sizes[depth]; let fill_z = (tile.corner[2] + tile_size + 1).try_into().unwrap(); if (0..tile_size).all(|y| { - let i = self.config.tile_to_offset(tile, 0, y); + let i = self.config.tile_row_offset(tile, y); (0..tile_size).all(|x| self.depth[i + x] >= fill_z) }) { return; @@ -95,7 +95,7 @@ impl Worker<'_, F> { // `data_interval` to scratch memory for reuse. if i.upper() < 0.0 { for y in 0..tile_size { - let i = self.config.tile_to_offset(tile, 0, y); + let i = self.config.tile_row_offset(tile, y); for x in 0..tile_size { self.depth[i + x] = self.depth[i + x].max(fill_z); } @@ -157,7 +157,10 @@ impl Worker<'_, F> { let i = xy % tile_size; let j = xy / tile_size; - let o = self.config.tile_to_offset(tile, i, j); + let o = self + .config + .tile_sizes + .pixel_offset(tile.add(Vector2::new(i, j))); // Skip pixels which are behind the image let zmax = (tile.corner[2] + tile_size).try_into().unwrap(); @@ -222,7 +225,10 @@ impl Worker<'_, F> { let k = tile_size - 1 - k; // Set the depth of the pixel - let o = self.config.tile_to_offset(tile, i, j); + let o = self + .config + .tile_sizes + .pixel_offset(tile.add(Vector2::new(i, j))); let z = (tile.corner[2] + k + 1).try_into().unwrap(); assert!(self.depth[o] < z); self.depth[o] = z; @@ -286,7 +292,7 @@ fn worker( mut shape: RenderHandle, queues: &[Queue<3>], mut index: usize, - config: &RenderConfig<3>, + config: &VoxelRenderConfig, ) -> HashMap<[usize; 2], Image> { let mut out = HashMap::new(); @@ -354,7 +360,7 @@ fn worker( /// perform evaluation. pub fn render( shape: Shape, - config: &RenderConfig<3>, + config: &VoxelRenderConfig, ) -> (Vec, Vec<[u8; 3]>) { let shape = shape.apply_transform(config.mat()); render_inner(shape, config) @@ -362,7 +368,7 @@ pub fn render( pub fn render_inner( shape: Shape, - config: &RenderConfig<3>, + config: &VoxelRenderConfig, ) -> (Vec, Vec<[u8; 3]>) { let mut tiles = vec![]; let t = config.tile_sizes[0]; @@ -381,8 +387,7 @@ pub fn render_inner( } } - let threads = config.threads(); - + let threads = config.threads.get().unwrap_or(1); let tiles_per_thread = (tiles.len() / threads).max(1); let mut tile_queues = vec![]; for ts in tiles.chunks(tiles_per_thread) { @@ -394,20 +399,17 @@ pub fn render_inner( let _ = rh.i_tape(&mut vec![]); // populate i_tape before cloning // Special-case for single-threaded operation, to give simpler backtraces - let out: Vec<_> = if threads == 1 { - worker::(rh, tile_queues.as_slice(), 0, config) + let out: Vec<_> = match config.threads { + ThreadCount::One => worker::(rh, tile_queues.as_slice(), 0, config) .into_iter() - .collect() - } else { - #[cfg(target_arch = "wasm32")] - unreachable!("multithreaded rendering is not supported on wasm32"); + .collect(), #[cfg(not(target_arch = "wasm32"))] - std::thread::scope(|s| { + ThreadCount::Many(threads) => std::thread::scope(|s| { let config = &config; let mut handles = vec![]; let queues = tile_queues.as_slice(); - for i in 0..threads { + for i in 0..threads.get() { let rh = rh.clone(); handles .push(s.spawn(move || worker::(rh, queues, i, config))); @@ -417,7 +419,7 @@ pub fn render_inner( out.extend(h.join().unwrap().into_iter()); } out - }) + }), }; let mut image_depth = vec![0; width * height]; @@ -454,9 +456,9 @@ mod test { let x = ctx.x(); let shape = VmShape::new(&ctx, x).unwrap(); - let cfg = RenderConfig::<3> { + let cfg = VoxelRenderConfig { image_size: VoxelSize::from(128), // very small! - ..RenderConfig::default() + ..Default::default() }; let out = cfg.run(shape); assert!(out.is_ok()); diff --git a/fidget/src/render/view.rs b/fidget/src/render/view.rs new file mode 100644 index 00000000..f1f9c144 --- /dev/null +++ b/fidget/src/render/view.rs @@ -0,0 +1,125 @@ +use nalgebra::{ + geometry::{Similarity2, Similarity3}, + Matrix3, Matrix4, Point2, Vector2, Vector3, +}; + +/// Helper object for a camera in 2D or 3D space +/// +/// Rendering and meshing happen in the ±1 square or cube; these are referred to +/// as _world_ coordinates. A `Camera` generates a homogeneous transform matrix +/// that maps from positions in world coordinates to _model_ coordinates, which +/// can be whatever you want. +/// +/// Here's an example of using a `View2` to focus on the region `[4, 6]`: +/// +/// ``` +/// # use nalgebra::{Vector2, Point2}; +/// # use fidget::render::{View2}; +/// let view = View2::from_center_and_scale(Vector2::new(5.0, 5.0), 1.0); +/// +/// // -------d------- +/// // | | +/// // | | +/// // c a b +/// // | | +/// // | | +/// // -------e------- +/// let a = view.transform_point(&Point2::new(0.0, 0.0)); +/// assert_eq!(a, Point2::new(5.0, 5.0)); +/// +/// let b = view.transform_point(&Point2::new(1.0, 0.0)); +/// assert_eq!(b, Point2::new(6.0, 5.0)); +/// +/// let c = view.transform_point(&Point2::new(-1.0, 0.0)); +/// assert_eq!(c, Point2::new(4.0, 5.0)); +/// +/// let d = view.transform_point(&Point2::new(0.0, 1.0)); +/// assert_eq!(d, Point2::new(5.0, 6.0)); +/// +/// let e = view.transform_point(&Point2::new(0.0, -1.0)); +/// assert_eq!(e, Point2::new(5.0, 4.0)); +/// ``` +/// +/// See also +/// [`RegionSize::screen_to_world`](crate::render::RegionSize::screen_to_world), +/// which converts from screen to world coordinates. +#[derive(Copy, Clone, Debug)] +pub struct View2 { + mat: Similarity2, +} + +impl Default for View2 { + fn default() -> Self { + Self { + mat: Similarity2::identity(), + } + } +} + +impl View2 { + /// Builds a camera from a center (in world coordinates) and a scale + /// + /// The resulting camera will point at the center, and the viewport will be + /// ± `scale` in size. + pub fn from_center_and_scale(center: Vector2, scale: f32) -> Self { + let mat = + Similarity2::from_parts(center.into(), Default::default(), scale); + Self { mat } + } + /// Returns the world-to-model transform matrix + pub fn world_to_model(&self) -> Matrix3 { + self.mat.into() + } + /// Transform a point from world to model space + pub fn transform_point(&self, p: &Point2) -> Point2 { + self.mat.transform_point(p) + } + + /// Applies a translation (in model units) to the current camera position + pub fn translate(&mut self, dt: Vector2) { + self.mat.append_translation_mut(&dt.into()); + } + + /// Zooms the camera about a particular position (in model space) + pub fn zoom(&mut self, amount: f32, pos: Option>) { + match pos { + Some(before) => { + // Convert to world space before scaling + let p = self.mat.inverse_transform_point(&before); + self.mat.append_scaling_mut(amount); + let pos_after = self.transform_point(&p); + self.mat + .append_translation_mut(&(before - pos_after).into()); + } + None => { + self.mat.append_scaling_mut(amount); + } + } + } +} + +/// See [`View2`] for docstrings +#[derive(Copy, Clone, Debug)] +pub struct View3 { + mat: Similarity3, +} + +impl Default for View3 { + fn default() -> Self { + Self { + mat: Similarity3::identity(), + } + } +} + +#[allow(missing_docs)] +impl View3 { + pub fn from_center_and_scale(center: Vector3, scale: f32) -> Self { + let mat = + Similarity3::from_parts(center.into(), Default::default(), scale); + Self { mat } + } + pub fn world_to_model(&self) -> Matrix4 { + self.mat.into() + } +}