From f15e65dafce84b6dff4202851a3f9e224686be56 Mon Sep 17 00:00:00 2001 From: Matt Keeter Date: Sun, 12 May 2024 14:10:52 -0400 Subject: [PATCH] Support interpolation for 2D rendering modes (#102) Changed to 2D rendering API to support render modes which use linear interpolation to process full / empty regions. - Specifically, `RenderMode::interval` now returns an `IntervalAction`, which can be `Fill(..)`, `Recurse`, or `Interpolate` - Modify `SdfRenderMode` use this interpolation; the previous pixel-perfect behavior is renamed to `SdfPixelRenderModel` - Make `RenderMode` trait methods static, because they weren't using `&self` - Change signature of `fidget::render::render2d` to pass the mode only as a generic parameter, instead of an argument --- CHANGELOG.md | 9 +++ demo/src/main.rs | 18 ++--- fidget/benches/render.rs | 36 ++++----- fidget/src/lib.rs | 2 +- fidget/src/render/config.rs | 3 +- fidget/src/render/render2d.rs | 138 +++++++++++++++++++++++----------- viewer/src/main.rs | 27 +++---- wasm-demo/Cargo.lock | 2 +- wasm-demo/src/lib.rs | 2 +- 9 files changed, 143 insertions(+), 94 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 601849dd..1baa17a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ # 0.2.7 (in progress) +- Changed to 2D rendering API to support render modes which use linear + interpolation to process full / empty regions + - Specifically, `RenderMode::interval` now returns an `IntervalAction`, + which can be `Fill(..)`, `Recurse`, or `Interpolate`. + - Modify `SdfRenderMode` use this interpolation; the previous pixel-perfect + behavior is renamed to `SdfPixelRenderModel` + - Make `RenderMode` trait methods static, because they weren't using `&self` + - Change signature of `fidget::render::render2d` to pass the mode only as a + generic parameter, instead of an argument # 0.2.6 This is a relatively small release; there are a few features to improve the diff --git a/demo/src/main.rs b/demo/src/main.rs index 368588cf..8c552b32 100644 --- a/demo/src/main.rs +++ b/demo/src/main.rs @@ -209,11 +209,10 @@ fn run2d( if sdf { let mut image = vec![]; for _ in 0..settings.n { - image = fidget::render::render2d( - shape.clone(), - &cfg, - &fidget::render::SdfRenderMode, - ); + image = fidget::render::render2d::< + _, + fidget::render::SdfRenderMode, + >(shape.clone(), &cfg); } image .into_iter() @@ -222,11 +221,10 @@ fn run2d( } else { let mut image = vec![]; for _ in 0..settings.n { - image = fidget::render::render2d( - shape.clone(), - &cfg, - &fidget::render::DebugRenderMode, - ); + image = fidget::render::render2d::< + _, + fidget::render::DebugRenderMode, + >(shape.clone(), &cfg); } image .into_iter() diff --git a/fidget/benches/render.rs b/fidget/benches/render.rs index 4a926e28..a0e2cdce 100644 --- a/fidget/benches/render.rs +++ b/fidget/benches/render.rs @@ -24,11 +24,10 @@ pub fn prospero_size_sweep(c: &mut Criterion) { group.bench_function(BenchmarkId::new("vm", size), move |b| { b.iter(|| { let tape = shape_vm.clone(); - black_box(fidget::render::render2d( - tape, - cfg, - &fidget::render::BitRenderMode, - )) + black_box(fidget::render::render2d::< + _, + fidget::render::BitRenderMode, + >(tape, cfg)) }) }); @@ -42,11 +41,10 @@ pub fn prospero_size_sweep(c: &mut Criterion) { group.bench_function(BenchmarkId::new("jit", size), move |b| { b.iter(|| { let tape = shape_jit.clone(); - black_box(fidget::render::render2d( - tape, - cfg, - &fidget::render::BitRenderMode, - )) + black_box(fidget::render::render2d::< + _, + fidget::render::BitRenderMode, + >(tape, cfg)) }) }); } @@ -72,11 +70,10 @@ pub fn prospero_thread_sweep(c: &mut Criterion) { group.bench_function(BenchmarkId::new("vm", threads), move |b| { b.iter(|| { let tape = shape_vm.clone(); - black_box(fidget::render::render2d( - tape, - cfg, - &fidget::render::BitRenderMode, - )) + black_box(fidget::render::render2d::< + _, + fidget::render::BitRenderMode, + >(tape, cfg)) }) }); #[cfg(feature = "jit")] @@ -90,11 +87,10 @@ pub fn prospero_thread_sweep(c: &mut Criterion) { group.bench_function(BenchmarkId::new("jit", threads), move |b| { b.iter(|| { let tape = shape_jit.clone(); - black_box(fidget::render::render2d( - tape, - cfg, - &fidget::render::BitRenderMode, - )) + black_box(fidget::render::render2d::< + _, + fidget::render::BitRenderMode, + >(tape, cfg)) }) }); } diff --git a/fidget/src/lib.rs b/fidget/src/lib.rs index d5e6502c..1ecba9ca 100644 --- a/fidget/src/lib.rs +++ b/fidget/src/lib.rs @@ -210,7 +210,7 @@ //! ..RenderConfig::default() //! }; //! let shape = VmShape::from_tree(&tree); -//! let out = cfg.run(shape, &BitRenderMode)?; +//! let out = cfg.run::<_, BitRenderMode>(shape)?; //! let mut iter = out.iter(); //! for y in 0..cfg.image_size { //! for x in 0..cfg.image_size { diff --git a/fidget/src/render/config.rs b/fidget/src/render/config.rs index 250f0c1e..7fe5a938 100644 --- a/fidget/src/render/config.rs +++ b/fidget/src/render/config.rs @@ -210,9 +210,8 @@ impl RenderConfig<2> { pub fn run( &self, shape: S, - mode: &M, ) -> Result::Output>, Error> { - Ok(crate::render::render2d::(shape, self, mode)) + Ok(crate::render::render2d::(shape, self)) } } diff --git a/fidget/src/render/render2d.rs b/fidget/src/render/render2d.rs index 9a55b87f..18050196 100644 --- a/fidget/src/render/render2d.rs +++ b/fidget/src/render/render2d.rs @@ -9,16 +9,23 @@ use nalgebra::Point2; //////////////////////////////////////////////////////////////////////////////// +/// Response type for [`RenderMode::interval`] +pub enum IntervalAction { + Fill(T), + Interpolate, + Recurse, +} + /// Configuration trait for rendering pub trait RenderMode { /// Type of output pixel type Output: Default + Copy + Clone + Send; /// Decide whether to subdivide or fill an interval - fn interval(&self, i: Interval, depth: usize) -> Option; + fn interval(i: Interval, depth: usize) -> IntervalAction; /// Per-pixel drawing - fn pixel(&self, f: f32) -> Self::Output; + fn pixel(f: f32) -> Self::Output; } //////////////////////////////////////////////////////////////////////////////// @@ -28,24 +35,24 @@ pub struct DebugRenderMode; impl RenderMode for DebugRenderMode { type Output = DebugPixel; - fn interval(&self, i: Interval, depth: usize) -> Option { + fn interval(i: Interval, depth: usize) -> IntervalAction { if i.upper() < 0.0 { if depth > 1 { - Some(DebugPixel::FilledSubtile) + IntervalAction::Fill(DebugPixel::FilledSubtile) } else { - Some(DebugPixel::FilledTile) + IntervalAction::Fill(DebugPixel::FilledTile) } } else if i.lower() > 0.0 { if depth > 1 { - Some(DebugPixel::EmptySubtile) + IntervalAction::Fill(DebugPixel::EmptySubtile) } else { - Some(DebugPixel::EmptyTile) + IntervalAction::Fill(DebugPixel::EmptyTile) } } else { - None + IntervalAction::Recurse } } - fn pixel(&self, f: f32) -> DebugPixel { + fn pixel(f: f32) -> DebugPixel { if f < 0.0 { DebugPixel::Filled } else { @@ -101,29 +108,33 @@ pub struct BitRenderMode; impl RenderMode for BitRenderMode { type Output = bool; - fn interval(&self, i: Interval, _depth: usize) -> Option { + fn interval(i: Interval, _depth: usize) -> IntervalAction { if i.upper() < 0.0 { - Some(true) + IntervalAction::Fill(true) } else if i.lower() > 0.0 { - Some(false) + IntervalAction::Fill(false) } else { - None + IntervalAction::Recurse } } - fn pixel(&self, f: f32) -> bool { + fn pixel(f: f32) -> bool { f < 0.0 } } -/// Rendering mode which mimicks many SDF demos on ShaderToy -pub struct SdfRenderMode; +/// Pixel-perfect render mode which mimicks many SDF demos on ShaderToy +/// +/// This mode recurses down to individual pixels, so it doesn't take advantage +/// of skipping empty / full regions; use [`SdfRenderMode`] for a +/// faster-but-approximate visualization. +pub struct SdfPixelRenderMode; -impl RenderMode for SdfRenderMode { +impl RenderMode for SdfPixelRenderMode { type Output = [u8; 3]; - fn interval(&self, _i: Interval, _depth: usize) -> Option<[u8; 3]> { - None // always recurse + fn interval(_i: Interval, _depth: usize) -> IntervalAction<[u8; 3]> { + IntervalAction::Recurse } - fn pixel(&self, f: f32) -> [u8; 3] { + fn pixel(f: f32) -> [u8; 3] { let r = 1.0 - 0.1f32.copysign(f); let g = 1.0 - 0.4f32.copysign(f); let b = 1.0 - 0.7f32.copysign(f); @@ -148,6 +159,26 @@ impl RenderMode for SdfRenderMode { } } +/// Fast rendering mode which mimicks many SDF demos on ShaderToy +/// +/// Unlike [`SdfPixelRenderMode`], this mode uses linear interpolation when +/// evaluating empty or full regions, which is significantly faster. +pub struct SdfRenderMode; + +impl RenderMode for SdfRenderMode { + type Output = [u8; 3]; + fn interval(i: Interval, _depth: usize) -> IntervalAction<[u8; 3]> { + if i.upper() < 0.0 || i.lower() > 0.0 { + IntervalAction::Interpolate + } else { + IntervalAction::Recurse + } + } + fn pixel(f: f32) -> [u8; 3] { + SdfPixelRenderMode::pixel(f) + } +} + //////////////////////////////////////////////////////////////////////////////// struct Scratch { @@ -194,7 +225,6 @@ impl Worker<'_, S, M> { shape: &mut RenderHandle, depth: usize, tile: Tile<2>, - mode: &M, ) { let tile_size = self.config.tile_sizes[depth]; @@ -209,14 +239,43 @@ impl Worker<'_, S, M> { .eval(shape.i_tape(&mut self.tape_storage), x, y, z) .unwrap(); - let fill = mode.interval(i, depth); - - if let Some(fill) = fill { - for y in 0..tile_size { - let start = self.config.tile_to_offset(tile, 0, y); - self.image[start..][..tile_size].fill(fill); + match M::interval(i, depth) { + IntervalAction::Fill(fill) => { + for y in 0..tile_size { + let start = self.config.tile_to_offset(tile, 0, y); + self.image[start..][..tile_size].fill(fill); + } + return; + } + IntervalAction::Interpolate => { + let xs = [x.lower(), x.lower(), x.upper(), x.upper()]; + let ys = [y.lower(), y.upper(), y.lower(), y.upper()]; + let zs = [0.0; 4]; + let vs = self + .eval_float_slice + .eval(shape.f_tape(&mut self.tape_storage), &xs, &ys, &zs) + .unwrap(); + // Bilinear interpolation on a per-pixel basis + for y in 0..tile_size { + // Y interpolation + let y_frac = (y as f32 - 1.0) / (tile_size as f32); + 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); + for x in 0..tile_size { + // X interpolation + let x_frac = (x as f32 - 1.0) / (tile_size as f32); + let v = v0 * (1.0 - x_frac) + v1 * x_frac; + + // Write out the pixel + self.image[i] = M::pixel(v); + i += 1; + } + } + return; } - return; + IntervalAction::Recurse => (), // keep going } let sub_tape = if let Some(trace) = simplify.as_ref() { @@ -241,12 +300,11 @@ impl Worker<'_, S, M> { tile.corner[0] + i * next_tile_size, tile.corner[1] + j * next_tile_size, ]), - mode, ); } } } else { - self.render_tile_pixels(sub_tape, tile_size, tile, mode); + self.render_tile_pixels(sub_tape, tile_size, tile); } } @@ -255,7 +313,6 @@ impl Worker<'_, S, M> { shape: &mut RenderHandle, tile_size: usize, tile: Tile<2>, - mode: &M, ) { let mut index = 0; for j in 0..tile_size { @@ -280,7 +337,7 @@ impl Worker<'_, S, M> { for j in 0..tile_size { let o = self.config.tile_to_offset(tile, 0, j); for i in 0..tile_size { - self.image[o + i] = mode.pixel(out[index]); + self.image[o + i] = M::pixel(out[index]); index += 1; } } @@ -293,7 +350,6 @@ fn worker( mut shape: RenderHandle, queue: &Queue<2>, config: &AlignedRenderConfig<2>, - mode: &M, ) -> Vec<(Tile<2>, Vec)> { let mut out = vec![]; let scratch = Scratch::new(config.tile_sizes.last().unwrap_or(&0).pow(2)); @@ -310,7 +366,7 @@ fn worker( }; 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, mode); + w.render_tile_recurse(&mut shape, 0, tile); let pixels = std::mem::take(&mut w.image); out.push((tile, pixels)) } @@ -331,7 +387,6 @@ fn worker( pub fn render( shape: S, config: &RenderConfig<2>, - mode: &M, ) -> Vec { let (config, mat) = config.align(); assert!(config.image_size % config.tile_sizes[0] == 0); @@ -344,13 +399,12 @@ pub fn render( let mat = mat.insert_column(2, 0.0); let shape = shape.apply_transform(mat); - render_inner(shape, config, mode) + render_inner::<_, M>(shape, config) } fn render_inner( shape: S, config: AlignedRenderConfig<2>, - mode: &M, ) -> Vec { let mut tiles = vec![]; for i in 0..config.image_size / config.tile_sizes[0] { @@ -369,9 +423,7 @@ fn render_inner( let _ = rh.i_tape(&mut vec![]); // populate i_tape before cloning let out: Vec<_> = if threads == 1 { - worker::(rh, &queue, &config, mode) - .into_iter() - .collect() + worker::(rh, &queue, &config).into_iter().collect() } else { #[cfg(target_arch = "wasm32")] unreachable!("multithreaded rendering is not supported on wasm32"); @@ -381,9 +433,7 @@ fn render_inner( let mut handles = vec![]; for _ in 0..threads { let rh = rh.clone(); - handles.push( - s.spawn(|| worker::(rh, &queue, &config, mode)), - ); + handles.push(s.spawn(|| worker::(rh, &queue, &config))); } let mut out = vec![]; for h in handles { @@ -440,7 +490,7 @@ mod test { bounds, ..RenderConfig::default() }; - let out = cfg.run(shape, &BitRenderMode).unwrap(); + let out = cfg.run::<_, BitRenderMode>(shape).unwrap(); let mut img_str = String::new(); for (i, b) in out.iter().enumerate() { if i % 32 == 0 { diff --git a/viewer/src/main.rs b/viewer/src/main.rs index 99c11203..f8e1841b 100644 --- a/viewer/src/main.rs +++ b/viewer/src/main.rs @@ -169,11 +169,10 @@ fn render( match mode { TwoDMode::Color => { - let image = fidget::render::render2d( - shape, - &config, - &fidget::render::BitRenderMode, - ); + let image = fidget::render::render2d::< + _, + fidget::render::BitRenderMode, + >(shape, &config); let c = egui::Color32::from_rgba_unmultiplied( color[0], color[1], @@ -188,22 +187,20 @@ fn render( } TwoDMode::Sdf => { - let image = fidget::render::render2d( - shape, - &config, - &fidget::render::SdfRenderMode, - ); + let image = fidget::render::render2d::< + _, + fidget::render::SdfRenderMode, + >(shape, &config); for (p, i) in pixels.iter_mut().zip(&image) { *p = egui::Color32::from_rgb(i[0], i[1], i[2]); } } TwoDMode::Debug => { - let image = fidget::render::render2d( - shape, - &config, - &fidget::render::DebugRenderMode, - ); + let image = fidget::render::render2d::< + _, + fidget::render::DebugRenderMode, + >(shape, &config); for (p, i) in pixels.iter_mut().zip(&image) { let c = i.as_debug_color(); *p = egui::Color32::from_rgb(c[0], c[1], c[2]); diff --git a/wasm-demo/Cargo.lock b/wasm-demo/Cargo.lock index 62111810..ff34b275 100644 --- a/wasm-demo/Cargo.lock +++ b/wasm-demo/Cargo.lock @@ -255,7 +255,7 @@ dependencies = [ [[package]] name = "fidget" -version = "0.2.6" +version = "0.2.7" dependencies = [ "arrayvec", "bimap", diff --git a/wasm-demo/src/lib.rs b/wasm-demo/src/lib.rs index 41f0b67a..05f61091 100644 --- a/wasm-demo/src/lib.rs +++ b/wasm-demo/src/lib.rs @@ -90,7 +90,7 @@ pub fn render_region( ..RenderConfig::default() }; - let out = cfg.run(shape, &BitRenderMode)?; + let out = cfg.run::<_, BitRenderMode>(shape)?; Ok(out .into_iter() .flat_map(|b| {