Skip to main content

fidget_raster/
voxel.rs

1//! 3D 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::{BoundShape, ShapeBulkEval, ShapeTracingEval, ShapeVars},
10    types::{Grad, Interval},
11};
12
13use nalgebra::{Matrix4, Point3, Vector2, Vector3};
14use zerocopy::{FromBytes, Immutable, IntoBytes};
15
16/// Image containing depth and normal at each pixel
17pub type Image = GenericImage<GeometryPixel, RenderSize>;
18
19/// Size type for 3D rendering
20pub type RenderSize = fidget_core::render::VoxelSize;
21
22////////////////////////////////////////////////////////////////////////////////
23
24/// Settings for 3D rendering
25pub struct RenderConfig<'a> {
26    /// Render size
27    ///
28    /// The resulting image will have the given width and height; depth sets the
29    /// number of voxels to evaluate within each pixel of the image (stacked
30    /// into a column going into the screen).
31    pub image_size: RenderSize,
32
33    /// World-to-model transform
34    pub world_to_model: Matrix4<f32>,
35
36    /// Tile sizes to use during evaluation.
37    ///
38    /// If this is `None`, then evaluation will use
39    /// [`RenderHints::tile_sizes_3d`] 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, and uses the render hints to choose tile sizes.
75    ///
76    /// To build a somewhat-customized `RenderConfig`, it's typical to use this
77    /// as the base object, then replace individual fields:
78    ///
79    /// ```
80    /// # use fidget_raster::voxel::RenderConfig;
81    /// let cfg = RenderConfig {
82    ///     threads: None, // override field
83    ///     ..RenderConfig::from_size(1024.into()) // base
84    /// };
85    /// ```
86    pub fn from_size(image_size: RenderSize) -> Self {
87        Self {
88            image_size,
89            tile_sizes: None,
90            world_to_model: Matrix4::identity(),
91            threads: Some(&ThreadPool::Global),
92            cancel: CancelToken::new(),
93        }
94    }
95    /// Render a shape in 3D using this configuration
96    ///
97    /// In the resulting image, saturated pixels (i.e. pixels in the image which
98    /// are fully occupied up to the camera) are represented with `depth =
99    /// self.image_size.depth()` and a normal of `[0, 0, 1]`.
100    pub fn run<F: Function + RenderHints>(
101        &self,
102        shape: BoundShape<F, f32>,
103    ) -> Option<Image> {
104        render(shape, self)
105    }
106
107    /// Returns the combined screen-to-model transform matrix
108    pub fn mat(&self) -> Matrix4<f32> {
109        self.world_to_model * self.image_size.screen_to_world()
110    }
111}
112
113////////////////////////////////////////////////////////////////////////////////
114
115/// Pixel type for a [`voxel::Image`](Image)
116///
117/// This type can be passed directly in a buffer to the GPU.  However, it is
118/// **unwise** to load it as a GPU texture: because it mixes `f32` and `u32`,
119/// there's not a reasonable interpolation mode.  Putting the whole thing into
120/// an `f32x4` texture is **especially unwise**: bitcasting a small `u32` depth
121/// to a `f32` produces a denormalized float, which some GPUs will flush to
122/// `0.0` (surprise!).
123#[repr(C)]
124#[derive(
125    Debug, Default, Copy, Clone, IntoBytes, FromBytes, Immutable, PartialEq,
126)]
127pub struct GeometryPixel {
128    /// Function gradients at this pixel
129    pub normal: [f32; 3],
130    /// Z position of this pixel, in voxel units
131    ///
132    /// The fractional component is always zero. Empty pixels always have a
133    /// depth of 0.
134    pub depth: u32,
135}
136
137impl GeometryPixel {
138    /// Converts the normal into a normalized RGB value
139    pub fn to_color(&self) -> [u8; 3] {
140        let [dx, dy, dz] = self.normal;
141        let s = (dx.powi(2) + dy.powi(2) + dz.powi(2)).sqrt();
142        if s != 0.0 {
143            let scale = u8::MAX as f32 / s;
144            [
145                (dx.abs() * scale) as u8,
146                (dy.abs() * scale) as u8,
147                (dz.abs() * scale) as u8,
148            ]
149        } else {
150            [0; 3]
151        }
152    }
153}
154
155////////////////////////////////////////////////////////////////////////////////
156
157struct Scratch {
158    x: Vec<f32>,
159    y: Vec<f32>,
160    z: Vec<f32>,
161
162    xg: Vec<Grad>,
163    yg: Vec<Grad>,
164    zg: Vec<Grad>,
165
166    /// Depth of each column
167    columns: Vec<usize>,
168}
169
170impl Scratch {
171    fn new(tile_size: usize) -> Self {
172        let size2 = tile_size.pow(2);
173        let size3 = tile_size.pow(3);
174
175        Self {
176            x: vec![0.0; size3],
177            y: vec![0.0; size3],
178            z: vec![0.0; size3],
179
180            xg: vec![Grad::from(0.0); size2],
181            yg: vec![Grad::from(0.0); size2],
182            zg: vec![Grad::from(0.0); size2],
183
184            columns: vec![0; size2],
185        }
186    }
187}
188
189////////////////////////////////////////////////////////////////////////////////
190
191struct Worker<'a, F: Function> {
192    tile_sizes: TileSizesRef<'a>,
193    vars: &'a ShapeVars<f32>,
194
195    transform: nalgebra::Matrix4<f32>,
196    image_size: RenderSize,
197
198    /// Reusable workspace for evaluation, to minimize allocation
199    scratch: Scratch,
200
201    eval_float_slice: ShapeBulkEval<F::FloatSliceEval>,
202    eval_grad_slice: ShapeBulkEval<F::GradSliceEval>,
203    eval_interval: ShapeTracingEval<F::IntervalEval>,
204
205    tape_storage: Vec<F::TapeStorage>,
206    shape_storage: Vec<F::Storage>,
207    workspace: F::Workspace,
208
209    /// Output images for this specific tile
210    out: Image,
211}
212
213impl<'a, F: Function> RenderWorker<'a, F> for Worker<'a, F> {
214    type Config = RenderConfig<'a>;
215    type Output = Image;
216
217    fn new(
218        cfg: &'a Self::Config,
219        tile_sizes: TileSizesRef<'a>,
220        vars: &'a ShapeVars<f32>,
221    ) -> Self {
222        let transform = cfg.mat();
223        let buf_size = tile_sizes.last();
224        let scratch = Scratch::new(buf_size);
225        Worker {
226            tile_sizes,
227            vars,
228
229            scratch,
230            out: Default::default(),
231
232            transform,
233            image_size: cfg.image_size,
234
235            eval_float_slice: Default::default(),
236            eval_interval: Default::default(),
237            eval_grad_slice: Default::default(),
238
239            tape_storage: vec![],
240            shape_storage: vec![],
241            workspace: Default::default(),
242        }
243    }
244
245    fn render_tile(
246        &mut self,
247        shape: &mut RenderHandle<F>,
248        tile: Tile<2>,
249    ) -> Self::Output {
250        // Prepare local tile data to fill out
251        let root_tile_size = self.tile_sizes[0];
252        self.out = Image::new(RenderSize::from(root_tile_size as u32));
253        for k in (0..self.image_size[2].div_ceil(root_tile_size as u32)).rev() {
254            let tile = Tile::new(Point3::new(
255                tile.corner.x,
256                tile.corner.y,
257                k as usize * root_tile_size,
258            ));
259            if !self.render_tile_recurse(shape, 0, tile) {
260                break;
261            }
262        }
263        std::mem::take(&mut self.out)
264    }
265}
266
267impl<F: Function> Worker<'_, F> {
268    /// Returns the data offset of a row within a subtile
269    pub(crate) fn tile_row_offset(&self, tile: Tile<3>, row: usize) -> usize {
270        self.tile_sizes.pixel_offset(tile.add(Vector2::new(0, row)))
271    }
272
273    /// Render a single tile
274    ///
275    /// Returns `true` if we should keep rendering, `false` otherwise
276    fn render_tile_recurse(
277        &mut self,
278        shape: &mut RenderHandle<F>,
279        depth: usize,
280        tile: Tile<3>,
281    ) -> bool {
282        // Early exit if every single pixel is filled
283        let tile_size = self.tile_sizes[depth];
284        let fill_z = (tile.corner[2] + tile_size + 1).try_into().unwrap();
285        if (0..tile_size).all(|y| {
286            let i = self.tile_row_offset(tile, y);
287            (0..tile_size).all(|x| self.out[i + x].depth >= fill_z)
288        }) {
289            return false;
290        }
291
292        let base = Point3::from(tile.corner).cast::<f32>();
293        let x = Interval::new(base.x, base.x + tile_size as f32);
294        let y = Interval::new(base.y, base.y + tile_size as f32);
295        let z = Interval::new(base.z, base.z + tile_size as f32);
296
297        let (i, trace) = self
298            .eval_interval
299            .eval_with_transform_and_vars(
300                shape.i_tape(&mut self.tape_storage),
301                x,
302                y,
303                z,
304                &self.transform,
305                self.vars,
306            )
307            .unwrap();
308
309        // Return early if this tile is completely empty or full, returning
310        // `data_interval` to scratch memory for reuse.
311        if i.upper() < 0.0 {
312            for y in 0..tile_size {
313                let i = self.tile_row_offset(tile, y);
314                for x in 0..tile_size {
315                    self.out[i + x].depth = self.out[i + x].depth.max(fill_z);
316                }
317            }
318            return false; // completely full, stop rendering
319        } else if i.lower() > 0.0 {
320            return true; // complete empty, keep going
321        }
322
323        // Calculate a simplified tape based on the trace
324        let sub_tape = if let Some(trace) = trace.as_ref() {
325            shape.simplify(
326                trace,
327                &mut self.workspace,
328                &mut self.shape_storage,
329                &mut self.tape_storage,
330            )
331        } else {
332            shape
333        };
334
335        // Recurse!
336        if let Some(next_tile_size) = self.tile_sizes.get(depth + 1) {
337            let n = tile_size / next_tile_size;
338
339            for j in 0..n {
340                for i in 0..n {
341                    for k in (0..n).rev() {
342                        self.render_tile_recurse(
343                            sub_tape,
344                            depth + 1,
345                            Tile::new(
346                                tile.corner
347                                    + Vector3::new(i, j, k) * next_tile_size,
348                            ),
349                        );
350                    }
351                }
352            }
353        } else {
354            self.render_tile_pixels(sub_tape, tile_size, tile);
355        };
356        // TODO recycle something here?
357        true // keep going
358    }
359
360    fn render_tile_pixels(
361        &mut self,
362        shape: &mut RenderHandle<F>,
363        tile_size: usize,
364        tile: Tile<3>,
365    ) {
366        // Prepare for pixel-by-pixel evaluation
367        let mut index = 0;
368        assert!(self.scratch.x.len() >= tile_size.pow(3));
369        assert!(self.scratch.y.len() >= tile_size.pow(3));
370        assert!(self.scratch.z.len() >= tile_size.pow(3));
371        self.scratch.columns.clear();
372        for xy in 0..tile_size.pow(2) {
373            let i = xy % tile_size;
374            let j = xy / tile_size;
375
376            let o = self.tile_sizes.pixel_offset(tile.add(Vector2::new(i, j)));
377
378            // Skip pixels which are behind the image
379            let zmax = (tile.corner[2] + tile_size).try_into().unwrap();
380            if self.out[o].depth >= zmax {
381                continue;
382            }
383
384            for k in (0..tile_size).rev() {
385                // SAFETY:
386                // Index cannot exceed tile_size**3, which is (a) the size
387                // that we allocated in `Scratch::new` and (b) checked by
388                // assertions above.
389                //
390                // Using unsafe indexing here is a roughly 2.5% speedup,
391                // since this is the hottest loop.
392                unsafe {
393                    *self.scratch.x.get_unchecked_mut(index) =
394                        (tile.corner[0] + i) as f32;
395                    *self.scratch.y.get_unchecked_mut(index) =
396                        (tile.corner[1] + j) as f32;
397                    *self.scratch.z.get_unchecked_mut(index) =
398                        (tile.corner[2] + k) as f32;
399                }
400                index += 1;
401            }
402            self.scratch.columns.push(xy);
403        }
404        let size = index;
405        assert!(size > 0);
406
407        let out = self
408            .eval_float_slice
409            .eval_with_transform_and_vars(
410                shape.f_tape(&mut self.tape_storage),
411                &self.scratch.x[..index],
412                &self.scratch.y[..index],
413                &self.scratch.z[..index],
414                &self.transform,
415                self.vars,
416            )
417            .unwrap();
418
419        // We're iterating over a few things simultaneously
420        // - col refers to the xy position in the tile
421        // - grad refers to points that we must do gradient evaluation on
422        let mut grad = 0;
423        let mut depth = out.chunks(tile_size);
424        for col in 0..self.scratch.columns.len() {
425            // Find the first set pixel in the column
426            let depth = depth.next().unwrap();
427            let k = match depth.iter().enumerate().find(|(_, d)| **d < 0.0) {
428                Some((i, _)) => i,
429                None => continue,
430            };
431
432            // Get X and Y values from the `columns` array.  Note that we can't
433            // iterate over the array directly because we're also modifying it
434            // (below)
435            let xy = self.scratch.columns[col];
436            let i = xy % tile_size;
437            let j = xy / tile_size;
438
439            // Flip Z value, since voxels are packed front-to-back
440            let k = tile_size - 1 - k;
441
442            // Set the depth of the pixel
443            let o = self.tile_sizes.pixel_offset(tile.add(Vector2::new(i, j)));
444            let z = (tile.corner[2] + k + 1).try_into().unwrap();
445            assert!(self.out[o].depth < z);
446            self.out[o].depth = z;
447
448            // Prepare to do gradient rendering of this point.
449            // We step one voxel above the surface to reduce
450            // glitchiness on edges and corners, where rendering
451            // inside the surface could pick the wrong normal.
452            self.scratch.xg[grad] =
453                Grad::new((tile.corner[0] + i) as f32, 1.0, 0.0, 0.0);
454            self.scratch.yg[grad] =
455                Grad::new((tile.corner[1] + j) as f32, 0.0, 1.0, 0.0);
456            self.scratch.zg[grad] =
457                Grad::new((tile.corner[2] + k) as f32, 0.0, 0.0, 1.0);
458
459            // This can only be called once per iteration, so we'll
460            // never overwrite parts of columns that are still used
461            // by the outer loop
462            self.scratch.columns[grad] = o;
463            grad += 1;
464        }
465
466        if grad > 0 {
467            let out = self
468                .eval_grad_slice
469                .eval_with_transform_and_vars(
470                    shape.g_tape(&mut self.tape_storage),
471                    &self.scratch.xg[..grad],
472                    &self.scratch.yg[..grad],
473                    &self.scratch.zg[..grad],
474                    &self.transform,
475                    self.vars,
476                )
477                .unwrap();
478
479            for (index, o) in self.scratch.columns[0..grad].iter().enumerate() {
480                let g = out[index];
481                self.out[*o].normal = [g.dx, g.dy, g.dz];
482            }
483        }
484    }
485}
486
487////////////////////////////////////////////////////////////////////////////////
488
489/// Renders the given shape into a 3D image with a particular configuration
490/// configuration.
491///
492/// The shape provides the evaluator backend (`F`) and bound variables; the
493/// configuration supplies resolution, transforms, etc.
494///
495/// Returns [`Some(Image)`](Image) of pixel data on success, or `None` if
496/// the render was cancelled.
497pub fn render<F: Function + RenderHints>(
498    b: BoundShape<F, f32>,
499    config: &RenderConfig,
500) -> Option<Image> {
501    let shape = b.shape().clone();
502    let vars = b.vars();
503    let max_size = config.width().max(config.height()) as usize;
504    let default_tile_sizes;
505
506    let tile_sizes = if let Some(ts) = &config.tile_sizes {
507        TileSizesRef::new(ts, max_size)
508    } else {
509        default_tile_sizes = F::tile_sizes_3d();
510        TileSizesRef::new(&default_tile_sizes, max_size)
511    };
512    let tiles =
513        super::render_tiles::<F, Worker<F>>(shape, vars, config, tile_sizes)?;
514
515    let width = config.image_size.width() as usize;
516    let height = config.image_size.height() as usize;
517    let mut image = Image::new(config.image_size);
518    for (tile, out) in tiles {
519        let mut index = 0;
520        for j in 0..tile_sizes[0] {
521            let y = j + tile.corner.y;
522            for i in 0..tile_sizes[0] {
523                let x = i + tile.corner.x;
524                if x < width && y < height {
525                    let o = y * width + x;
526                    if out[index].depth >= image[o].depth {
527                        // Clamp voxels to the image depth
528                        let d = config.image_size.depth() - 1;
529                        if out[index].depth >= d {
530                            image[o] = GeometryPixel {
531                                depth: d + 1,
532                                normal: [0.0, 0.0, 1.0],
533                            };
534                        } else {
535                            image[o] = out[index];
536                        }
537                    }
538                }
539                index += 1;
540            }
541        }
542    }
543    Some(image)
544}
545
546#[cfg(test)]
547mod test {
548    use super::*;
549    use fidget_core::{Context, var::Var, vm::VmShape};
550
551    /// Make sure we don't crash if there's only a single tile
552    #[test]
553    fn test_tile_queues() {
554        let mut ctx = Context::new();
555        let x = ctx.x();
556        let shape = VmShape::new(&ctx, x).unwrap().try_into().unwrap();
557
558        let cfg = RenderConfig::from_size(128.into()); // very small
559        let image = cfg.run(shape).expect("rendering should not be cancelled");
560        assert_eq!(image.len(), 128 * 128);
561    }
562
563    #[test]
564    fn cancel_render() {
565        let mut ctx = Context::new();
566        let x = ctx.x();
567        let shape = VmShape::new(&ctx, x).unwrap().try_into().unwrap();
568
569        let cfg = RenderConfig::from_size(64.into());
570        let cancel = cfg.cancel.clone();
571        cancel.cancel();
572        assert!(cfg.run::<_>(shape).is_none());
573    }
574
575    #[test]
576    fn shape_with_var() {
577        let mut ctx = Context::new();
578        let x = ctx.x();
579        let var = Var::new();
580        let v = ctx.var(var);
581        let s = ctx.sub(x, v).unwrap();
582        let shape = VmShape::new(&ctx, s).unwrap();
583
584        let cfg = RenderConfig::from_size(64.into());
585
586        let mut vars = ShapeVars::new();
587        let i = var.index().expect("expected Var::V");
588        vars.insert(i, 1.0);
589        cfg.run::<_>(shape.bind(&vars).expect("all vars present"))
590            .expect("not cancelled");
591    }
592}