1use 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
16pub type Image = GenericImage<GeometryPixel, RenderSize>;
18
19pub type RenderSize = fidget_core::render::VoxelSize;
21
22pub struct RenderConfig<'a> {
26 pub image_size: RenderSize,
32
33 pub world_to_model: Matrix4<f32>,
35
36 pub tile_sizes: Option<TileSizes>,
41
42 pub threads: Option<&'a ThreadPool>,
47
48 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 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 pub fn run<F: Function + RenderHints>(
101 &self,
102 shape: BoundShape<F, f32>,
103 ) -> Option<Image> {
104 render(shape, self)
105 }
106
107 pub fn mat(&self) -> Matrix4<f32> {
109 self.world_to_model * self.image_size.screen_to_world()
110 }
111}
112
113#[repr(C)]
124#[derive(
125 Debug, Default, Copy, Clone, IntoBytes, FromBytes, Immutable, PartialEq,
126)]
127pub struct GeometryPixel {
128 pub normal: [f32; 3],
130 pub depth: u32,
135}
136
137impl GeometryPixel {
138 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
155struct 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 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
189struct 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 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 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 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 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 fn render_tile_recurse(
277 &mut self,
278 shape: &mut RenderHandle<F>,
279 depth: usize,
280 tile: Tile<3>,
281 ) -> bool {
282 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 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; } else if i.lower() > 0.0 {
320 return true; }
322
323 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 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 true }
359
360 fn render_tile_pixels(
361 &mut self,
362 shape: &mut RenderHandle<F>,
363 tile_size: usize,
364 tile: Tile<3>,
365 ) {
366 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 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 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 let mut grad = 0;
423 let mut depth = out.chunks(tile_size);
424 for col in 0..self.scratch.columns.len() {
425 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 let xy = self.scratch.columns[col];
436 let i = xy % tile_size;
437 let j = xy / tile_size;
438
439 let k = tile_size - 1 - k;
441
442 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 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 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
487pub 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 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 #[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()); 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}