use super::RenderHandle;
use crate::{
Image as GenericImage, RenderSize as _, RenderWorker, Tile, TileSizesRef,
};
use fidget_core::{
eval::Function,
render::{CancelToken, RenderHints, ThreadPool, TileSizes},
shape::{BoundShape, ShapeBulkEval, ShapeTracingEval, ShapeVars},
types::{Grad, Interval},
};
use nalgebra::{Matrix4, Point3, Vector2, Vector3};
use zerocopy::{FromBytes, Immutable, IntoBytes};
pub type Image = GenericImage<GeometryPixel, RenderSize>;
pub type RenderSize = fidget_core::render::VoxelSize;
pub struct RenderConfig<'a> {
pub image_size: RenderSize,
pub world_to_model: Matrix4<f32>,
pub tile_sizes: Option<TileSizes>,
pub threads: Option<&'a ThreadPool>,
pub cancel: CancelToken,
}
impl crate::RenderConfig for RenderConfig<'_> {
fn threads(&self) -> Option<&ThreadPool> {
self.threads
}
fn is_cancelled(&self) -> bool {
self.cancel.is_cancelled()
}
}
impl crate::RenderSize for RenderConfig<'_> {
fn width(&self) -> u32 {
self.image_size.width()
}
fn height(&self) -> u32 {
self.image_size.height()
}
}
impl RenderConfig<'_> {
pub fn from_size(image_size: RenderSize) -> Self {
Self {
image_size,
tile_sizes: None,
world_to_model: Matrix4::identity(),
threads: Some(&ThreadPool::Global),
cancel: CancelToken::new(),
}
}
pub fn run<F: Function + RenderHints>(
&self,
shape: BoundShape<F, f32>,
) -> Option<Image> {
render(shape, self)
}
pub fn mat(&self) -> Matrix4<f32> {
self.world_to_model * self.image_size.screen_to_world()
}
}
#[repr(C)]
#[derive(
Debug, Default, Copy, Clone, IntoBytes, FromBytes, Immutable, PartialEq,
)]
pub struct GeometryPixel {
pub normal: [f32; 3],
pub depth: u32,
}
impl GeometryPixel {
pub fn to_color(&self) -> [u8; 3] {
let [dx, dy, dz] = self.normal;
let s = (dx.powi(2) + dy.powi(2) + dz.powi(2)).sqrt();
if s != 0.0 {
let scale = u8::MAX as f32 / s;
[
(dx.abs() * scale) as u8,
(dy.abs() * scale) as u8,
(dz.abs() * scale) as u8,
]
} else {
[0; 3]
}
}
}
struct Scratch {
x: Vec<f32>,
y: Vec<f32>,
z: Vec<f32>,
xg: Vec<Grad>,
yg: Vec<Grad>,
zg: Vec<Grad>,
columns: Vec<usize>,
}
impl Scratch {
fn new(tile_size: usize) -> Self {
let size2 = tile_size.pow(2);
let size3 = tile_size.pow(3);
Self {
x: vec![0.0; size3],
y: vec![0.0; size3],
z: vec![0.0; size3],
xg: vec![Grad::from(0.0); size2],
yg: vec![Grad::from(0.0); size2],
zg: vec![Grad::from(0.0); size2],
columns: vec![0; size2],
}
}
}
struct Worker<'a, F: Function> {
tile_sizes: TileSizesRef<'a>,
vars: &'a ShapeVars<f32>,
transform: nalgebra::Matrix4<f32>,
image_size: RenderSize,
scratch: Scratch,
eval_float_slice: ShapeBulkEval<F::FloatSliceEval>,
eval_grad_slice: ShapeBulkEval<F::GradSliceEval>,
eval_interval: ShapeTracingEval<F::IntervalEval>,
tape_storage: Vec<F::TapeStorage>,
shape_storage: Vec<F::Storage>,
workspace: F::Workspace,
out: Image,
}
impl<'a, F: Function> RenderWorker<'a, F> for Worker<'a, F> {
type Config = RenderConfig<'a>;
type Output = Image;
fn new(
cfg: &'a Self::Config,
tile_sizes: TileSizesRef<'a>,
vars: &'a ShapeVars<f32>,
) -> Self {
let transform = cfg.mat();
let buf_size = tile_sizes.last();
let scratch = Scratch::new(buf_size);
Worker {
tile_sizes,
vars,
scratch,
out: Default::default(),
transform,
image_size: cfg.image_size,
eval_float_slice: Default::default(),
eval_interval: Default::default(),
eval_grad_slice: Default::default(),
tape_storage: vec![],
shape_storage: vec![],
workspace: Default::default(),
}
}
fn render_tile(
&mut self,
shape: &mut RenderHandle<F>,
tile: Tile<2>,
) -> Self::Output {
let root_tile_size = self.tile_sizes[0];
self.out = Image::new(RenderSize::from(root_tile_size as u32));
for k in (0..self.image_size[2].div_ceil(root_tile_size as u32)).rev() {
let tile = Tile::new(Point3::new(
tile.corner.x,
tile.corner.y,
k as usize * root_tile_size,
));
if !self.render_tile_recurse(shape, 0, tile) {
break;
}
}
std::mem::take(&mut self.out)
}
}
impl<F: Function> Worker<'_, F> {
pub(crate) fn tile_row_offset(&self, tile: Tile<3>, row: usize) -> usize {
self.tile_sizes.pixel_offset(tile.add(Vector2::new(0, row)))
}
fn render_tile_recurse(
&mut self,
shape: &mut RenderHandle<F>,
depth: usize,
tile: Tile<3>,
) -> bool {
let tile_size = self.tile_sizes[depth];
let fill_z = (tile.corner[2] + tile_size + 1).try_into().unwrap();
if (0..tile_size).all(|y| {
let i = self.tile_row_offset(tile, y);
(0..tile_size).all(|x| self.out[i + x].depth >= fill_z)
}) {
return false;
}
let base = Point3::from(tile.corner).cast::<f32>();
let x = Interval::new(base.x, base.x + tile_size as f32);
let y = Interval::new(base.y, base.y + tile_size as f32);
let z = Interval::new(base.z, base.z + tile_size as f32);
let (i, trace) = self
.eval_interval
.eval_with_transform_and_vars(
shape.i_tape(&mut self.tape_storage),
x,
y,
z,
&self.transform,
self.vars,
)
.unwrap();
if i.upper() < 0.0 {
for y in 0..tile_size {
let i = self.tile_row_offset(tile, y);
for x in 0..tile_size {
self.out[i + x].depth = self.out[i + x].depth.max(fill_z);
}
}
return false; } else if i.lower() > 0.0 {
return true; }
let sub_tape = if let Some(trace) = trace.as_ref() {
shape.simplify(
trace,
&mut self.workspace,
&mut self.shape_storage,
&mut self.tape_storage,
)
} else {
shape
};
if let Some(next_tile_size) = self.tile_sizes.get(depth + 1) {
let n = tile_size / next_tile_size;
for j in 0..n {
for i in 0..n {
for k in (0..n).rev() {
self.render_tile_recurse(
sub_tape,
depth + 1,
Tile::new(
tile.corner
+ Vector3::new(i, j, k) * next_tile_size,
),
);
}
}
}
} else {
self.render_tile_pixels(sub_tape, tile_size, tile);
};
true }
fn render_tile_pixels(
&mut self,
shape: &mut RenderHandle<F>,
tile_size: usize,
tile: Tile<3>,
) {
let mut index = 0;
assert!(self.scratch.x.len() >= tile_size.pow(3));
assert!(self.scratch.y.len() >= tile_size.pow(3));
assert!(self.scratch.z.len() >= tile_size.pow(3));
self.scratch.columns.clear();
for xy in 0..tile_size.pow(2) {
let i = xy % tile_size;
let j = xy / tile_size;
let o = self.tile_sizes.pixel_offset(tile.add(Vector2::new(i, j)));
let zmax = (tile.corner[2] + tile_size).try_into().unwrap();
if self.out[o].depth >= zmax {
continue;
}
for k in (0..tile_size).rev() {
unsafe {
*self.scratch.x.get_unchecked_mut(index) =
(tile.corner[0] + i) as f32;
*self.scratch.y.get_unchecked_mut(index) =
(tile.corner[1] + j) as f32;
*self.scratch.z.get_unchecked_mut(index) =
(tile.corner[2] + k) as f32;
}
index += 1;
}
self.scratch.columns.push(xy);
}
let size = index;
assert!(size > 0);
let out = self
.eval_float_slice
.eval_with_transform_and_vars(
shape.f_tape(&mut self.tape_storage),
&self.scratch.x[..index],
&self.scratch.y[..index],
&self.scratch.z[..index],
&self.transform,
self.vars,
)
.unwrap();
let mut grad = 0;
let mut depth = out.chunks(tile_size);
for col in 0..self.scratch.columns.len() {
let depth = depth.next().unwrap();
let k = match depth.iter().enumerate().find(|(_, d)| **d < 0.0) {
Some((i, _)) => i,
None => continue,
};
let xy = self.scratch.columns[col];
let i = xy % tile_size;
let j = xy / tile_size;
let k = tile_size - 1 - k;
let o = self.tile_sizes.pixel_offset(tile.add(Vector2::new(i, j)));
let z = (tile.corner[2] + k + 1).try_into().unwrap();
assert!(self.out[o].depth < z);
self.out[o].depth = z;
self.scratch.xg[grad] =
Grad::new((tile.corner[0] + i) as f32, 1.0, 0.0, 0.0);
self.scratch.yg[grad] =
Grad::new((tile.corner[1] + j) as f32, 0.0, 1.0, 0.0);
self.scratch.zg[grad] =
Grad::new((tile.corner[2] + k) as f32, 0.0, 0.0, 1.0);
self.scratch.columns[grad] = o;
grad += 1;
}
if grad > 0 {
let out = self
.eval_grad_slice
.eval_with_transform_and_vars(
shape.g_tape(&mut self.tape_storage),
&self.scratch.xg[..grad],
&self.scratch.yg[..grad],
&self.scratch.zg[..grad],
&self.transform,
self.vars,
)
.unwrap();
for (index, o) in self.scratch.columns[0..grad].iter().enumerate() {
let g = out[index];
self.out[*o].normal = [g.dx, g.dy, g.dz];
}
}
}
}
pub fn render<F: Function + RenderHints>(
b: BoundShape<F, f32>,
config: &RenderConfig,
) -> Option<Image> {
let shape = b.shape().clone();
let vars = b.vars();
let max_size = config.width().max(config.height()) as usize;
let default_tile_sizes;
let tile_sizes = if let Some(ts) = &config.tile_sizes {
TileSizesRef::new(ts, max_size)
} else {
default_tile_sizes = F::tile_sizes_3d();
TileSizesRef::new(&default_tile_sizes, max_size)
};
let tiles =
super::render_tiles::<F, Worker<F>>(shape, vars, config, tile_sizes)?;
let width = config.image_size.width() as usize;
let height = config.image_size.height() as usize;
let mut image = Image::new(config.image_size);
for (tile, out) in tiles {
let mut index = 0;
for j in 0..tile_sizes[0] {
let y = j + tile.corner.y;
for i in 0..tile_sizes[0] {
let x = i + tile.corner.x;
if x < width && y < height {
let o = y * width + x;
if out[index].depth >= image[o].depth {
let d = config.image_size.depth() - 1;
if out[index].depth >= d {
image[o] = GeometryPixel {
depth: d + 1,
normal: [0.0, 0.0, 1.0],
};
} else {
image[o] = out[index];
}
}
}
index += 1;
}
}
}
Some(image)
}
#[cfg(test)]
mod test {
use super::*;
use fidget_core::{Context, var::Var, vm::VmShape};
#[test]
fn test_tile_queues() {
let mut ctx = Context::new();
let x = ctx.x();
let shape = VmShape::new(&ctx, x).unwrap().try_into().unwrap();
let cfg = RenderConfig::from_size(128.into()); let image = cfg.run(shape).expect("rendering should not be cancelled");
assert_eq!(image.len(), 128 * 128);
}
#[test]
fn cancel_render() {
let mut ctx = Context::new();
let x = ctx.x();
let shape = VmShape::new(&ctx, x).unwrap().try_into().unwrap();
let cfg = RenderConfig::from_size(64.into());
let cancel = cfg.cancel.clone();
cancel.cancel();
assert!(cfg.run::<_>(shape).is_none());
}
#[test]
fn shape_with_var() {
let mut ctx = Context::new();
let x = ctx.x();
let var = Var::new();
let v = ctx.var(var);
let s = ctx.sub(x, v).unwrap();
let shape = VmShape::new(&ctx, s).unwrap();
let cfg = RenderConfig::from_size(64.into());
let mut vars = ShapeVars::new();
let i = var.index().expect("expected Var::V");
vars.insert(i, 1.0);
cfg.run::<_>(shape.bind(&vars).expect("all vars present"))
.expect("not cancelled");
}
}