From c98c044cb9cee863f5ab61f0a10833f5be629583 Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Sun, 17 Nov 2024 09:34:27 -0500 Subject: [PATCH] Add vars and test to meshing --- CHANGELOG.md | 8 +++ fidget/src/core/shape/mod.rs | 4 +- fidget/src/mesh/mt/octree.rs | 21 +++++-- fidget/src/mesh/octree.rs | 106 +++++++++++++++++++++++++++++----- fidget/src/render/render3d.rs | 20 ++----- 5 files changed, 125 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b4f11b8..56407db8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,14 @@ - 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 variables are contant). 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/shape/mod.rs b/fidget/src/core/shape/mod.rs index a1642713..004eeb79 100644 --- a/fidget/src/core/shape/mod.rs +++ b/fidget/src/core/shape/mod.rs @@ -502,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( 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..78d71398 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}; @@ -96,17 +96,18 @@ impl Octree { /// Builds an octree to the given depth /// /// 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,19 @@ impl Octree { } } + /// Builds an octree to the given depth + /// + /// The shape is evaluated on the region specified by `settings.bounds`. + pub fn build( + shape: &Shape, + settings: Settings, + ) -> Self { + Self::build_with_vars(shape, &Default::default(), settings) + } + fn build_inner( shape: &Shape, + vars: &ShapeVars, settings: Settings, ) -> Self { let eval = Arc::new(EvalGroup::new(shape.clone())); @@ -127,13 +139,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 +366,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 +397,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 +447,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 +460,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 +489,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 +507,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 +614,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 +686,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 +1216,7 @@ mod test { mesh::types::{Edge, X, Y, Z}, render::{ThreadCount, View3}, shape::EzShape, + var::Var, vm::{VmFunction, VmShape}, }; use nalgebra::Vector3; @@ -1545,7 +1585,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, + &Default::default(), + CellIndex::default(), + settings.depth, + ); out } @@ -1723,4 +1768,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/render3d.rs b/fidget/src/render/render3d.rs index f0e9535d..d9029e9e 100644 --- a/fidget/src/render/render3d.rs +++ b/fidget/src/render/render3d.rs @@ -453,7 +453,8 @@ pub fn render( mod test { use super::*; use crate::{ - eval::MathFunction, render::VoxelSize, var::Var, vm::VmShape, Context, + context::Tree, eval::MathFunction, render::VoxelSize, var::Var, + vm::VmShape, Context, }; /// Make sure we don't crash if there's only a single tile @@ -473,20 +474,11 @@ mod test { } fn sphere_var() { - let mut ctx = Context::new(); - let x = ctx.x(); - let y = ctx.y(); - let z = ctx.z(); - let x2 = ctx.square(x).unwrap(); - let y2 = ctx.square(y).unwrap(); - let z2 = ctx.square(z).unwrap(); - let x2y2 = ctx.add(x2, y2).unwrap(); - let r2 = ctx.add(x2y2, z2).unwrap(); - let r = ctx.sqrt(r2).unwrap(); + let (x, y, z) = Tree::axes(); let v = Var::new(); - let c = ctx.var(v); - let root = ctx.sub(r, c).unwrap(); - let shape = Shape::::new(&ctx, root).unwrap(); + 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 {