From 3b94b8cf0fc1b0bff7fef02453ea9a70e3862bf1 Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Sun, 17 Nov 2024 14:55:59 -0500 Subject: [PATCH] Add `ShapeVars` helper type; use it in rendering + meshing (#190) - Add new `ShapeVars` type, representing a map from `VarIndex -> T`. This type is used for high-level rendering and meshing of `Shape` objects that include supplementary variables - Add `Octree::build_with_vars` and `Image/VoxelRenderConfig::run_with_vars` functions for shapes with supplementary variables - Change `ShapeBulkEval::eval_v` to take **single** variables (i.e. `x`, `y`, `z` vary but each variable has a constant value across the whole slice). Add `ShapeBulkEval::eval_vs` if `x`, `y`, `z` and variables are _all_ changing through the slices. --- CHANGELOG.md | 9 + fidget/src/core/eval/test/float_slice.rs | 18 +- fidget/src/core/eval/test/point.rs | 5 +- fidget/src/core/shape/mod.rs | 145 +++++++++-- fidget/src/mesh/mt/octree.rs | 21 +- fidget/src/mesh/octree.rs | 112 ++++++-- fidget/src/render/config.rs | 24 +- fidget/src/render/render2d.rs | 316 +++++++++++++---------- fidget/src/render/render3d.rs | 112 ++++++-- 9 files changed, 559 insertions(+), 203 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4f11b8..15617058 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,15 @@ - Remove fine-grained features from `fidget` crate, because we aren't actually testing the power-set of feature combinations in CI (and some were breaking!). The only remaining features are `rhai`, `jit` and `eval-tests`. +- Add new `ShapeVars` type, representing a map from `VarIndex -> T`. This + type is used for high-level rendering and meshing of `Shape` objects that + include supplementary variables +- Add `Octree::build_with_vars` and `Image/VoxelRenderConfig::run_with_vars` + functions for shapes with supplementary variables +- Change `ShapeBulkEval::eval_v` to take **single** variables (i.e. `x`, `y`, + `z` vary but each variable has a constant value across the whole slice). Add + `ShapeBulkEval::eval_vs` if `x`, `y`, `z` and variables are _all_ changing + through the slices. # 0.3.3 - `Function` and evaluator types now produce multiple outputs diff --git a/fidget/src/core/eval/test/float_slice.rs b/fidget/src/core/eval/test/float_slice.rs index b1f7046d..addde2f2 100644 --- a/fidget/src/core/eval/test/float_slice.rs +++ b/fidget/src/core/eval/test/float_slice.rs @@ -9,11 +9,10 @@ use super::{ use crate::{ context::Context, eval::{BulkEvaluator, Function, MathFunction, Tape}, - shape::{EzShape, Shape}, - var::{Var, VarIndex}, + shape::{EzShape, Shape, ShapeVars}, + var::Var, Error, }; -use std::collections::HashMap; /// Helper struct to put constrains on our `Shape` object pub struct TestFloatSlice(std::marker::PhantomData<*const F>); @@ -126,21 +125,22 @@ impl TestFloatSlice { assert!(eval .eval(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0]) .is_err()); - let mut h: HashMap = HashMap::new(); + let mut h: ShapeVars<&[f32]> = ShapeVars::new(); assert!(eval - .eval_v(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h) + .eval_vs(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h) .is_err()); let index = v.index().unwrap(); h.insert(index, &[4.0, 5.0]); assert_eq!( - eval.eval_v(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h) + eval.eval_vs(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h) .unwrap(), &[7.0, 10.0] ); + h.insert(index, &[4.0, 5.0, 6.0]); assert!(matches!( - eval.eval_v(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h), - Err(Error::MismatchedSlices) + eval.eval_vs(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h), + Err(Error::MismatchedSlices), )); // Get a new var index that isn't valid for this tape @@ -148,7 +148,7 @@ impl TestFloatSlice { h.insert(index, &[4.0, 5.0]); h.insert(v2.index().unwrap(), &[4.0, 5.0]); assert!(matches!( - eval.eval_v(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h), + eval.eval_vs(&tape, &[1.0, 2.0], &[2.0, 3.0], &[0.0, 0.0], &h), Err(Error::BadVarSlice(..)) )); } diff --git a/fidget/src/core/eval/test/point.rs b/fidget/src/core/eval/test/point.rs index 80b78624..8c74854f 100644 --- a/fidget/src/core/eval/test/point.rs +++ b/fidget/src/core/eval/test/point.rs @@ -9,11 +9,10 @@ use super::{ use crate::{ context::Context, eval::{Function, MathFunction, Tape, TracingEvaluator}, - shape::{EzShape, Shape}, + shape::{EzShape, Shape, ShapeVars}, var::Var, vm::Choice, }; -use std::collections::HashMap; /// Helper struct to put constrains on our `Shape` object pub struct TestPoint(std::marker::PhantomData<*const F>); @@ -369,7 +368,7 @@ where let tape = s.ez_point_tape(); assert!(eval.eval(&tape, 1.0, 2.0, 0.0).is_err()); - let mut h = HashMap::new(); + let mut h = ShapeVars::new(); assert!(eval.eval_v(&tape, 1.0, 2.0, 0.0, &h).is_err()); let index = v.index().unwrap(); diff --git a/fidget/src/core/shape/mod.rs b/fidget/src/core/shape/mod.rs index b6284afb..6504b193 100644 --- a/fidget/src/core/shape/mod.rs +++ b/fidget/src/core/shape/mod.rs @@ -213,6 +213,53 @@ impl Shape { } } +/// Variables bound to values for shape evaluation +/// +/// Note that this cannot store `X`, `Y`, `Z` variables (which are passed in as +/// first-class arguments); it only stores [`Var::V`] values (identified by +/// their inner [`VarIndex`]). +pub struct ShapeVars(HashMap); + +impl Default for ShapeVars { + fn default() -> Self { + Self(HashMap::default()) + } +} + +impl ShapeVars { + /// Builds a new, empty variable set + pub fn new() -> Self { + Self(HashMap::default()) + } + /// Returns the number of variables stored in the set + pub fn len(&self) -> usize { + self.0.len() + } + /// Checks whether the variable set is empty + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + /// Inserts a new variable + /// + /// Returns the previous value (if present) + pub fn insert(&mut self, v: VarIndex, f: F) -> Option { + self.0.insert(v, f) + } + + /// Iterates over values + pub fn values(&self) -> impl Iterator { + self.0.values() + } +} + +impl<'a, F> IntoIterator for &'a ShapeVars { + type Item = (&'a VarIndex, &'a F); + type IntoIter = std::collections::hash_map::Iter<'a, VarIndex, F>; + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + /// Extension trait for working with a shape without thinking much about memory /// /// All of the [`Shape`] functions that use significant amounts of memory @@ -372,20 +419,20 @@ where y: F, z: F, ) -> Result<(E::Data, Option<&E::Trace>), Error> { - let h = HashMap::default(); + let h = ShapeVars::::new(); self.eval_v(tape, x, y, z, &h) } /// Tracing evaluation of a single sample /// /// Before evaluation, the tape's transform matrix is applied (if present). - pub fn eval_v + Copy>( + pub fn eval_v + Copy, V: Into + Copy>( &mut self, tape: &ShapeTape, x: F, y: F, z: F, - vars: &HashMap, + vars: &ShapeVars, ) -> Result<(E::Data, Option<&E::Trace>), Error> { assert_eq!( tape.tape.output_count(), @@ -455,8 +502,8 @@ where /// Bulk evaluation of many samples, without any variables /// /// If the shape includes variables other than `X`, `Y`, `Z`, - /// [`eval_v`](Self::eval_v) should be used instead (and this function will - /// return an error). + /// [`eval_v`](Self::eval_v) or [`eval_vs`](Self::eval_vs) should be used + /// instead (and this function will return an error). /// /// Before evaluation, the tape's transform matrix is applied (if present). pub fn eval( @@ -466,21 +513,19 @@ where y: &[E::Data], z: &[E::Data], ) -> Result<&[E::Data], Error> { - let h: HashMap = HashMap::default(); - self.eval_v(tape, x, y, z, &h) + let h: ShapeVars<&[E::Data]> = ShapeVars::new(); + self.eval_vs(tape, x, y, z, &h) } - /// Bulk evaluation of many samples, with variables - /// - /// Before evaluation, the tape's transform matrix is applied (if present). - pub fn eval_v>( + /// Helper function to do common setup + fn setup( &mut self, tape: &ShapeTape, x: &[E::Data], y: &[E::Data], z: &[E::Data], - vars: &HashMap, - ) -> Result<&[E::Data], Error> { + vars: &ShapeVars, + ) -> Result { assert_eq!( tape.tape.output_count(), 1, @@ -492,9 +537,7 @@ where return Err(Error::MismatchedSlices); } let n = x.len(); - if vars.values().any(|vs| vs.len() != n) { - return Err(Error::MismatchedSlices); - } + let vs = tape.vars(); let expected_vars = vs.len() - vs.get(&Var::X).is_some() as usize @@ -539,10 +582,78 @@ where // TODO fast path if there are no extra vars, reusing slices }; + Ok(n) + } + /// Bulk evaluation of many samples, with slices of variables + /// + /// Each variable is a slice (or `Vec`) of values, which must be the same + /// length as the `x`, `y`, `z` slices. This is in contrast with + /// [`eval_vs`](Self::eval_v), where variables have a single value used for + /// every position in the `x`, `y,` `z` slices. + /// + /// + /// Before evaluation, the tape's transform matrix is applied (if present). + pub fn eval_vs< + V: std::ops::Deref, + G: Into + Copy, + >( + &mut self, + tape: &ShapeTape, + x: &[E::Data], + y: &[E::Data], + z: &[E::Data], + vars: &ShapeVars, + ) -> Result<&[E::Data], Error> { + let n = self.setup(tape, x, y, z, vars)?; + + if vars.values().any(|vs| vs.len() != n) { + return Err(Error::MismatchedSlices); + } + + let vs = tape.vars(); + for (var, value) in vars { + if let Some(i) = vs.get(&Var::V(*var)) { + if i < self.scratch.len() { + for (a, b) in + self.scratch[i].iter_mut().zip(value.deref().iter()) + { + *a = (*b).into(); + } + // TODO fast path if we can use the slices directly? + } else { + return Err(Error::BadVarIndex(i, self.scratch.len())); + } + } else { + // Passing in Bonus Variables is allowed (for now) + } + } + + let out = self.eval.eval(&tape.tape, &self.scratch)?; + Ok(out.borrow(0)) + } + + /// Bulk evaluation of many samples, with fixed variables + /// + /// Each variable has a single value, which is used for every position in + /// the `x`, `y`, `z` slices. This is in contrast with + /// [`eval_vs`](Self::eval_vs), where variables can be different for every + /// position in the `x`, `y,` `z` slices. + /// + /// Before evaluation, the tape's transform matrix is applied (if present). + pub fn eval_v + Copy>( + &mut self, + tape: &ShapeTape, + x: &[E::Data], + y: &[E::Data], + z: &[E::Data], + vars: &ShapeVars, + ) -> Result<&[E::Data], Error> { + self.setup(tape, x, y, z, vars)?; + let vs = tape.vars(); for (var, value) in vars { if let Some(i) = vs.get(&Var::V(*var)) { if i < self.scratch.len() { - self.scratch[i].copy_from_slice(value); + self.scratch[i].fill((*value).into()); } else { return Err(Error::BadVarIndex(i, self.scratch.len())); } diff --git a/fidget/src/mesh/mt/octree.rs b/fidget/src/mesh/mt/octree.rs index 42f52b3c..818757f7 100644 --- a/fidget/src/mesh/mt/octree.rs +++ b/fidget/src/mesh/mt/octree.rs @@ -12,6 +12,7 @@ use crate::{ Octree, }, render::RenderHints, + shape::ShapeVars, }; use std::sync::{mpsc::TryRecvError, Arc}; @@ -123,6 +124,7 @@ pub struct OctreeWorker { impl OctreeWorker { pub fn scheduler( eval: Arc>, + vars: &ShapeVars, settings: MultithreadedSettings, ) -> Octree { let thread_count = settings.threads.get(); @@ -151,7 +153,9 @@ impl OctreeWorker { .collect::>(); let root = CellIndex::default(); - let r = workers[0].octree.eval_cell(&eval, root, settings.depth); + let r = workers[0] + .octree + .eval_cell(&eval, vars, root, settings.depth); let c = match r { CellResult::Done(cell) => Some(cell), CellResult::Recurse(eval) => { @@ -168,7 +172,9 @@ impl OctreeWorker { let out: Vec = std::thread::scope(|s| { let mut handles = vec![]; for w in workers { - handles.push(s.spawn(move || w.run(pool, settings.depth))); + handles.push( + s.spawn(move || w.run(vars, pool, settings.depth)), + ); } handles.into_iter().map(|h| h.join().unwrap()).collect() }); @@ -177,7 +183,12 @@ impl OctreeWorker { } /// Runs a single worker to completion as part of a worker group - pub fn run(mut self, threads: &ThreadPool, max_depth: u8) -> Octree { + pub fn run( + mut self, + vars: &ShapeVars, + 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 @@ -212,7 +223,9 @@ 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, max_depth) + match self + .octree + .eval_cell(&task.eval, vars, 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/octree.rs b/fidget/src/mesh/octree.rs index 1ec4961d..70815bcd 100644 --- a/fidget/src/mesh/octree.rs +++ b/fidget/src/mesh/octree.rs @@ -13,7 +13,7 @@ use super::{ use crate::{ eval::{BulkEvaluator, Function, TracingEvaluator}, render::{RenderHints, ThreadCount}, - shape::{Shape, ShapeBulkEval, ShapeTape, ShapeTracingEval}, + shape::{Shape, ShapeBulkEval, ShapeTape, ShapeTracingEval, ShapeVars}, types::Grad, }; use std::{num::NonZeroUsize, sync::Arc, sync::OnceLock}; @@ -93,20 +93,21 @@ pub struct Octree { } impl Octree { - /// Builds an octree to the given depth + /// Builds an octree to the given depth, with user-provided variables /// /// The shape is evaluated on the region specified by `settings.bounds`. - pub fn build( + pub fn build_with_vars( shape: &Shape, + vars: &ShapeVars, settings: Settings, ) -> Self { // Transform the shape given our world-to-model matrix let t = settings.view.world_to_model(); if t == nalgebra::Matrix4::identity() { - Self::build_inner(shape, settings) + Self::build_inner(shape, vars, settings) } else { let shape = shape.clone().apply_transform(t); - let mut out = Self::build_inner(&shape, settings); + let mut out = Self::build_inner(&shape, vars, settings); // Apply the transform from [-1, +1] back to model space for v in &mut out.verts { @@ -118,8 +119,23 @@ impl Octree { } } + /// Builds an octree to the given depth + /// + /// If the shape uses variables other than `x`, `y`, `z`, then + /// [`build_with_vars`](Octree::build_with_vars) should be used instead 9and + /// this function will return an error). + /// + /// The shape is evaluated on the region specified by `settings.bounds`. + pub fn build( + shape: &Shape, + settings: Settings, + ) -> Self { + Self::build_with_vars(shape, &ShapeVars::new(), settings) + } + fn build_inner( shape: &Shape, + vars: &ShapeVars, settings: Settings, ) -> Self { let eval = Arc::new(EvalGroup::new(shape.clone())); @@ -127,13 +143,14 @@ impl Octree { match settings.threads { ThreadCount::One => { let mut out = OctreeBuilder::new(); - out.recurse(&eval, CellIndex::default(), settings.depth); + out.recurse(&eval, vars, CellIndex::default(), settings.depth); out.into() } #[cfg(not(target_arch = "wasm32"))] ThreadCount::Many(threads) => OctreeWorker::scheduler( eval.clone(), + vars, MultithreadedSettings { depth: settings.depth, threads, @@ -353,16 +370,18 @@ impl OctreeBuilder { pub(crate) fn eval_cell( &mut self, eval: &Arc>, + vars: &ShapeVars, cell: CellIndex, max_depth: u8, ) -> CellResult { let (i, r) = self .eval_interval - .eval( + .eval_v( eval.interval_tape(&mut self.tape_storage), cell.bounds.x, cell.bounds.y, cell.bounds.z, + vars, ) .unwrap(); if i.upper() < 0.0 { @@ -382,7 +401,7 @@ impl OctreeBuilder { }; if cell.depth == max_depth as usize { let eval = sub_tape.unwrap_or_else(|| eval.clone()); - let out = CellResult::Done(self.leaf(&eval, cell)); + let out = CellResult::Done(self.leaf(&eval, vars, cell)); if let Ok(t) = Arc::try_unwrap(eval) { self.reclaim(t); } @@ -432,10 +451,11 @@ impl OctreeBuilder { fn recurse( &mut self, eval: &Arc>, + vars: &ShapeVars, cell: CellIndex, max_depth: u8, ) { - match self.eval_cell(eval, cell, max_depth) { + match self.eval_cell(eval, vars, cell, max_depth) { CellResult::Done(c) => self.o[cell] = c.into(), CellResult::Recurse(sub_eval) => { let index = self.o.cells.len(); @@ -444,7 +464,7 @@ impl OctreeBuilder { } for i in Corner::iter() { let cell = cell.child(index, i); - self.recurse(&sub_eval, cell, max_depth); + self.recurse(&sub_eval, vars, cell, max_depth); } if let Ok(t) = Arc::try_unwrap(sub_eval) { @@ -473,7 +493,12 @@ impl OctreeBuilder { /// Writes the leaf vertex to `self.o.verts`, hermite data to /// `self.hermite`, and the leaf data to `self.leafs`. Does **not** write /// anything to `self.o.cells`; the cell is returned instead. - fn leaf(&mut self, eval: &EvalGroup, cell: CellIndex) -> Cell { + fn leaf( + &mut self, + eval: &EvalGroup, + vars: &ShapeVars, + cell: CellIndex, + ) -> Cell { let mut xs = [0.0; 8]; let mut ys = [0.0; 8]; let mut zs = [0.0; 8]; @@ -486,7 +511,13 @@ impl OctreeBuilder { let out = self .eval_float_slice - .eval(eval.float_slice_tape(&mut self.tape_storage), &xs, &ys, &zs) + .eval_v( + eval.float_slice_tape(&mut self.tape_storage), + &xs, + &ys, + &zs, + vars, + ) .unwrap(); debug_assert_eq!(out.len(), 8); @@ -587,7 +618,13 @@ impl OctreeBuilder { // Do the actual evaluation let out = self .eval_float_slice - .eval(eval.float_slice_tape(&mut self.tape_storage), xs, ys, zs) + .eval_v( + eval.float_slice_tape(&mut self.tape_storage), + xs, + ys, + zs, + vars, + ) .unwrap(); // Update start and end positions based on evaluation @@ -653,7 +690,13 @@ impl OctreeBuilder { // TODO: special case for cells with multiple gradients ("features") let grads = self .eval_grad_slice - .eval(eval.grad_slice_tape(&mut self.tape_storage), xs, ys, zs) + .eval_v( + eval.grad_slice_tape(&mut self.tape_storage), + xs, + ys, + zs, + vars, + ) .unwrap(); let mut verts: arrayvec::ArrayVec<_, 4> = arrayvec::ArrayVec::new(); @@ -1177,6 +1220,7 @@ mod test { mesh::types::{Edge, X, Y, Z}, render::{ThreadCount, View3}, shape::EzShape, + var::Var, vm::{VmFunction, VmShape}, }; use nalgebra::Vector3; @@ -1545,7 +1589,12 @@ 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.depth); + out.recurse( + &eval, + &ShapeVars::new(), + CellIndex::default(), + settings.depth, + ); out } @@ -1723,4 +1772,37 @@ mod test { assert!(n > 0.2 && n < 0.3, "invalid vertex at {v:?}: {n}"); } } + + #[test] + fn test_mesh_vars() { + let (x, y, z) = Tree::axes(); + let v = Var::new(); + let c = Tree::from(v); + let sphere = (x.square() + y.square() + z.square()).sqrt() - c; + let shape = VmShape::from(sphere); + + for threads in + [ThreadCount::One, ThreadCount::Many(4.try_into().unwrap())] + { + let settings = Settings { + depth: 4, + threads, + view: View3::default(), + }; + + for r in [0.5, 0.75] { + let mut vars = ShapeVars::new(); + vars.insert(v.index().unwrap(), r); + let octree = Octree::build_with_vars(&shape, &vars, settings) + .walk_dual(settings); + for v in octree.vertices.iter() { + let n = v.norm(); + assert!( + n > r - 0.05 && n < r + 0.05, + "invalid vertex at {v:?}: {n} != {r}" + ); + } + } + } + } } diff --git a/fidget/src/render/config.rs b/fidget/src/render/config.rs index 4ad43769..43c52e64 100644 --- a/fidget/src/render/config.rs +++ b/fidget/src/render/config.rs @@ -1,7 +1,7 @@ use crate::{ eval::Function, render::{ImageSize, RenderMode, TileSizes, View2, View3, VoxelSize}, - shape::Shape, + shape::{Shape, ShapeVars}, }; use nalgebra::{Const, Matrix3, Matrix4, OPoint, Point2, Vector2}; use std::sync::atomic::{AtomicUsize, Ordering}; @@ -106,7 +106,16 @@ impl ImageRenderConfig { &self, shape: Shape, ) -> Vec<::Output> { - crate::render::render2d::(shape, self) + self.run_with_vars::(shape, &ShapeVars::new()) + } + + /// Render a shape in 2D using this configuration and variables + pub fn run_with_vars( + &self, + shape: Shape, + vars: &ShapeVars, + ) -> Vec<::Output> { + crate::render::render2d::(shape, vars, self) } /// Returns the combined screen-to-model transform matrix @@ -158,7 +167,16 @@ impl VoxelRenderConfig { &self, shape: Shape, ) -> (Vec, Vec<[u8; 3]>) { - crate::render::render3d::(shape, self) + self.run_with_vars::(shape, &ShapeVars::new()) + } + + /// Render a shape in 2D using this configuration and variables + pub fn run_with_vars( + &self, + shape: Shape, + vars: &ShapeVars, + ) -> (Vec, Vec<[u8; 3]>) { + crate::render::render3d::(shape, vars, self) } /// Returns the combined screen-to-model transform matrix diff --git a/fidget/src/render/render2d.rs b/fidget/src/render/render2d.rs index dbbf938e..04f33086 100644 --- a/fidget/src/render/render2d.rs +++ b/fidget/src/render/render2d.rs @@ -4,7 +4,7 @@ use crate::{ eval::Function, render::config::{ImageRenderConfig, Queue, Tile}, render::ThreadCount, - shape::{Shape, ShapeBulkEval, ShapeTracingEval}, + shape::{Shape, ShapeBulkEval, ShapeTracingEval, ShapeVars}, types::Interval, }; use nalgebra::{Point2, Vector2}; @@ -228,6 +228,7 @@ impl Worker<'_, F, M> { fn render_tile_recurse( &mut self, shape: &mut RenderHandle, + vars: &ShapeVars, depth: usize, tile: Tile<2>, ) { @@ -242,7 +243,7 @@ impl Worker<'_, F, M> { // The shape applies the screen-to-model transform let (i, simplify) = self .eval_interval - .eval(shape.i_tape(&mut self.tape_storage), x, y, z) + .eval_v(shape.i_tape(&mut self.tape_storage), x, y, z, vars) .unwrap(); match M::interval(i, depth) { @@ -308,6 +309,7 @@ impl Worker<'_, F, M> { for i in 0..n { self.render_tile_recurse( sub_tape, + vars, depth + 1, Tile::new( tile.corner + Vector2::new(i, j) * next_tile_size, @@ -316,13 +318,14 @@ impl Worker<'_, F, M> { } } } else { - self.render_tile_pixels(sub_tape, tile_size, tile); + self.render_tile_pixels(sub_tape, vars, tile_size, tile); } } fn render_tile_pixels( &mut self, shape: &mut RenderHandle, + vars: &ShapeVars, tile_size: usize, tile: Tile<2>, ) { @@ -337,11 +340,12 @@ impl Worker<'_, F, M> { let out = self .eval_float_slice - .eval( + .eval_v( shape.f_tape(&mut self.tape_storage), &self.scratch.x, &self.scratch.y, &self.scratch.z, + vars, ) .unwrap(); @@ -363,6 +367,7 @@ impl Worker<'_, F, M> { fn worker( mut shape: RenderHandle, + vars: &ShapeVars, queue: &Queue<2>, config: &ImageRenderConfig, ) -> Vec<(Tile<2>, Vec)> { @@ -379,9 +384,10 @@ fn worker( shape_storage: vec![], workspace: Default::default(), }; + while let Some(tile) = queue.next() { w.image = vec![M::Output::default(); config.tile_sizes[0].pow(2)]; - w.render_tile_recurse(&mut shape, 0, tile); + w.render_tile_recurse(&mut shape, vars, 0, tile); let pixels = std::mem::take(&mut w.image); out.push((tile, pixels)) } @@ -401,6 +407,7 @@ fn worker( /// resulting pixels). pub fn render( shape: Shape, + vars: &ShapeVars, config: &ImageRenderConfig, ) -> Vec { // Convert to a 4x4 matrix and apply to the shape @@ -409,11 +416,12 @@ pub fn render( let mat = mat.insert_column(2, 0.0); let shape = shape.apply_transform(mat); - render_inner::<_, M>(shape, config) + render_inner::<_, M>(shape, vars, config) } fn render_inner( shape: Shape, + vars: &ShapeVars, config: &ImageRenderConfig, ) -> Vec { let mut tiles = vec![]; @@ -435,16 +443,17 @@ fn render_inner( let _ = rh.i_tape(&mut vec![]); // populate i_tape before cloning let out: Vec<_> = match config.threads { - ThreadCount::One => { - worker::(rh, &queue, config).into_iter().collect() - } + ThreadCount::One => worker::(rh, vars, &queue, config) + .into_iter() + .collect(), #[cfg(not(target_arch = "wasm32"))] ThreadCount::Many(v) => std::thread::scope(|s| { let mut handles = vec![]; for _ in 0..v.get() { let rh = rh.clone(); - handles.push(s.spawn(|| worker::(rh, &queue, config))); + handles + .push(s.spawn(|| worker::(rh, vars, &queue, config))); } let mut out = vec![]; for h in handles { @@ -478,6 +487,7 @@ mod test { eval::{Function, MathFunction}, render::{ImageSize, View2}, shape::Shape, + var::Var, vm::{GenericVmFunction, VmFunction}, Context, }; @@ -489,51 +499,41 @@ mod test { "/../models/quarter.vm" )); - fn render_and_compare_with_view( - shape: Shape, - expected: &'static str, + #[derive(Default)] + struct Cfg { + vars: ShapeVars, view: View2, wide: bool, - ) { - let width = if wide { 64 } else { 32 }; - let cfg = ImageRenderConfig { - image_size: ImageSize::new(width, 32), - view, - ..Default::default() - }; - let out = cfg.run::<_, BitRenderMode>(shape); - let mut img_str = String::new(); - for (i, b) in out.iter().enumerate() { - if i % width as usize == 0 { - img_str += "\n "; + } + + impl Cfg { + fn test(&self, shape: Shape, expected: &'static str) { + let width = if self.wide { 64 } else { 32 }; + let cfg = ImageRenderConfig { + image_size: ImageSize::new(width, 32), + view: self.view, + ..Default::default() + }; + let out = cfg.run_with_vars::<_, BitRenderMode>(shape, &self.vars); + let mut img_str = String::new(); + for (i, b) in out.iter().enumerate() { + if i % width as usize == 0 { + img_str += "\n "; + } + img_str.push(if *b { 'X' } else { '.' }); } - img_str.push(if *b { 'X' } else { '.' }); - } - if img_str != expected { - println!("image mismatch detected!"); - println!("Expected:\n{expected}\nGot:\n{img_str}"); - println!("Diff:"); - for (a, b) in img_str.chars().zip(expected.chars()) { - print!("{}", if a != b { '!' } else { a }); + if img_str != expected { + println!("image mismatch detected!"); + println!("Expected:\n{expected}\nGot:\n{img_str}"); + println!("Diff:"); + for (a, b) in img_str.chars().zip(expected.chars()) { + print!("{}", if a != b { '!' } else { a }); + } + panic!("image mismatch"); } - panic!("image mismatch"); } } - fn render_and_compare( - shape: Shape, - expected: &'static str, - ) { - render_and_compare_with_view(shape, expected, View2::default(), false) - } - - fn render_and_compare_wide( - shape: Shape, - expected: &'static str, - ) { - render_and_compare_with_view(shape, expected, View2::default(), true) - } - fn check_hi() { let (ctx, root) = Context::from_text(HI.as_bytes()).unwrap(); let shape = Shape::::new(&ctx, root).unwrap(); @@ -570,7 +570,7 @@ mod test { ................................ ................................ ................................"; - render_and_compare(shape, EXPECTED); + Cfg::default().test(shape, EXPECTED); } fn check_hi_wide() { @@ -609,7 +609,11 @@ mod test { ................................................................ ................................................................ ................................................................"; - render_and_compare_wide(shape, EXPECTED); + Cfg { + wide: true, + ..Default::default() + } + .test(shape, EXPECTED); } fn check_hi_transformed() { @@ -652,7 +656,7 @@ mod test { .XXX...........XXX......XXX..... .XXX...........XXX......XXX..... ................................"; - render_and_compare(shape, EXPECTED); + Cfg::default().test(shape, EXPECTED); } fn check_hi_bounded() { @@ -691,12 +695,13 @@ mod test { .XXX...........XXX......XXX..... .XXX...........XXX......XXX..... ................................"; - render_and_compare_with_view( - shape, - EXPECTED, - View2::from_center_and_scale(nalgebra::Vector2::new(0.5, 0.5), 0.5), - false, - ); + let view = + View2::from_center_and_scale(nalgebra::Vector2::new(0.5, 0.5), 0.5); + Cfg { + view, + ..Default::default() + } + .test(shape, EXPECTED); } fn check_quarter() { @@ -735,86 +740,129 @@ mod test { ................................ ................................ ................................"; - render_and_compare(shape, EXPECTED); - } - - #[test] - fn render_hi_vm() { - check_hi::(); - } - - #[test] - fn render_hi_vm3() { - check_hi::>(); - } - - #[cfg(feature = "jit")] - #[test] - fn render_hi_jit() { - check_hi::(); - } - - #[test] - fn render_hi_wide_vm() { - check_hi_wide::(); - } - - #[test] - fn render_hi_wide_vm3() { - check_hi_wide::>(); - } - - #[cfg(feature = "jit")] - #[test] - fn render_hi_wide_jit() { - check_hi_wide::(); - } - - #[test] - fn render_hi_transformed_vm() { - check_hi_transformed::(); - } - - #[test] - fn render_hi_transformed_vm3() { - check_hi_transformed::>(); - } - - #[cfg(feature = "jit")] - #[test] - fn render_hi_transformed_jit() { - check_hi_transformed::(); - } - - #[test] - fn render_hi_bounded_vm() { - check_hi_bounded::(); - } - - #[test] - fn render_hi_bounded_vm3() { - check_hi_bounded::>(); - } - - #[cfg(feature = "jit")] - #[test] - fn render_hi_bounded_jit() { - check_hi_bounded::(); - } + Cfg::default().test(shape, EXPECTED); + } + + fn check_circle_var() { + let mut ctx = Context::new(); + let x = ctx.x(); + let y = ctx.y(); + let x2 = ctx.square(x).unwrap(); + let y2 = ctx.square(y).unwrap(); + let r2 = ctx.add(x2, y2).unwrap(); + let r = ctx.sqrt(r2).unwrap(); + let v = Var::new(); + let c = ctx.var(v); + let root = ctx.sub(r, c).unwrap(); + let shape = Shape::::new(&ctx, root).unwrap(); + const EXPECTED_075: &str = " + ................................ + ................................ + ................................ + ................................ + ............XXXXXXXXX........... + ..........XXXXXXXXXXXXX......... + .........XXXXXXXXXXXXXXX........ + ........XXXXXXXXXXXXXXXXX....... + .......XXXXXXXXXXXXXXXXXXX...... + ......XXXXXXXXXXXXXXXXXXXXX..... + ......XXXXXXXXXXXXXXXXXXXXX..... + .....XXXXXXXXXXXXXXXXXXXXXXX.... + .....XXXXXXXXXXXXXXXXXXXXXXX.... + .....XXXXXXXXXXXXXXXXXXXXXXX.... + .....XXXXXXXXXXXXXXXXXXXXXXX.... + .....XXXXXXXXXXXXXXXXXXXXXXX.... + .....XXXXXXXXXXXXXXXXXXXXXXX.... + .....XXXXXXXXXXXXXXXXXXXXXXX.... + .....XXXXXXXXXXXXXXXXXXXXXXX.... + .....XXXXXXXXXXXXXXXXXXXXXXX.... + ......XXXXXXXXXXXXXXXXXXXXX..... + ......XXXXXXXXXXXXXXXXXXXXX..... + .......XXXXXXXXXXXXXXXXXXX...... + ........XXXXXXXXXXXXXXXXX....... + .........XXXXXXXXXXXXXXX........ + ..........XXXXXXXXXXXXX......... + ............XXXXXXXXX........... + ................................ + ................................ + ................................ + ................................ + ................................"; + let mut vars = ShapeVars::new(); + vars.insert(v.index().unwrap(), 0.75); + Cfg { + vars, + ..Default::default() + } + .test(shape.clone(), EXPECTED_075); - #[test] - fn render_quarter_vm() { - check_quarter::(); + const EXPECTED_05: &str = " + ................................ + ................................ + ................................ + ................................ + ................................ + ................................ + ................................ + ................................ + .............XXXXXXX............ + ...........XXXXXXXXXXX.......... + ..........XXXXXXXXXXXXX......... + ..........XXXXXXXXXXXXX......... + .........XXXXXXXXXXXXXXX........ + .........XXXXXXXXXXXXXXX........ + .........XXXXXXXXXXXXXXX........ + .........XXXXXXXXXXXXXXX........ + .........XXXXXXXXXXXXXXX........ + .........XXXXXXXXXXXXXXX........ + .........XXXXXXXXXXXXXXX........ + ..........XXXXXXXXXXXXX......... + ..........XXXXXXXXXXXXX......... + ...........XXXXXXXXXXX.......... + .............XXXXXXX............ + ................................ + ................................ + ................................ + ................................ + ................................ + ................................ + ................................ + ................................ + ................................"; + let mut vars = ShapeVars::new(); + vars.insert(v.index().unwrap(), 0.5); + Cfg { + vars, + ..Default::default() + } + .test(shape, EXPECTED_05); } - #[test] - fn render_quarter_vm3() { - check_quarter::>(); + macro_rules! render_tests { + ($i:ident) => { + mod $i { + use super::*; + #[test] + fn vm() { + $i::(); + } + #[test] + fn vm3() { + $i::>(); + } + #[cfg(feature = "jit")] + #[test] + fn jit() { + $i::<$crate::jit::JitFunction>(); + } + } + }; } - #[cfg(feature = "jit")] - #[test] - fn render_quarter_jit() { - check_quarter::(); - } + render_tests!(check_hi); + render_tests!(check_hi_wide); + render_tests!(check_hi_transformed); + render_tests!(check_hi_bounded); + render_tests!(check_quarter); + render_tests!(check_circle_var); } diff --git a/fidget/src/render/render3d.rs b/fidget/src/render/render3d.rs index 01160c61..d9029e9e 100644 --- a/fidget/src/render/render3d.rs +++ b/fidget/src/render/render3d.rs @@ -3,7 +3,7 @@ use super::RenderHandle; use crate::{ eval::Function, render::config::{Queue, ThreadCount, Tile, VoxelRenderConfig}, - shape::{Shape, ShapeBulkEval, ShapeTracingEval}, + shape::{Shape, ShapeBulkEval, ShapeTracingEval, ShapeVars}, types::{Grad, Interval}, }; @@ -29,6 +29,7 @@ impl Scratch { fn new(tile_size: usize) -> Self { let size2 = tile_size.pow(2); let size3 = tile_size.pow(3); + Self { x: vec![0.0; size3], y: vec![0.0; size3], @@ -68,6 +69,7 @@ impl Worker<'_, F> { fn render_tile_recurse( &mut self, shape: &mut RenderHandle, + vars: &ShapeVars, depth: usize, tile: Tile<3>, ) { @@ -88,7 +90,7 @@ impl Worker<'_, F> { let (i, trace) = self .eval_interval - .eval(shape.i_tape(&mut self.tape_storage), x, y, z) + .eval_v(shape.i_tape(&mut self.tape_storage), x, y, z, vars) .unwrap(); // Return early if this tile is completely empty or full, returning @@ -126,6 +128,7 @@ impl Worker<'_, F> { for k in (0..n).rev() { self.render_tile_recurse( sub_tape, + vars, depth + 1, Tile::new( tile.corner @@ -136,7 +139,7 @@ impl Worker<'_, F> { } } } else { - self.render_tile_pixels(sub_tape, tile_size, tile); + self.render_tile_pixels(sub_tape, vars, tile_size, tile); }; // TODO recycle something here? } @@ -144,6 +147,7 @@ impl Worker<'_, F> { fn render_tile_pixels( &mut self, shape: &mut RenderHandle, + vars: &ShapeVars, tile_size: usize, tile: Tile<3>, ) { @@ -193,11 +197,12 @@ impl Worker<'_, F> { let out = self .eval_float_slice - .eval( + .eval_v( shape.f_tape(&mut self.tape_storage), &self.scratch.x[..index], &self.scratch.y[..index], &self.scratch.z[..index], + vars, ) .unwrap(); @@ -254,11 +259,12 @@ impl Worker<'_, F> { if grad > 0 { let out = self .eval_grad_slice - .eval( + .eval_v( shape.g_tape(&mut self.tape_storage), &self.scratch.xg[..grad], &self.scratch.yg[..grad], &self.scratch.zg[..grad], + vars, ) .unwrap(); @@ -290,6 +296,7 @@ impl Image { fn worker( mut shape: RenderHandle, + vars: &ShapeVars, queues: &[Queue<3>], mut index: usize, config: &VoxelRenderConfig, @@ -328,7 +335,7 @@ fn worker( // Prepare to render, allocating space for a tile w.depth = image.depth; w.color = image.color; - w.render_tile_recurse(&mut shape, 0, tile); + w.render_tile_recurse(&mut shape, vars, 0, tile); // Steal the tile, replacing it with an empty vec let depth = std::mem::take(&mut w.depth); @@ -360,16 +367,11 @@ fn worker( /// perform evaluation. pub fn render( shape: Shape, + vars: &ShapeVars, config: &VoxelRenderConfig, ) -> (Vec, Vec<[u8; 3]>) { let shape = shape.apply_transform(config.mat()); - render_inner(shape, config) -} -pub fn render_inner( - shape: Shape, - config: &VoxelRenderConfig, -) -> (Vec, Vec<[u8; 3]>) { let mut tiles = vec![]; let t = config.tile_sizes[0]; let width = config.image_size[0] as usize; @@ -400,9 +402,11 @@ pub fn render_inner( // Special-case for single-threaded operation, to give simpler backtraces let out: Vec<_> = match config.threads { - ThreadCount::One => worker::(rh, tile_queues.as_slice(), 0, config) - .into_iter() - .collect(), + ThreadCount::One => { + worker::(rh, vars, tile_queues.as_slice(), 0, config) + .into_iter() + .collect() + } #[cfg(not(target_arch = "wasm32"))] ThreadCount::Many(threads) => std::thread::scope(|s| { @@ -411,8 +415,9 @@ pub fn render_inner( let queues = tile_queues.as_slice(); for i in 0..threads.get() { let rh = rh.clone(); - handles - .push(s.spawn(move || worker::(rh, queues, i, config))); + handles.push( + s.spawn(move || worker::(rh, vars, queues, i, config)), + ); } let mut out = vec![]; for h in handles { @@ -447,7 +452,10 @@ pub fn render_inner( #[cfg(test)] mod test { use super::*; - use crate::{render::VoxelSize, vm::VmShape, Context}; + use crate::{ + context::Tree, eval::MathFunction, render::VoxelSize, var::Var, + vm::VmShape, Context, + }; /// Make sure we don't crash if there's only a single tile #[test] @@ -464,4 +472,72 @@ mod test { assert_eq!(depth.len(), 128 * 128); assert_eq!(rgb.len(), 128 * 128); } + + fn sphere_var() { + let (x, y, z) = Tree::axes(); + let v = Var::new(); + let c = Tree::from(v); + let sphere = (x.square() + y.square() + z.square()).sqrt() - c; + let shape = Shape::::from(sphere); + + let size = 32; + let cfg = VoxelRenderConfig { + image_size: VoxelSize::from(size), + ..Default::default() + }; + + for r in [0.5, 0.75] { + let mut vars = ShapeVars::new(); + vars.insert(v.index().unwrap(), r); + let (depth, _normal) = cfg.run_with_vars::<_>(shape.clone(), &vars); + + let epsilon = 0.08; + for (i, p) in depth.iter().enumerate() { + let size = size as i32; + let i = i as i32; + let x = (((i % size) - size / 2) as f32 / size as f32) * 2.0; + let y = (((i / size) - size / 2) as f32 / size as f32) * 2.0; + let z = (*p as i32 - size / 2) as f32 / size as f32 * 2.0; + if *p == 0 { + let v = (x.powi(2) + y.powi(2)).sqrt(); + assert!( + v + epsilon > r, + "got z = 0 inside the sphere ({x}, {y}, {z}); \ + radius is {v}" + ); + } else { + let v = (x.powi(2) + y.powi(2) + z.powi(2)).sqrt(); + let err = (r - v).abs(); + assert!( + err < epsilon, + "too much error {err} at ({x}, {y}, {z}); \ + radius is {v}, expected {r}" + ); + } + } + } + } + + macro_rules! render_tests { + ($i:ident) => { + mod $i { + use super::*; + #[test] + fn vm() { + $i::<$crate::vm::VmFunction>(); + } + #[test] + fn vm3() { + $i::<$crate::vm::GenericVmFunction<3>>(); + } + #[cfg(feature = "jit")] + #[test] + fn jit() { + $i::<$crate::jit::JitFunction>(); + } + } + }; + } + + render_tests!(sphere_var); }