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