Skip to main content

fidget_raster/
pixel.rs

1//! 2D bitmap rendering / rasterization
2use super::RenderHandle;
3use crate::{
4    Image as GenericImage, RenderSize as _, RenderWorker, Tile, TileSizesRef,
5};
6use fidget_core::{
7    eval::Function,
8    render::{CancelToken, RenderHints, ThreadPool, TileSizes},
9    shape::{
10        BoundShape, ShapeBulkEval, ShapeBulkEvalError, ShapeTracingEval,
11        ShapeTracingEvalError, ShapeVars,
12    },
13    types::Interval,
14};
15use nalgebra::{Matrix3, Point2, Vector2};
16
17////////////////////////////////////////////////////////////////////////////////
18
19/// Image type for 2D rendering
20pub type Image = GenericImage<RawDistancePixel>;
21
22/// Size for 2D rendering
23pub type RenderSize = fidget_core::render::ImageSize;
24
25/// Settings for 2D rendering
26pub struct RenderConfig<'a> {
27    /// Render size
28    pub image_size: RenderSize,
29
30    /// World-to-model transform
31    pub world_to_model: Matrix3<f32>,
32
33    /// Render the distance values of individual pixels
34    pub pixel_perfect: bool,
35
36    /// Tile sizes to use during evaluation.
37    ///
38    /// If this is `None`, then evaluation will use
39    /// [`RenderHints::tile_sizes_2d`] to select based on evaluator type.
40    pub tile_sizes: Option<TileSizes>,
41
42    /// Thread pool to use for rendering
43    ///
44    /// If this is `None`, then rendering is done in a single thread; otherwise,
45    /// the provided pool is used.
46    pub threads: Option<&'a ThreadPool>,
47
48    /// Token to cancel rendering
49    pub cancel: CancelToken,
50}
51
52impl crate::RenderConfig for RenderConfig<'_> {
53    fn threads(&self) -> Option<&ThreadPool> {
54        self.threads
55    }
56    fn is_cancelled(&self) -> bool {
57        self.cancel.is_cancelled()
58    }
59}
60
61impl crate::RenderSize for RenderConfig<'_> {
62    fn width(&self) -> u32 {
63        self.image_size.width()
64    }
65    fn height(&self) -> u32 {
66        self.image_size.height()
67    }
68}
69
70impl RenderConfig<'_> {
71    /// Constructs a [`RenderConfig`] with reasonable defaults
72    ///
73    /// This config uses the global thread pool for rendering, has an identity
74    /// transform matrix, uses render hints to choose tile sizes, and is not
75    /// pixel-perfect.
76    ///
77    /// To build a somewhat-customized `RenderConfig`, it's typical to use this
78    /// as the base object, then replace individual fields:
79    ///
80    /// ```
81    /// # use fidget_raster::pixel::RenderConfig;
82    /// let cfg = RenderConfig {
83    ///     pixel_perfect: true, // override field
84    ///     ..RenderConfig::from_size(1024.into()) // base
85    /// };
86    /// ```
87    pub fn from_size(image_size: RenderSize) -> Self {
88        Self {
89            image_size,
90            tile_sizes: None,
91            world_to_model: Matrix3::identity(),
92            pixel_perfect: false,
93            threads: Some(&ThreadPool::Global),
94            cancel: CancelToken::new(),
95        }
96    }
97
98    /// Render a shape in 2D using this configuration
99    ///
100    /// Returns [`Some(Image)`](Image) of pixel data on success, or `None`
101    /// if the render was cancelled.
102    pub fn run<F: Function + RenderHints>(
103        &self,
104        shape: BoundShape<F, f32>,
105    ) -> Option<Image> {
106        render(shape, self)
107    }
108
109    /// Returns the combined screen-to-model transform matrix
110    pub fn mat(&self) -> Matrix3<f32> {
111        self.world_to_model * self.image_size.screen_to_world()
112    }
113}
114
115////////////////////////////////////////////////////////////////////////////////
116
117struct Scratch {
118    x: Vec<f32>,
119    y: Vec<f32>,
120    z: Vec<f32>,
121}
122
123impl Scratch {
124    fn new(size: usize) -> Self {
125        Self {
126            x: vec![0.0; size],
127            y: vec![0.0; size],
128            z: vec![0.0; size],
129        }
130    }
131}
132
133/// A raw pixel in a 2D image
134///
135/// This is either a single distance value or a description of a fill; both
136/// cases are packed into an `f32` (using `NaN`-boxing in the latter case).
137///
138/// The `NaN`-boxing is transparent to the user; for the curious, it is
139/// implemented by splitting the mantissa into three components:
140///
141/// - Bit 0 indicates the sign of the fill (1 for inside, 0 for outside)
142/// - Bits 1-9 indicate the depth at which evaluation terminated, which is
143///   useful for certain debug visualizations
144/// - Bits 10-17 are a fixed bit pattern, which both ensures that the value is
145///   treated as `NaN` in the `[empty, depth 0]` case (rather than infinity) and
146///   distinguishes from `NaN` values generated during normal evaluation
147#[derive(Copy, Clone, Debug, Default)]
148pub struct RawDistancePixel(f32);
149
150/// Unpacked data from a [`RawDistancePixel`]
151#[derive(Copy, Clone, Debug)]
152pub enum DistancePixel {
153    /// The pixel represents a pseudo-distance value
154    Value(f32),
155    /// The pixel is from a fill region and no distance is available
156    Fill {
157        /// Recursion depth which wrote the pixel
158        depth: u8,
159        /// Is the pixel inside or outside the image
160        inside: bool,
161    },
162}
163
164impl RawDistancePixel {
165    const KEY: u32 = 0b1111_0110 << 9;
166    const KEY_MASK: u32 = 0b1111_1111 << 9;
167
168    /// Checks whether the pixel is inside or outside the model
169    ///
170    /// Distances of exactly `0.0` and non-boxed `NaN` values are treated as
171    /// outside (i.e. returning `false`).
172    #[inline]
173    pub fn inside(self) -> bool {
174        match self.unpack() {
175            DistancePixel::Fill { inside, .. } => inside,
176            DistancePixel::Value(v) => v < 0.0,
177        }
178    }
179
180    /// Checks whether this is a distance point sample
181    #[inline]
182    pub fn is_distance(self) -> bool {
183        if !self.0.is_nan() {
184            return true;
185        }
186        let bits = self.0.to_bits();
187        (bits & Self::KEY_MASK) != Self::KEY
188    }
189
190    /// Unpacks into a [`DistancePixel`]
191    #[inline]
192    pub fn unpack(self) -> DistancePixel {
193        if self.is_distance() {
194            DistancePixel::Value(self.0)
195        } else {
196            let bits = self.0.to_bits();
197            let inside = (bits & 1) == 1;
198            let depth = (bits >> 1) as u8;
199            DistancePixel::Fill { inside, depth }
200        }
201    }
202}
203
204impl From<DistancePixel> for RawDistancePixel {
205    #[inline]
206    fn from(p: DistancePixel) -> Self {
207        match p {
208            DistancePixel::Value(f) => Self::from(f),
209            DistancePixel::Fill { depth, inside } => {
210                let bits = 0x7FC00000
211                    | (u32::from(depth) << 1)
212                    | u32::from(inside)
213                    | Self::KEY;
214                Self(f32::from_bits(bits))
215            }
216        }
217    }
218}
219
220impl From<f32> for RawDistancePixel {
221    #[inline]
222    fn from(p: f32) -> Self {
223        // Canonicalize the NAN value to avoid colliding with a fill
224        Self(if p.is_nan() { f32::NAN } else { p })
225    }
226}
227
228////////////////////////////////////////////////////////////////////////////////
229
230/// Per-thread worker
231struct Worker<'a, F: Function> {
232    tile_sizes: TileSizesRef<'a>,
233    vars: &'a ShapeVars<f32>,
234
235    pixel_perfect: bool,
236    scratch: Scratch,
237    transform: nalgebra::Matrix4<f32>,
238
239    eval_float_slice: ShapeBulkEval<F::FloatSliceEval>,
240    eval_interval: ShapeTracingEval<F::IntervalEval>,
241
242    /// Spare tape storage for reuse
243    tape_storage: Vec<F::TapeStorage>,
244
245    /// Spare shape storage for reuse
246    shape_storage: Vec<F::Storage>,
247
248    /// Workspace for shape simplification
249    workspace: F::Workspace,
250
251    /// Tile being rendered
252    ///
253    /// This is a root tile, i.e. width and height of `config.tile_sizes[0]`
254    image: Image,
255}
256
257impl<'a, F: Function> RenderWorker<'a, F> for Worker<'a, F> {
258    type Config = RenderConfig<'a>;
259    type Output = Image;
260    fn new(
261        cfg: &'a Self::Config,
262        tile_sizes: TileSizesRef<'a>,
263        vars: &'a ShapeVars<f32>,
264    ) -> Self {
265        // Convert to a 4x4 matrix and apply to the shape
266        let transform = cfg.mat();
267        let transform = transform.insert_row(2, 0.0);
268        let transform = transform.insert_column(2, 0.0);
269
270        Worker::<F> {
271            tile_sizes,
272            transform,
273            vars,
274
275            scratch: Scratch::new(tile_sizes.last().pow(2)),
276            pixel_perfect: cfg.pixel_perfect,
277            image: Default::default(),
278            eval_float_slice: Default::default(),
279            eval_interval: Default::default(),
280            tape_storage: vec![],
281            shape_storage: vec![],
282            workspace: Default::default(),
283        }
284    }
285
286    fn render_tile(
287        &mut self,
288        shape: &mut RenderHandle<F>,
289        tile: Tile<2>,
290    ) -> Self::Output {
291        self.image = Image::new((self.tile_sizes[0] as u32).into());
292        self.render_tile_recurse(shape, 0, tile);
293        std::mem::take(&mut self.image)
294    }
295}
296
297impl<F: Function> Worker<'_, F> {
298    fn render_tile_recurse(
299        &mut self,
300        shape: &mut RenderHandle<F>,
301        depth: usize,
302        tile: Tile<2>,
303    ) {
304        let tile_size = self.tile_sizes[depth];
305
306        // Find the interval bounds of the region, in screen coordinates
307        let base = Point2::from(tile.corner).cast::<f32>();
308        let x = Interval::new(base.x, base.x + tile_size as f32);
309        let y = Interval::new(base.y, base.y + tile_size as f32);
310        let z = Interval::new(0.0, 0.0);
311
312        // Evaluation applies the world-to-model transform.  We know that vars
313        // are valid because we check them at the top of `render`
314        let (i, simplify) =
315            match self.eval_interval.eval_with_transform_and_vars(
316                shape.i_tape(&mut self.tape_storage),
317                x,
318                y,
319                z,
320                &self.transform,
321                self.vars,
322            ) {
323                Ok(v) => v,
324                Err(ShapeTracingEvalError::MissingVar(..)) => unreachable!(),
325            };
326
327        if !self.pixel_perfect {
328            let pixel = if i.upper() < 0.0 {
329                Some(DistancePixel::Fill {
330                    inside: true,
331                    depth: depth as u8,
332                })
333            } else if i.lower() > 0.0 {
334                Some(DistancePixel::Fill {
335                    inside: false,
336                    depth: depth as u8,
337                })
338            } else {
339                None
340            };
341            if let Some(pixel) = pixel {
342                let fill = pixel.into();
343                for y in 0..tile_size {
344                    let start = self
345                        .tile_sizes
346                        .pixel_offset(tile.add(Vector2::new(0, y)));
347                    self.image[start..][..tile_size].fill(fill);
348                }
349                return;
350            }
351        }
352
353        let sub_tape = if let Some(trace) = simplify.as_ref() {
354            shape.simplify(
355                trace,
356                &mut self.workspace,
357                &mut self.shape_storage,
358                &mut self.tape_storage,
359            )
360        } else {
361            shape
362        };
363
364        if let Some(next_tile_size) = self.tile_sizes.get(depth + 1) {
365            let n = tile_size / next_tile_size;
366            for j in 0..n {
367                for i in 0..n {
368                    self.render_tile_recurse(
369                        sub_tape,
370                        depth + 1,
371                        Tile::new(
372                            tile.corner + Vector2::new(i, j) * next_tile_size,
373                        ),
374                    );
375                }
376            }
377        } else {
378            self.render_tile_pixels(sub_tape, tile_size, tile);
379        }
380    }
381
382    fn render_tile_pixels(
383        &mut self,
384        shape: &mut RenderHandle<F>,
385        tile_size: usize,
386        tile: Tile<2>,
387    ) {
388        let mut index = 0;
389        for j in 0..tile_size {
390            for i in 0..tile_size {
391                self.scratch.x[index] = (tile.corner[0] + i) as f32;
392                self.scratch.y[index] = (tile.corner[1] + j) as f32;
393                index += 1;
394            }
395        }
396
397        let out = match self.eval_float_slice.eval_with_transform_and_vars(
398            shape.f_tape(&mut self.tape_storage),
399            &self.scratch.x,
400            &self.scratch.y,
401            &self.scratch.z,
402            &self.transform,
403            self.vars,
404        ) {
405            Ok(v) => v,
406            // We checked the var map at the beginning of `render`
407            Err(ShapeBulkEvalError::MissingVar(..))
408            // We know that our X/Y/Z slices are all the same length
409            | Err(ShapeBulkEvalError::MismatchedVarSlices { .. })
410                => unreachable!(),
411        };
412
413        let mut index = 0;
414        for j in 0..tile_size {
415            let o = self.tile_sizes.pixel_offset(tile.add(Vector2::new(0, j)));
416            for i in 0..tile_size {
417                self.image[o + i] = out[index].into();
418                index += 1;
419            }
420        }
421    }
422}
423
424////////////////////////////////////////////////////////////////////////////////
425
426/// Renders a shape into a 2D image at Z = 0, with the provided configuration
427///
428/// The shape provides the evaluator backend (`F`) and bound variables; the
429/// configuration supplies resolution, transforms, etc.
430///
431/// Returns [`Some(Image)`](Image) of pixel data if rendering succeeds, or
432/// `None` if rendering was cancelled (using the [`RenderConfig::cancel`].
433pub fn render<F: Function + RenderHints>(
434    b: BoundShape<F, f32>,
435    config: &RenderConfig,
436) -> Option<Image> {
437    let shape = b.shape();
438    let vars = b.vars();
439    let max_size = config.width().max(config.height()) as usize;
440    let default_tile_sizes;
441    let tile_sizes = if let Some(ts) = &config.tile_sizes {
442        TileSizesRef::new(ts, max_size)
443    } else {
444        default_tile_sizes = F::tile_sizes_2d();
445        TileSizesRef::new(&default_tile_sizes, max_size)
446    };
447    let tiles = super::render_tiles::<F, Worker<F>>(
448        shape.clone(),
449        vars,
450        config,
451        tile_sizes,
452    )?;
453
454    let width = config.image_size.width() as usize;
455    let height = config.image_size.height() as usize;
456    let mut image = Image::new(config.image_size);
457    for (tile, data) in tiles.iter() {
458        let mut index = 0;
459        for j in 0..tile_sizes[0] {
460            let y = j + tile.corner.y;
461            for i in 0..tile_sizes[0] {
462                let x = i + tile.corner.x;
463                if y < height && x < width {
464                    image[(y, x)] = data[index];
465                }
466                index += 1;
467            }
468        }
469    }
470    Some(image)
471}
472
473#[cfg(test)]
474mod test {
475    use super::*;
476    use fidget_core::{
477        Context,
478        shape::Shape,
479        var::Var,
480        vm::{VmFunction, VmShape},
481    };
482
483    const HI: &str =
484        include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../models/hi.vm"));
485
486    #[test]
487    fn render2d_cancel() {
488        let (ctx, root) = Context::from_text(HI.as_bytes()).unwrap();
489        let shape = Shape::<VmFunction>::new(&ctx, root)
490            .unwrap()
491            .try_into()
492            .expect("no vars");
493
494        let cfg = RenderConfig::from_size(64.into());
495        let cancel = cfg.cancel.clone();
496        cancel.cancel();
497        assert!(cfg.run(shape).is_none());
498    }
499
500    #[test]
501    fn shape_with_var() {
502        let mut ctx = Context::new();
503        let x = ctx.x();
504        let var = Var::new();
505        let v = ctx.var(var);
506        let s = ctx.sub(x, v).unwrap();
507        let shape = VmShape::new(&ctx, s).unwrap();
508
509        let cfg = RenderConfig::from_size(64.into());
510        let mut vars = ShapeVars::new();
511        let i = var.index().expect("expected Var::V");
512        vars.insert(i, 1.0);
513        cfg.run::<_>(shape.bind(&vars).expect("all vars present"))
514            .expect("not cancelled");
515    }
516
517    #[test]
518    fn test_render_config_transforms() {
519        let config = RenderConfig::from_size(512.into());
520        let mat = config.mat();
521        assert_eq!(
522            mat.transform_point(&Point2::new(0.0, -1.0)),
523            Point2::new(-1.0, 1.0)
524        );
525        assert_eq!(
526            mat.transform_point(&Point2::new(512.0, -1.0)),
527            Point2::new(1.0, 1.0)
528        );
529        assert_eq!(
530            mat.transform_point(&Point2::new(512.0, 511.0)),
531            Point2::new(1.0, -1.0)
532        );
533
534        let config = RenderConfig::from_size(575.into());
535        let mat = config.mat();
536        assert_eq!(
537            mat.transform_point(&Point2::new(0.0, -1.0)),
538            Point2::new(-1.0, 1.0)
539        );
540        assert_eq!(
541            mat.transform_point(&Point2::new(
542                config.image_size.width() as f32,
543                -1.0
544            )),
545            Point2::new(1.0, 1.0)
546        );
547        assert_eq!(
548            mat.transform_point(&Point2::new(
549                config.image_size.width() as f32,
550                config.image_size.height() as f32 - 1.0,
551            )),
552            Point2::new(1.0, -1.0)
553        );
554    }
555}