Skip to main content

fidget_raster/
render2d.rs

1//! 2D bitmap rendering / rasterization
2use super::RenderHandle;
3use crate::{
4    Image, RenderConfig, RenderWorker, TileSizesRef,
5    config::{ImageRenderConfig, Tile},
6};
7use fidget_core::{
8    eval::Function,
9    shape::{Shape, ShapeBulkEval, ShapeTracingEval, ShapeVars},
10    types::Interval,
11};
12use nalgebra::{Point2, Vector2};
13
14////////////////////////////////////////////////////////////////////////////////
15
16struct Scratch {
17    x: Vec<f32>,
18    y: Vec<f32>,
19    z: Vec<f32>,
20}
21
22impl Scratch {
23    fn new(size: usize) -> Self {
24        Self {
25            x: vec![0.0; size],
26            y: vec![0.0; size],
27            z: vec![0.0; size],
28        }
29    }
30}
31
32/// A pixel in a 2D image
33///
34/// This is either a single distance value or a description of a fill; both
35/// cases are packed into an `f32` (using `NaN`-boxing in the latter case).
36#[derive(Copy, Clone, Debug, Default)]
37pub struct DistancePixel(f32);
38
39#[derive(Copy, Clone, Debug)]
40pub struct PixelFill {
41    pub depth: u8,
42    pub inside: bool,
43}
44
45impl DistancePixel {
46    const KEY: u32 = 0b1111_0110 << 9;
47    const KEY_MASK: u32 = 0b1111_1111 << 9;
48
49    /// Checks whether the pixel is inside or outside the model
50    ///
51    /// This will return `false` if the distance value is `NAN`
52    #[inline]
53    pub fn inside(self) -> bool {
54        match self.fill() {
55            Ok(f) => f.inside,
56            Err(v) => v < 0.0,
57        }
58    }
59
60    /// Checks whether this is a distance point sample
61    #[inline]
62    pub fn is_distance(self) -> bool {
63        if !self.0.is_nan() {
64            return true;
65        }
66        let bits = self.0.to_bits();
67        (bits & Self::KEY_MASK) != Self::KEY
68    }
69
70    /// Returns the distance value (if present)
71    #[inline]
72    pub fn distance(self) -> Result<f32, PixelFill> {
73        match self.fill() {
74            Ok(v) => Err(v),
75            Err(v) => Ok(v),
76        }
77    }
78
79    /// Returns the fill details (if present)
80    #[inline]
81    pub fn fill(self) -> Result<PixelFill, f32> {
82        if self.is_distance() {
83            Err(self.0)
84        } else {
85            let bits = self.0.to_bits();
86            let inside = (bits & 1) == 1;
87            let depth = (bits >> 1) as u8;
88            Ok(PixelFill { inside, depth })
89        }
90    }
91}
92
93impl From<PixelFill> for DistancePixel {
94    #[inline]
95    fn from(p: PixelFill) -> Self {
96        let bits = 0x7FC00000
97            | (u32::from(p.depth) << 1)
98            | u32::from(p.inside)
99            | Self::KEY;
100        DistancePixel(f32::from_bits(bits))
101    }
102}
103
104impl From<f32> for DistancePixel {
105    #[inline]
106    fn from(p: f32) -> Self {
107        // Canonicalize the NAN value to avoid colliding with a fill
108        Self(if p.is_nan() { f32::NAN } else { p })
109    }
110}
111
112////////////////////////////////////////////////////////////////////////////////
113
114/// Per-thread worker
115struct Worker<'a, F: Function> {
116    tile_sizes: TileSizesRef<'a>,
117    pixel_perfect: bool,
118    scratch: Scratch,
119
120    eval_float_slice: ShapeBulkEval<F::FloatSliceEval>,
121    eval_interval: ShapeTracingEval<F::IntervalEval>,
122
123    /// Spare tape storage for reuse
124    tape_storage: Vec<F::TapeStorage>,
125
126    /// Spare shape storage for reuse
127    shape_storage: Vec<F::Storage>,
128
129    /// Workspace for shape simplification
130    workspace: F::Workspace,
131
132    /// Tile being rendered
133    ///
134    /// This is a root tile, i.e. width and height of `config.tile_sizes[0]`
135    image: Image<DistancePixel>,
136}
137
138impl<'a, F: Function, T> RenderWorker<'a, F, T> for Worker<'a, F> {
139    type Config = ImageRenderConfig<'a>;
140    type Output = Image<DistancePixel>;
141    fn new(cfg: &'a Self::Config) -> Self {
142        let tile_sizes = cfg.tile_sizes();
143        Worker::<F> {
144            scratch: Scratch::new(tile_sizes.last().pow(2)),
145            pixel_perfect: cfg.pixel_perfect,
146            image: Default::default(),
147            tile_sizes,
148            eval_float_slice: Default::default(),
149            eval_interval: Default::default(),
150            tape_storage: vec![],
151            shape_storage: vec![],
152            workspace: Default::default(),
153        }
154    }
155
156    fn render_tile(
157        &mut self,
158        shape: &mut RenderHandle<F, T>,
159        vars: &ShapeVars<f32>,
160        tile: super::config::Tile<2>,
161    ) -> Self::Output {
162        self.image = Image::new((self.tile_sizes[0] as u32).into());
163        self.render_tile_recurse(shape, vars, 0, tile);
164        std::mem::take(&mut self.image)
165    }
166}
167
168impl<F: Function> Worker<'_, F> {
169    fn render_tile_recurse<T>(
170        &mut self,
171        shape: &mut RenderHandle<F, T>,
172        vars: &ShapeVars<f32>,
173        depth: usize,
174        tile: Tile<2>,
175    ) {
176        let tile_size = self.tile_sizes[depth];
177
178        // Find the interval bounds of the region, in screen coordinates
179        let base = Point2::from(tile.corner).cast::<f32>();
180        let x = Interval::new(base.x, base.x + tile_size as f32);
181        let y = Interval::new(base.y, base.y + tile_size as f32);
182        let z = Interval::new(0.0, 0.0);
183
184        // The shape applies the screen-to-model transform
185        let (i, simplify) = self
186            .eval_interval
187            .eval_v(shape.i_tape(&mut self.tape_storage), x, y, z, vars)
188            .unwrap();
189
190        if !self.pixel_perfect {
191            let pixel = if i.upper() < 0.0 {
192                Some(PixelFill {
193                    inside: true,
194                    depth: depth as u8,
195                })
196            } else if i.lower() > 0.0 {
197                Some(PixelFill {
198                    inside: false,
199                    depth: depth as u8,
200                })
201            } else {
202                None
203            };
204            if let Some(pixel) = pixel {
205                let fill = pixel.into();
206                for y in 0..tile_size {
207                    let start = self
208                        .tile_sizes
209                        .pixel_offset(tile.add(Vector2::new(0, y)));
210                    self.image[start..][..tile_size].fill(fill);
211                }
212                return;
213            }
214        }
215
216        let sub_tape = if let Some(trace) = simplify.as_ref() {
217            shape.simplify(
218                trace,
219                &mut self.workspace,
220                &mut self.shape_storage,
221                &mut self.tape_storage,
222            )
223        } else {
224            shape
225        };
226
227        if let Some(next_tile_size) = self.tile_sizes.get(depth + 1) {
228            let n = tile_size / next_tile_size;
229            for j in 0..n {
230                for i in 0..n {
231                    self.render_tile_recurse(
232                        sub_tape,
233                        vars,
234                        depth + 1,
235                        Tile::new(
236                            tile.corner + Vector2::new(i, j) * next_tile_size,
237                        ),
238                    );
239                }
240            }
241        } else {
242            self.render_tile_pixels(sub_tape, vars, tile_size, tile);
243        }
244    }
245
246    fn render_tile_pixels<T>(
247        &mut self,
248        shape: &mut RenderHandle<F, T>,
249        vars: &ShapeVars<f32>,
250        tile_size: usize,
251        tile: Tile<2>,
252    ) {
253        let mut index = 0;
254        for j in 0..tile_size {
255            for i in 0..tile_size {
256                self.scratch.x[index] = (tile.corner[0] + i) as f32;
257                self.scratch.y[index] = (tile.corner[1] + j) as f32;
258                index += 1;
259            }
260        }
261
262        let out = self
263            .eval_float_slice
264            .eval_v(
265                shape.f_tape(&mut self.tape_storage),
266                &self.scratch.x,
267                &self.scratch.y,
268                &self.scratch.z,
269                vars,
270            )
271            .unwrap();
272
273        let mut index = 0;
274        for j in 0..tile_size {
275            let o = self.tile_sizes.pixel_offset(tile.add(Vector2::new(0, j)));
276            for i in 0..tile_size {
277                self.image[o + i] = out[index].into();
278                index += 1;
279            }
280        }
281    }
282}
283
284////////////////////////////////////////////////////////////////////////////////
285
286/// Renders the given tape into a 2D image at Z = 0 according to the provided
287/// configuration.
288///
289/// The tape provides the shape; the configuration supplies resolution,
290/// transforms, etc.
291///
292/// This function is parameterized by shape type (which determines how we
293/// perform evaluation).
294///
295/// Returns an `Image<DistancePixel>` of pixel data if rendering succeeds, or
296/// `None` if rendering was cancelled (using the [`ImageRenderConfig::cancel`]
297/// token)
298pub fn render<F: Function>(
299    shape: Shape<F>,
300    vars: &ShapeVars<f32>,
301    config: &ImageRenderConfig,
302) -> Option<Image<DistancePixel>> {
303    // Convert to a 4x4 matrix and apply to the shape
304    let mat = config.mat();
305    let mat = mat.insert_row(2, 0.0);
306    let mat = mat.insert_column(2, 0.0);
307    let shape = shape.with_transform(mat);
308
309    let tiles =
310        super::render_tiles::<F, Worker<F>, _>(shape.clone(), vars, config)?;
311    let tile_sizes = config.tile_sizes();
312
313    let width = config.image_size.width() as usize;
314    let height = config.image_size.height() as usize;
315    let mut image = Image::new(config.image_size);
316    for (tile, data) in tiles.iter() {
317        let mut index = 0;
318        for j in 0..tile_sizes[0] {
319            let y = j + tile.corner.y;
320            for i in 0..tile_sizes[0] {
321                let x = i + tile.corner.x;
322                if y < height && x < width {
323                    image[(y, x)] = data[index];
324                }
325                index += 1;
326            }
327        }
328    }
329    Some(image)
330}
331
332#[cfg(test)]
333mod test {
334    use super::*;
335    use fidget_core::{
336        Context, render::ImageSize, shape::Shape, vm::VmFunction,
337    };
338
339    const HI: &str =
340        include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/../models/hi.vm"));
341
342    #[test]
343    fn render2d_cancel() {
344        let (ctx, root) = Context::from_text(HI.as_bytes()).unwrap();
345        let shape = Shape::<VmFunction>::new(&ctx, root).unwrap();
346
347        let cfg = ImageRenderConfig {
348            image_size: ImageSize::new(64, 64),
349            ..Default::default()
350        };
351        let cancel = cfg.cancel.clone();
352        cancel.cancel();
353        let out = cfg.run(shape);
354        assert!(out.is_none());
355    }
356}